Resumed function after yield, but class instance is gone.

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

Hi everyone.

I am slowly starting to understand a few things about GDScript and Godot. More importantly, I feel I am understanding the error messages a bit better too!

I am currently following a tutorial about creating a shooter game with space ships. Since I feel that doing the tutorials as they are is a bit boring, I try to add as much extra stuff as I can (just to see if I can).

I have succeeded in creating what I wanted, but am getting an error. I come to you for advice on a workaround.

E 0:00:04.207 resume: Resumed function ‘create_laser()’ after yield, but class instance is gone. At script: res://scripts/enemy_clever.gd:20
<C++ Error> Method failed. Returning: Variant()
<C++ Source> modules/gdscript/gdscript_function.cpp:1882 @ resume()

The problem lies in this part of the script. The original version had the enemy fire at fixed times and indefinitely. I wanted to change it to a more randomized version. The issue is that I am getting this error in the debugger (game works fine, btw. No crashes)

From what I understand, another function I have is erasing the enemy ship while the loop is happening. Correct? How do I solve this? There has to be something simple I am missing. :smiley:

Learning is fun!

Regards!

func create_laser():

var times = [1,2,3] 
var shots = 0
var cooldown = rand_range(1,3)
	
while shots!= times[randi() % times.size()]:
	yield(get_tree().create_timer(cooldown),"timeout")  #line 20
	var laser = scn_laser.instance()
	laser.set_position($cannon.get_global_position())
	get_tree().get_root().call_deferred("add_child",laser)
	shots += 1
:bust_in_silhouette: Reply From: alwaysusa

I had this problem, and it can cause a crash in exports of the game.

The solution is to replace the yield call with an actual timer on your node. You can still randomize the length of the timer, but it does not kick up an error when the instance is freed during a yield.

I replaced every single yield I have in my game because of this issue. I searched deep for a solution - but the only viable option until Godot 4.0 seems to be replacing yield with a timer node.

TLDR: Don’t use yield, ever. Use a timer node.

Figured it out as well a while ago.

Kind of lost hope of getting any answer to this question.

Thanks for the reply!

Good luck on your projects!

Samomba | 2020-08-06 19:52

Sometimes it’s really useful or nearly unavoidable as far as I know (e.g. when you need to wait for the signal on another object or function completion) so this answer and EXOMODE’s answer (checking with is_instance_valid() should be used together.

Dri | 2022-04-28 12:17

:bust_in_silhouette: Reply From: MintSoda

You could resume() the function from yield.

extends Sprite

var wait_fs: GDScriptFunctionState

func ready():
	wait_fs = yield(get_tree().create_timer(2.0), "timeout")
	hide()

func safe_free():
	if wait_fs.is_valid():
		wait_fs.resume()
	queue_free()
:bust_in_silhouette: Reply From: EXOMODE

You get an error because the instance of the object on which the wait is exiting has been deleted. Before returning the call to the wait method, don’t forget to check instances of your objects with is_instance_valid() for Node and is_valid() for awaiters.

Here is an example of a typical situation where your mistake can happen:

func anim_backwards(name: String, blend: float = -1.0) -> void:
	if _anim and _anim.has_animation(name):
		_anim.play_backwards(name, blend)
		yield(_anim, "animation_finished")

	yield(get_tree(), "idle_frame")

If the object in which this method is expected was deleted after the animation started and before it finished, you will get the same error. To prevent this, you must check the validity of the object before returning from the method:

func anim_backwards(name: String, blend: float = -1.0) -> void:
	if _anim and _anim.has_animation(name):
		_anim.play_backwards(name, blend)
		yield(_anim, "animation_finished")
	
	if is_instance_valid(_anim):
		yield(get_tree(), "idle_frame")
:bust_in_silhouette: Reply From: voidshine

It’s easy to create object lifetime issues in concurrent code. A reference is outliving its referent. The same kind of thing can happen in C# using async & await because coroutines are cooperative. When you await (or yield to) something, it holds a reference to the caller: a continuation to be executed later. If the caller is destroyed sooner than later, that continuation becomes invalid because it’s pointing to a dead object. You get a friendly error message in debug, or a crash in release.

I didn’t want to give up async and write a bunch of state machines. So I made a rule: Never await anything that might outlive you. So far, this is working well. I don’t use GetTree().CreateTimer() because the user might exit my scene completely before its timeout. The tree keeps the timer even if I change scenes. I don’t await ToSignal(GetTree(), "idle_frame") for the same reason: the tree will almost certainly outlive me, and the very next frame would crash.

Instead, I keep Timer nodes in my scene, which will die with the caller by design. And it’s easy enough to await a frame signaled by _Process or anything else. Just create a user signal on any object with the right lifetime.

I haven’t confirmed that the GodotTaskScheduler is properly cleaning up every trace of coroutines interrupted by total scene destruction, but it seems reasonable that it can and does. Signal sources can clean up their traces on destruction. If anyone knows the engine coroutine code well and wants to share the details, it would be great to confirm that I won’t encounter a coroutine graveyard littered with the husks of thousands of dead coroutines in a long-running game. For now, at least, my async code is running without errors or crashes. :slight_smile:

:bust_in_silhouette: Reply From: k2kra

Do not use the tree timer, add a child timer node instead.

Change this

yield(get_tree().create_timer(cooldown),"timeout")  #line 20

to

$Timer.start(cooldown); yield($Timer, "timeout")

Such this should work.

TIL that you can use semicolons in GDScript to reduce code that would otherwise take multiple lines into one line.

slightly_seasoned | 2021-09-29 07:31