+8 votes

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?

in Engine by (666 points)

More discussion on this issue can be found here: https://github.com/godotengine/godot/issues/24311

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.

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

6 Answers

+1 vote
Best answer

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.

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

3) 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()
    #...

4) Don't use yield:

func _process(delta):
    if timer.get_time_left() >= 0.1:
        # Do things
by (27,958 points)
selected by

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.

0 votes
by (69 points)
0 votes

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.

by (9,712 points)

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.

+1 vote

What worked for me is using the isinstancevalid 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.

by (16 points)
+5 votes

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")
by (1,667 points)
+1 vote

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)
by (20 points)
Welcome to Godot Engine Q&A, where you can ask questions and receive answers from other members of the community.

Please make sure to read How to use this Q&A? before posting your first questions.
Social login is currently unavailable. If you've previously logged in with a Facebook or GitHub account, use the I forgot my password link in the login box to set a password for your account. If you still can't access your account, send an email to webmaster@godotengine.org with your username.