Issues with await

Godot Version

v4.6.stable.official [89cea1439]

Question

Hi everyone!

I’ve been making a game for the past few months - I am a Gameplay Engineer on my 9-5 - and so far things have gone pretty smoothly :slight_smile:

However, I’ve been having a weird issue today, and after a few hours of just looking at it, I am completely stumped and so I am turning on to the wider community!

In a nutshell, I am awaiting a coroutine, however when the coroutine finishes, the code that called the coroutine does not resume. Now, I have been using await for the past few months without any issue, but in this particular scenario, I can repro this bug 100% - which tells me I messed things up somewhere.

I’ll get to the code in a minute, but to describe what it happening, the game I work on has a lot of actions happening (which are called effectin my code), and effects can react to other effects. I have an EffectManagerclass that takes care of all of that, and queuing up effects and reactions nicely. In this particular case, I have a unit dying, which triggers an effect (UnitDeath), which then goes on to trigger a reaction, which triggers more effects. It works well in most case, but I have one edge case where it fails 100% of the time.

In my code, I await for the UnitDeath effect to be done, however, the coroutine never finishes, despite extensive logging showing that the function called has completed.

Finally, here is some code to explain all that. First, the code of the unit itself, which will trigger the UnitDeath effect:

func take_damage(damage_amount: int) -> int:
	if ignore_damage or damage_amount <= 0:
		return 0
	var unblocked_damage_amount : int = _block_damage(damage_amount)
	if current_health > 0 and unblocked_damage_amount > 0:
		current_health = max(current_health - unblocked_damage_amount, 0)
		update_health_ui()
		animation_player.play(damage_animation)
		if current_health <= 0:
			Logging.log(UnitBase, "before")
			await create_death_effect()
            # This is sadly not reached
			Logging.log(UnitBase, "after")
	on_damage_taken.emit(unblocked_damage_amount, damage_amount - unblocked_damage_amount)
	return unblocked_damage_amount

func create_death_effect() -> void:
	var unit_death_effect := UnitDeath.new(self)
	await gameplay.effect_manager.execute_effect(unit_death_effect, true, true)

The important bit being within the if current_health <= 0:block:

Logging.log(UnitBase, "before")
await create_death_effect()
# This is sadly not reached
Logging.log(UnitBase, "after")

Now the effect manager side:

func execute_effect(effect: Effect, auto_delete_effect: bool = true, overlog: bool = false) -> bool:
	var ret_value : bool = false
	if !is_simulating:
		var effect_script : Script = effect.get_script()
		Logging.log(EffectManager, "Processing effect [%s]..." % effect_script.get_global_name())
		var pre_reactions : ReactionList = effect_pre_reactions.get(effect_script)
		await process_reaction_list(pre_reactions, effect, "Pre")
		Logging.log(EffectManager, "Executing effect [%s]..." % effect_script.get_global_name())
		@warning_ignore("redundant_await") # child class may override execute as a coroutine
		ret_value = await effect.execute(gameplay)
		if overlog:
			Logging.log(EffectManager, "effect succeeded? %s" % str(ret_value))
		# get post-reactions after the effect has executed in case some were dynamically added from the effect itself
		var post_reactions : ReactionList = effect_post_reactions.get(effect_script)
		# only process post reactions if the effect succeeded
		if ret_value:
			await process_reaction_list(post_reactions, effect, "Post")
		Logging.log(EffectManager, "Finished processing effect [%s]..." % effect_script.get_global_name())
	if overlog:
		Logging.log(EffectManager, "deleting effect? %s" % str(auto_delete_effect))
	if auto_delete_effect:
		effect.free()
	if overlog:
		Logging.log(EffectManager, "Returning %s" % str(ret_value))
	return ret_value

It’s important to note that the overlog bool is a new one that I have just added for debugging this issue, and that it will only be true when called by the create_death_effect function posted above.

Finally, here are the logs that this code prints in this case:

I want to re-iterate that the UnitDeath code works almost constantly, but in one particular edge case, it always fails. The thing that really makes this hard to understand for me is that I can definitely see that we are exiting the coroutine (thanks to the “Returning true” log), however the “after” is never printed even though it should be the next instruction. The particulars of this edge case is that it triggers more chained reactions that usual - could it be that there is a limit on how many coroutine we can have at the same time?

I’d really appreciate it if anyone had any clue what might be the problem here, I am loosing my mind!

Thanks,

Aymeric

Are you deleting this unit before the coroutine can finish? What is the one edge case where it fails specifically? Is it a particular effect?

1 Like

The unit should still be alive - though you’re right, that would absolutely make sense. I’ll double check that, can’t believe I didn’t double check this already :sweat_smile:

1 Like

Well, you were absolutely spot on, the unit was destroyed a tad too early, and so of course the code wasn’t triggered properly :smiley:

Thank you so much for your help!

1 Like