Await stuck waiting for signal from custom Resource

Godot Version

4.6.1

Question

I have what seems like an extremely simple event system comprised of different custom resources all inheriting from the same type. There is a top-level “Event” resource that runs any number of “EventItem” resources in sequential order. The “Event” resource runs a for-loop that awaits an “event_item_finished” from each “EventItem” derived resource. Most work perfectly fine but some get completely stuck with the “Event” for-loop awaiting for the finished signal from only certain “EventItem” scripts. They are very simple scripts and I can’t for the life of me figure out why as there are no errors thrown and the debugging stack trace follows the correct order.

event.gd

class_name Event
extends Resource

signal events_finished

@export var events: Array[EventItem]

func run_event(_gm: GameManager) → Signal:
   for event_item in events:
      await event_item.run(_gm)
   return events_finished

event_item.gd

@abstract
class_name EventItem
extends Resource

signal event_item_finished

func run(_gm: GameManager) → Signal:
   return event_item_finished

event_item_currency.gd

class_name EventItemCurrency
extends EventItem

@export var amount: int

func run(_gm: GameManager) → Signal:
   File.progress.inventory.change_currency(amount)
   return event_item_finished

The currency one causes the “Event” for-loop to get stuck awaiting “event_item_finished” forever. The File.progress.inventory is a very simple autoloaded inventory resource:

inventory.gd

class_name Inventory
extends Resource

signal currency_changed

@export var currency: int

func change_currency(amount: int) → void:
   currency += amount
   currency = clampi(currency, 0, 99999)
   currency_changed.emit()

Who emits the signal?

The signal is returned from each EventItem’s run(). The odd thing is most of the other EventItem derived scripts work just fine and are more complex. For example, I have a dialog one that requests dialog from the top level game manager. The only difference is there is an additional await from another object:

event_item_dialog.gd

class_name EventItemDialog
extends EventItem

@export var speaker: String = “”
@export_multiline() var dialog_lines: Array[String]

func run(_gm: GameManager) → Signal:
   _gm.dialog.display_dialog(dialog_lines, speaker)
   await _gm.dialog.dialog_finished
   return event_item_finished

Am I missing something about how coroutines work? I will admit I don’t have the strongest understanding of code flow.

Yes, await waits for the signal to be emitted. Returning the signal object doesn’t emit the signal, and function that does so it not a coroutine. For example, the following will never print “done”

signal s

func _ready():
	print("start")
	await foo()
	print("done")

func foo():
	return s

Ok, I switched all EventItem scripts to emit the signal instead and they all work as intended. What is the use case for returning a Signal from a function then? I’ve read the docs section on await about 10 times and I guess the examples were a bit too succinct for me to understand the concept.

What’s the use case for returning any object from a function? To provide the object to the caller.

Judging from the code you posted so far, you really don’t need any signals or coroutines here. Just iterate and call the functions normally.

In some cases I do need to wait until the previous event finishes, such as a character animation or waiting for the player to advance dialog. Some events can run immediately while others need to wait. The idea was create a simple but flexible system that could accommodate any number of design decisions.

Then you can let run() await for the relevant signal and the caller await for run().

So basically I was overusing await and signals where they weren’t really needed. I think I understand enough to get things working as intended, thanks!

1 Like