cak3_lover
cak3_lover

Reputation: 1958

Deleting/Disconnecting a SceneTreeTimer

I'm trying to disconnect a SceneTreeTimer to avoid a function being called on timeout like this:

extends Node2D

onready var something = $Node2D
var timer

func abort():
    timer.disconnect("timeout",something,"queue_free")
    timer.emit_signal("timeout")
    
    print("timer=>",timer)

func _ready():
    timer=get_tree().create_timer(3)
    timer.connect("timeout",something,"queue_free")
    
    ...

    abort()

And while it does stop the timer from invoking the function
I'm still seeing the timer after aborting it, Output:

timer=>[SceneTreeTimer:1276]

Shouldn't it be something like this since it's time has elapsed?

timer=>[Deleted Object]

Upvotes: 2

Views: 1042

Answers (2)

Theraot
Theraot

Reputation: 40295

SceneTreeTimer unlike Node is a Reference.

If you have a look at the good old class diagram. You are going to see that some classes extend Node others Reference (including Resource) and other extend Object directly.

The classes that extend Reference are reference counted, they won't be deleted as long as you hold (a not WeakRef) reference to them.

While the classes that extend Node use explicit memory management, so they are deleted by calling free or queue_free on them.

Thus, drop the reference once you are no longer using the SceneTreeTimer:

func abort():
    timer.disconnect("timeout",something,"queue_free")
    timer.emit_signal("timeout")
    timer = null
    print("timer=>",timer) # null, duh

Godot will still emit the "timeout" signal, and when it does it releases its internal reference. We find this in "scene_tree.cpp" (source):

        if (time_left < 0) {
            E->get()->emit_signal("timeout");
            timers.erase(E);
        }

We can also experiment using a WeakRef to get a result similar to the one you expect. However, remember that since Godot is holding a reference internally the timer won't be deleted before its normal timeout.

extends Node2D

onready var something = $Node2D
var timer_ref:WeakRef

func abort():
    var timer := timer_ref.get_ref() as SceneTreeTimer
    timer.disconnect("timeout",something,"queue_free")
    timer.emit_signal("timeout")


func _ready():
    var timer := get_tree().create_timer(3)
    # warning-ignore:return_value_discarded
    timer.connect("timeout",something,"queue_free")
    timer_ref = weakref(timer)

    abort()

func _process(_delta: float) -> void:
    print("timer=>", timer_ref.get_ref())

You should see it change from

timer=>[SceneTreeTimer:1234]

To

timer=>null

After 3 seconds, since that is the argument we gave to create_timer.

Trivia: Here you will get some number where I put "1234", that number is the instance id of the object. You can get it with get_instance_id and you can get the instance from the id with instance_from_id. We saw an example of instance_from_id in FauxBody2D.


You might also find it convenient to create an Autoload where you create, stop, and even pause your timers while keeping a API similar to create_timer, for example see Godot 4.0. how stop a auto call SceneTreeTimer?.


Addendum:

DON'T DO THIS

You might actually mess up with Godot. Since it is reference counted, and we can freely change the count, we can make it release the timer early:

    var timer := timer_ref.get_ref() as SceneTreeTimer
    timer.disconnect("timeout",something,"queue_free")
    timer.emit_signal("timeout")
    timer.unreference()

I tested this both on the debugger and on a release export, with Godot 3.5.1, and it didn't crash the game, not output any errors.

For clarity unreference is not the same as free, instead:

  • reference increases the count by one.
  • unreference decreases the count by one.

I'm calling unreference to cancel out the internal reference that Godot has.

We can confirm that the timer is being freed, either by using a weak reference or by looking at Godot's profiler. However, Godot has an internal list with references to the timers which are not being cleared properly.

I made this code to test out if the timer loop was being affected by the timers being released early by the above means.

extends Node2D

var can_fire := true

func _process(_delta: float) -> void:
    var timer := get_tree().create_timer(60)
    # warning-ignore:return_value_discarded
    timer.unreference()

    if can_fire:
        can_fire = false
        print("CREATED")
        # warning-ignore:return_value_discarded
        get_tree().create_timer(2).connect("timeout", self, "fire")


func fire() -> void:
    print("FIRED")
    can_fire = true

You might expect it to output FIRED each couple seconds. However, what I found out is that by using unreference on unrelated timers, we get the others to fire much faster.

My hypothesis is that Godot is keeping the dead reference in its internal list, then when another timer is allocated it takes the same memory. Then the timer counts faster because it appears multiple times in the list.

Removing unreference results in the expected behavior.

Upvotes: 3

i ate my dog
i ate my dog

Reputation: 1

It does seem to exist still because calling "disconnect" function won't automatically free itself. Try doing timer.stop() instead.

Upvotes: 0

Related Questions