call_deferred, yield & Threads in Godot

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
hey everyone and welcome to another episode I'm Ombarus and this week we're talking about treading, asynchronous tasks in gdscript and the reason we're talking about this this week is because for the past couple of days I've been fighting with my game Solar Rogue to try to avoid the lag spike that comes with writing the savefile to the disk and the way I found to avoid that is to try to write this file in a thread instead of in the main thread so that it doesn't lock the main thread when it's writing the file to the disk and so since I got into a couple of different issues doing this and I had to look at the documentation and all that and learn a lot about all the different nifty ways that gdscript allows you to do asynchronous tasks I thought I'd make a video about it the first method I want to talk about is call_deferred() and it's not really thread related per see but it's very useful to delay the execution of some piece of code at the time where you're certain that nothing else is running and the reason you'd want to do that is because some code like manipulating the scene tree with add_child() for example needs to run on the main thread while nothing else is modifying the scene tree and the way to do this is to use call_deferred() to make sure that this will be executed in between two frame where it's safe to do operation on physics or the scene tree or other stuff like that. For some methods in gdscript it's so important that you never call it without call_deferred() that they even integrated options to make sure that it's always deferred the free operation for deleting a node now is called queue_free() because it's basically going to guarantee that the free method is going to be called as a call_deferred. Then there's various flavors of yield() which looks like a very simple method but can do some really powerful stuff. What it basically does is that it interrupts the execution of the current method until some kind of event is fired and then it resumes where it left and this can become really powerful used in the right hands. One of the most popular way to use it that you'll see in the documentation is to create a timer call yield on it and return when the timer has finished this allows you in one single method to run multiple pieces of code for example in my visual effects what I'm gonna do is I'm gonna create the effect then call the timer to wait for the animation of the effect to finish and then I'm gonna delete the object all in the same method just with this little yield that will allow for the effects to play until it's finished and then it gets deleted this is really powerful but one of my favorite way to use it personally is in the loading screen so while I'm loading my stuff I want the loading screen to keep updating and to do this what I do is that I start loading my stuff which is usually in a kind of really long for loop for all the objects I want to load but I check the time and if too much time has passed then I will call the yield() which will allow the main thread to update itself and update the visual of the game and then come back to my method to continue where it left off without me having to do anything complicated with treads and semaphore and and mutexes and stuff like that so this is a really nice trick you can use also with yield but you have to be a little bit careful because honestly yield can become really hard to follow for example let's say you have method1 that calls method2 and method2 has a yield in it that means that when method2 yields it will return the execution to method1 right away and since method1 is not waiting on method2 then the execution will keep on going and when an event is fired then method2 will restart and complete but since it already returned to method1 then it's just going to go back into the void so if you had like a return of some value or something then you know it's not gonna work but let's say you call method2 from method1 but this time you wrap it in a yield and await the event "completed" which is a little trick for doing something like awaits in c# using gdscript well when you do that method2 will yield but method1 will also yield at that point and when method2 restarts and completes then method1 will also restart so by chaining yields like that you can basically cut off a whole branch of code that will all resume when one single event is fired which is really powerful but you have to be a little bit careful because I've had this issue multiple time where one of the method in my chain didn't yield as expected for some reason either I forgot to put the yield in it or maybe it failed some validation and decided to return early and if the method you're calling with yield returns null then gdscript will crash with something like 'first argument to yield must not be null' and the way I found to avoid that is to put a yield(self, 'idleframe') at the beginning of your method so that any validation that will return early will still make this method yield-able. So one of the most advanced stuff you can do with yield is something close to functional programming now I'll admit I know basically nothing about functional programming so if I'm wrong here I'm sorry but one of the thing I find really nice in c# is something called LINQ which allow you to basically write SQL-like bunch of methods that will all resolve when you pass it some data so it's kind of a cool way of doing some processing and it's very readable when you read it, it reads like just plain English and another cool usage I've seen with this is often in UI design so especially in games where often you want to chain multiple actions one after the other, yield() can be a powerful tool to simplify a whole huge bunch of if-else that would otherwise look incredibly ugly for example let's say you're making a fighting game and you want some combo move that's like up-down-up-down-punch. Well one of the way you could do it which is with a whole lot of timers and if-else and and really ugly code and another way you can do it now is with yield so for example what you could have is a method that awaits an input and when it receives this input as an event then it waits for the next input and the next input and it counts the time it took to do all these input and if the time is less than let's say 0.2 second then you trigger the combo and you start waiting again now the only thing that's missing in this is the cancellation so if the timer runs out you need to go back to the beginning of your you know sequence and I'm not quite sure how you would do that but I think it would be feasible and let's admit a whole bunch of yield like that it's so much easier and cleaner to read then like the equivalent you would need in if-else to make sure that this works this brings us to the actual creation and management of threads, mutexes and semaphores in gdscript and the way to do that is not so bad all you have to do is Thread.new() to create your thread and then when you want to execute some method in a child process all you have to do is call Thread.start() with the method name and a bunch of parameters if you have some and it will launch this method in a child process which won't lock the main thread now when you want to finish the child process you have to call wait_to_finish() on the main tread and this is a little bit weird because there's no way to know if the method you started in your child process has finished execution and when you call wait_to_finish() it's going to block until the method in the child process has completed and I haven't found a way to check without locking if the method is finished and I don't want to like create a five second timer or something to assume that the method has finished that that's not... that's not a good way of doing it. So the way I found is that I call wait_to_finish() from my child process at the end of my method but it needs to be called on the main thread but if you remember correctly I said that call_differed() will execute on the main trade in an idle part of the frame so it's perfect I do call_deferred('wait_to_finish') on my thread so that means that in the next frame then the execution of my thread will have completed and I can use is_active() to know if my save game has finished saving or not and if it's finished then I know I can start another save and if it hasn't then I wait until it's finished and it all happens automatically I could probably have done something similar with mutexes and semaphore but why use so complicated methods when this just works fine I don't know if it's a legit way/legal way of doing it but for me it works pretty well. Now I mentioned semaphore and mutexes and I haven't shown how to use them in Godot because honestly I think it's a little bit outside the scope of this video I mean all programming languages have mutexes and semaphore so if you need to brush up on your threading generalities then I suggest you read the documentation that I will link below in this video and you can just check up the more advanced stuff in the Godot documentation which is really good and there you have it, that's pretty much everything I learned since I started working on my little project Solar Rogue about tasks and asynchronous stuff and multitasking and threading in Godot. You've made it to the end of the video that's really impressive and I thank you very much and I would really appreciate it if you haven't done so already you click the like button, click the subscribe and we'll see you guys in my next video bye!
Info
Channel: Ombarus
Views: 7,758
Rating: undefined out of 5
Keywords: Guillaume Bouvier, Ombarus, GameDev, Programming, Coding, Software Engineering, gdscript, Godot Game Engine, Multitasking, Multithreading, Multiprocess, yield, coroutines, await, async, savegame
Id: 4xMaq-qVQGY
Channel Id: undefined
Length: 10min 38sec (638 seconds)
Published: Tue Mar 10 2020
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.