How do I properly stop a yield from resuming after the class is freed?

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By CowThing
:warning: Old Version Published before Godot 3 was released.

I have a function that does something over time, yielding every frame.

func example():
	timer.start()
	while timer.get_time_left() >= 0.1:
		# Do things
		yield(get_tree(), "fixed_frame")

The problem is, if I call this function and then queue_free() the object before this function finishes, it will give me an error, Resumed after yield, but class instance is gone. Now this error doesn’t seem to have any ill effects, the function is still working just as intended, but it only works in the debug version of the game. With an exported game this error causes a crash.

So, how do I properly stop a function like this?

I had the exact same issue. Took me some hours to get a workaround which doesn’t require you to change all your yield’s. Create a new singleton and paste this code:

func yield_wait(var timeout : float, var parent = get_tree().get_root()):
	
	var timeoutCap = max(0, timeout / 1000.0)
	
	if timeoutCap <= 0:
		return
	
	var timer = Timer.new()
	timer.set_one_shot(true)
	
	# ensure that the timer object is indeed within the tree
	yield(yield_call_deferred(parent, "add_child", timer), "completed")
	
	timer.start(timeoutCap)
	var timerRoutine = _yield_wait(timer)
	
	if timerRoutine.is_valid():
		yield(timerRoutine, "completed")
	
	yield(yield_call_deferred(parent, "remove_child", timer), "completed")
	
func _yield_wait(var timer : Timer):
	yield(timer, "timeout")

func yield_call_deferred(var node, var action, var parameter):
	node.call_deferred(action, parameter)
	yield(get_tree(), "idle_frame")

Now you can call, like before:

yield(yourgd.yield_wait(5000, self), "completed")

to wait for 5 seconds. (I used MS, since I dont like seconds)

The magic is simple: The code attaches a timer node to the caller. When the nodes gets queue_freed, then the timer node also will be removed. Thus, its promise (timer.create(…)) won’t wait till its end, but abort and return to your code.

Billy the Boy | 2020-12-28 02:58

This solution mostly works, although I get “ObjectDB instances leaked at exit” warnings if I quit my game during a yield_wait() call

WARNING: cleanup: ObjectDB instances leaked at exit (run with --verbose for details).
     At: core/object.cpp:2132
Leaked instance: Timer:11570 - Node name: @@3
Leaked instance: Timer:11602 - Node name: @@7
Leaked instance: Timer:11586 - Node name: @@5
Leaked instance: Timer:11618 - Node name: @@9

Poobslag | 2021-11-03 17:37

:bust_in_silhouette: Reply From: Zylann

yield returns a GDFunctionState, I believe it could allow you to force the execution to finish. Unfortunately it only has resume, so you have a few options:

  1. Forcing the state to resume until it doesn’t yields anymore:
var func_state = null

func somewhere():
	#...
	func_state = example()
	#...


func force_resume():
	while func_state extends GDFunctionState:
		func_state = func_state().resume()


func somewhere_else():
	#...
	if node.has_method("force_resume"):
		node.force_resume()
	node.queue_free()
	#...

But it looks awkwards.

  1. Prevent destruction by setting a boolean to true, or reset the timer, but what you do next would still be impractical.

  2. yield until the yield finishes, and destroy only then:


signal finished_example

func example():
	timer.start()
	while timer.get_time_left() >= 0.1:
		# Do things
		yield(get_tree(), "fixed_frame")
	emit_signal("finished_example")


func somewhere()
	#...
	yield(obj, "finished_example")
	obj.queue_free()
	#...
  1. Don’t use yield:
func _process(delta):
	if timer.get_time_left() >= 0.1:
		# Do things

Thanks! I did try stuff with the GDFunctionState, I was hoping there would be a simple way to end it from that. But I will use another method.

CowThing | 2016-10-08 12:26

:bust_in_silhouette: Reply From: binogure

Reddit’s answer seems more easier to implement:

https://www.reddit.com/r/godot/comments/9m83vc/how_to_prevent_yield_resumming_after_the_object/

:bust_in_silhouette: Reply From: volzhs

I solved this with custom signal

signal process

func _process(delta):
    emit_signal("process")

func example():
    timer.start()
    set_process(true)
    while timer.get_time_left() >= 0.1:
        # Do things
        yield(self, "process")
    set_process(false)

not much changed the original code.

Wouldn’t this cause a memory leak, although small? If ‘func example’ yields waiting for the ‘process’ signal but the node is destroyed before emitting that signal, then the coroutine will wait until the application terminates. The fact that it is possible for a coroutine to resume after the owner node has been freed is probably because the “owner” node does not, in fact, owns the memory allocated for the coroutine. This solution avoids a production build crash, which is the most important thing of course, but one must be aware of the possible memory leak.

gnumaru | 2020-01-07 10:14

:bust_in_silhouette: Reply From: ajayrwarrier

What worked for me is using the is_instance_valid function to check if the class instance is alive before yield

func example():
    if(is_instance_valid(self)): 
	    	yield(get_tree().create_timer(.15),"timeout")

You can also change self to $ the node you want. the logic works the same.

Underrated! 2 years later and this answer is still saving me.

knightofiam | 2022-10-23 22:37

:bust_in_silhouette: Reply From: rakkarage

change yield to connect, need to make a new func and continue there but works without error

error:

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

no error:

get_tree().create_timer(_delay).connect("timeout", self, "_next")
:bust_in_silhouette: Reply From: jpate

I ended up writing a class based on Reference to handle this:

const SafeYielder = preload('res://util/SafeYielder.gd')
_yielder = SafeYielder.new(self)

func _enter_tree():
	while not_ready()
		yield(_yielder.wrap(get_tree().create_timer(.5), 'timeout'), 'completed')
		if !is_instance_valid(self) || !is_inside_tree():
			return
	do_some_thing()

SafeYielder.gd

extends Reference

class Yielder extends Reference:
	signal completed(value)
	var _obj: Object
	var _sig: String
	var _owner: WeakRef

	func _init(owner: WeakRef):
		_owner = owner

	func _connect(obj: Object, sig: String) -> int:
		_obj = obj
		_sig = sig
		var err := obj.connect(sig, self, '_completed')
		assert(err == OK)
		return err

	func _completed(value=null):
		dispose()
		if _owner.get_ref():
			emit_signal('completed', value)

	func dispose():
		if _obj:
			for s in _obj.get_signal_connection_list(_sig):
				if s.target == self:
					_obj.disconnect(s.signal, s.target, s.method)
		_obj = null

	func is_disposed():
		_obj == null

var _yielders := []
var _deleting := false
var _i = 0
var _owner: WeakRef
const _stat = {i = 0}

func _init(owner: Object):
	_owner = weakref(owner)

func _notification(what):
	if what == NOTIFICATION_PREDELETE:
		for y in _yielders:
			y.dispose()
		print('yielder predelete %s:%s < %s' % [self, _i, _yielders])
		_yielders = []
		_deleting = true

# Wrap a connection so that godot won't complain
# when the yielded function completes after the instance is gone
func wrap(obj, sig: String):
	if _i == 0:
		_stat.i += 1
		_i = _stat.i
	if !_deleting:
		var y := Yielder.new(_owner)
		var err := y._connect(obj, sig)
		assert(err == OK)
		_cleanup()
		_yielders.append(y)
		print('yielder add %s:%s < %s' % [self, _i, y])
		return yield(y, 'completed')

func _cleanup():
	for y in _yielders:
		if y.is_disposed():
			_yielders.erase(y)