We have a term for what you describe: it is a code smell.
yield can be an indication of a bigger problem. Also there are cases where using
yield is not safe.
However, I don't think the example is a bad use of
yield. In particular I would worry if:
GUI be freed prematurely?
- Can something else interact with
GUI messing up this code?
Are there gotchas to yield() that I need to be aware of?
Yes. There are a couple:
- No cancellation: you yield on a signal and there is no way to cancel that.
- No auto-disconnection: if the object that yielded is freed, you have a runtime error.
Not so much a gotcha of
yield, but of the scene tree timers: they don't pause when you pause the game (
get_tree().paused = true).
Rules of thumb
Rules of thumb?
yield on the correct object and signal will avoid timing issues. For example, if you need to wait for an
AudioStreamPlayer to finish, yield on the
AudioStreamPlayer instead of creating a timer.
yield on signals that must happen (that nothing will prevent the signal from happening) will avoid the code from being awaiting until tear down. The
AudioStream loops and never finishes, oops.
Addendum: I should mention that there is a delay when playing audio, so if you play audio on a finished signal of another audio, there is an audible gap (there is a silence, I mean). But a timer won't fix that (they don't have enough temporal resolution). You need a thread sleeping for the appropriate time. Thankfully, we rarely need that much precision. Try audio tracks in the
AnimationPlayer instead (and queue animations if needed).
Sometimes you want to convert your yielding code into a state machine in
yield inside a process that needs to cancel its prior execution (if there is a chance you will call the method before the prior execution finished, and it needs to operate on the same objects). In that case I suggest to use tweens, or animations (notice the call method tracks in the
Since there is no auto-disconnection, use
yield on things that will be there:
- The same object.
- The scene tree.
- Objects created for it.
Once you start yielding on other
Nodes you can start worrying. Is there a way for the other node to be freed independently? Can it happen while the code is awaiting in a yield?
- Yielding on an autoload is probably fine (you won't free those, right? RIGHT?).
- Yielding on children nodes might be fine, if there is no way for a
free to get them.
- Avoid yielding on anything else, even if there is no
free that can get them, if the yield can be ongoing during tear down (when the scene is being unloaded).
Node will be destroyed at the end.
I decided to make a minimal example where
yield fails. It looks like this:
func _ready() -> void:
If the path to the scene is invalid, that is the error you get.
But if the scene path is valid, after requesting the scene change the code will create the timer. While the code is awaiting the timer, the scene changes. And when the timer emits the
"timeout" signal, Godot will try to resume this code it is already gone since the scene changed, and that is the error.
Note that it didn't require to access another
I also tried yielding on a signal from a
Node that is being freed:
print("DONE") # never happens
And to my surprise instead of an error,
yield never returned.
What reminds an error is freeing the same
You might want to refactor to using signals connections. Signal connection auto-disconnect when the objects are freed.. In particular with
CONNECT_ONESHOT (which auto-disconnect when they are fired).
However, be aware you cannot have the same method of the same object connected to same signal multiple times. Which means that sometimes it is better to create an object - of a custom class - to represent the process. Once every process has an object, you can connect to its method without worry of collision.
If you are using
yield in a loop, the refactor becomes a bit more complicated, but it is worth it. You need to rotate (unroll) the instructions so that the check to continue iterating happens where the
yield is… Which also means you might have to repeat some instruction before the loop. Then you make a method for the iteration of the loop, and another method for the lead up to the loop. So the lead up method connects to the iteration method, and the iteration method connects to itself.
I want to mention that Godot 4 has anonymous functions ("lambda" functions), so you can do all of this inline, and you can avoid the extra class.
On return values
Know that a method that uses
yield returns a
GDScriptFunctionState that has a
completed signal. So you can do this:
But the method only returns
GDScriptFunctionState if it yields. Thus, make sure you can predict if a method will yield from its arguments. The easy way to do this is to make sure your methods either always yield, or never yield. This is so you know when and how you can yield on the method when calling it.
If you migrate said method to not use
yield, you can make it return an object with a "completed" signal, and you don't have to change the way you call it. Furthermore, the object it returns might be itself, in which case you might need to do this:
This is because the signal must happen after the method returned for yield to get it.
I'll also point out that
yield returns. It returns the first argument (if any) it gets from the signal. So you can do this:
var my_var = yield(my_method(), "completed")
Which is how it works if the method uses
yield and also returns.
If the method does not use
yield, but instead returns an object that has the signal, that object would have to emit it the signal like this
And if the method is returning the same object, then you want do this:
call_deferred("emit_signal", "completed", result)