2
\$\begingroup\$

I understand that the title is a bit vague, but I'd like to get a better sense of where it is appropriate to use yield(). Up until recently I wasn't even aware of yield(), but now that I know it exists, I've been using it a lot. Too much, I suspect. Take the following code for example:

#NPC set up
if hide_gameplay_npcs or hide_player:
    GUI.fade_out(1.0)
    yield(GUI,"fade_out_complete")
    
    if hide_gameplay_npcs: toggle_character_group(Globals.group_gameplay_npcs, false)
    if hide_player: toggle_character_group(Globals.group_player, false)
    yield(get_tree().create_timer(0.5),"timeout")
    
    GUI.fade_in(1.0)
    yield(GUI,"fade_in_complete")

Previously, this code would have been a massive headache. I need to hide these characters in between a lot of other set-up work, so I would have had to separate this functionality into a bunch of small, conditional functions held together by signal connections. Yield completely solves this problem and lets me keep one logical grouping of functionality together in one place.

The problem is that using this many yields in one code block is setting off huge red flags. It's more a hunch than anything, but this block of code feels like something that's easy to write, but bad in practice.

Am I missing something? Are there gotchas to yield() that I need to be aware of? Rules of thumb? I know not to use yield() in _process() or any other constantly recurring function, and to not use it on nodes that could easily be destroyed. But is there something else I'm missing?

\$\endgroup\$

1 Answer 1

6
\$\begingroup\$

Code Smell

We have a term for what you describe: it is a code smell.

Sometimes 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:

  • Can 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?

Using 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.

Using 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).

No cancellation

Sometimes you want to convert your yielding code into a state machine in _process.

Also, avoid 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 AnimationPlayer).

No auto-disconnection

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 queue_free or free to get them.
  • Avoid yielding on anything else, even if there is no queue_free or free that can get them, if the yield can be ongoing during tear down (when the scene is being unloaded).

Every Node will be destroyed at the end.


Error example

I decided to make a minimal example where yield fails. It looks like this:

func _ready() -> void:
    get_tree().change_scene("res://some_scene.tscn")
    yield(get_tree().create_timer(0.1), "timeout")
    print("DONE")

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 Node.

I also tried yielding on a signal from a Node that is being freed:

$other_node.queue_free()
yield($other_node, "some_signal")
print("DONE") # never happens

And to my surprise instead of an error, yield never returned.

What reminds an error is freeing the same Node:

queue_free()
yield($other_node, "some_signal")
print("DONE")

Refactoring yield

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:

yield(my_method(), "completed")

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:

call_deferred("emit_signal", "completed")

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

emit_signal("completed", result)

And if the method is returning the same object, then you want do this:

call_deferred("emit_signal", "completed", result)
\$\endgroup\$
2
  • \$\begingroup\$ This is super, super helpful. Thank you for the info! \$\endgroup\$
    – dahrasz
    Jan 2 at 17:58
  • \$\begingroup\$ One additional gotcha. If a function with yields inside is called multiple times while the old yields are in progress, then you have multiple yields running at the same time, resuming the same function at some arbitrary moments of time, creating very weird behavior. Generally if you use yield you have to make sure that the function with yields is never called a second time while previous yields are not finished. You especially don't want to call yield in _process or any other function that is called from _process or _physics_process. \$\endgroup\$
    – Exerion
    Jan 21 at 20:21

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .