I don't understand how await is a replacement for yield

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By pangolinmontanari

In Godot 4, yield has been removed and await is marked as a replacement, but they don’t appear to function comparably.
I’m reading the docs on the subject.

My understanding was that yield allowed you to pause a function part way through, storing its state, and then continue it with another call. It’s useful if you have a function with a loop in it that returns a number of values or does a certain thing for a period of time(for example, once per process loop).

await, on the other hand… I’m actually having trouble understanding what it’s actually useful for. It needs signals to operate at all? And it doesn’t allow for looping. It’s so different that I’m having trouble understanding why it’s considered a replacement. Like it can do one of the things yield could do?

:bust_in_silhouette: Reply From: Juxxec

The await keyword functions the exact same way the yield keyword does. The only thing that changes here is the syntax.

Before you would use:

yield(get_tree().create_timer(1.0), "timeout")

var value = yield(some_function(), "completed")

Now you simply use:

await get_tree().create_timer(1.0).timeout

var value = await some_function()

I haven’t tested the last part, but I am pretty sure it should work as before. The difference here is that you do not use the completed signal as before when waiting for values, as opposed to signals.

Personally, I prefer the old yield syntax as it is more Python like. The new await keyword is more reminiscent of javascript.

:bust_in_silhouette: Reply From: FancyMongoose

I had similar problems to you I think. The problem seems to be that GDScriptFunctionState was removed from the engine. This means that out of the box some scenarios like looping, etc are a bit tricky to handle. I solved this by writing a wrapper class the gives me similar behavior to GDScriptFunctionState. Maybe it will be helpful.

#simple wrapper that helps you in situations where you still need
#behavior similar to GDScriptFunctionState
class CoroutineWrapper:
	signal resumed
	signal stopped

	var completed = false

	func _init():
		stopped.connect(_on_stopped)
		run()

	func resume():
		resumed.emit()

	func run():
		pass

	func _on_stopped():
		for item in [resumed, stopped]:
			var sig: Signal = item
			for connection in sig.get_connections():
				var callable: Callable = connection.callable
				sig.disconnect(callable)
		completed = true

An example of usage.

extends Node
var count_to_ten: CountToTen

class Abc extends CoroutineWrapper:
	func run():
		print("\tA")
		await resumed
		print("\tB")
		await resumed
		print("\tC")
		stopped.emit()

class CountToTen extends CoroutineWrapper:
	func run():
		var idx = 0
		while idx <= 10:
			print(idx)
			if idx == 5 or idx == 2:
				var abc = Abc.new()
				while abc.completed == false:
					abc.resume()
					await resumed
			await resumed
			idx += 1
		stopped.emit()

func _ready():
	count_to_ten = CountToTen.new()

func _process(delta):
	if count_to_ten == null:
		return

	if count_to_ten.completed:
		print("Stopped")
		count_to_ten = null
		return
		
	count_to_ten.resume()

And the output

0
1
2
	A
	B
	C
3
4
5
	A
	B
	C
6
7
8
9
10
Stopped

It’s a little bit more work, but it helped me figure out how to port my 3.x code bases to 4.0. I use co-routines a lot. Hopefully it can help you too :slight_smile:

Thanks for putting this example up! Its great to see a fully working example with await and resume() nicely packaged into a single class.

mellowed | 2023-04-25 06:34

You are correct, await is a full replacement only for one case of yield usage. But the thing is you don’t need the other case any more. The old pattern where yield returned a function execution state object is now redundant and should now be replaced with awaiting signals. So the migration from yield is not always a trivial one.

Before, you could have a function, that contained a loop with logic, that shouldn’t be run every tick or every frame. E.g. a check that should be made every 10 seconds, or only when something happens. This could also be a function that returns different values upon each subsequent call, e.g. telling us how many of something we have left, or something like that. It was convenient to contain this loop in a separate function, a coroutine, i.e. a routine that could be thought of running at the same time with some main logic. It definitely ran periodically and thus definitely could be described with a loop and some “checkpoints”. Now, the thing about coroutine functions: they are called coroutines, because they know that they are going to be called from other places, and they know they are not simple functions. This is not a very intuitive pattern, in my opinion, especially for beginner game developers. But still, if you used such coroutines you would have another function (or functions) somewhere that called this coroutine function every time it/they needed. For example such a caller function would do it’s own business and then, say, wait 10 seconds, or calculate something, or just run a piece of game logic, and then periodically resume the coroutine. So the coroutine was simply a container of event handlers, that had to happen every once in a while, and the caller function was a “signal”, that called the coroutine and told it to execute the next handler. You could have just a collection of handler functions instead, but the coroutine could elegantly and conveniently express anything in a special place and even contain a loop to express some repetitiveness. You can see that I’ve put the signal word in quotation marks, because it has nothing to do with Godot signals, but one of the problems this coroutine pattern tried to solve was a similar problem signals do - connect a trigger with a handler.

Well, if coroutines and signals coexist solving similar problems (and can even meet and cooperate - the second usage of yield to wait for signals) then why not simplify everything and always use signals! That’s exactly what happened and what is now encouraged by Godot. If something has to happen every 10 seconds, we can still create something that emits signals every 10 second: a timer! Now, if we need a more sophisticated trigger, we have to find a way to express it with a signal. If before we used to call a coroutine continuation every time when, say, player jumped, then why not just declare a signal for it. And then we could just consume the events: e.g. create a “background” procedure that starts in _ready() and, in a loop, awaits the jumping signal and runs the handler logic in the same function! Or just awaits this signal from any place in the game code. Well, you know what signals are. Every time we called a coroutine before, we need to figure out what signal can we await to make it behave the same way.

This is a very nice exercise for a game developer: “how would I implement it with signals?”. “If it needs to happen every once in a while, what signals should I use/create?”. Coroutines are harder way to do it, that’s why, in my opinion, the developers got rid of yield.