1

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]

cak3_lover
  • 1,440
  • 5
  • 26

2 Answers2

1

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.

Theraot
  • 31,890
  • 5
  • 57
  • 86
  • Just to confirm if I got it right: there is no way to free the `SceneTreeTimer` in code? and despite calling timeout it will not be deleted until the assigned time has passed – cak3_lover Jan 30 '23 at 09:03
  • @cak3_lover See addendum. I wanted to tested it first. – Theraot Jan 30 '23 at 09:04
  • so in the Addendum part the SceneTreeTimer is being freed immediately right? – cak3_lover Jan 30 '23 at 09:06
  • 1
    @cak3_lover it is being freed. Now, "completely"… I'm not sure. Since Godot has a list of timers that has a reference to it, I wonder if it leaks the dead reference. I don't see a contingency for this case in the source code. Edit: I'll fafo it. – Theraot Jan 30 '23 at 09:14
  • exactly my concern of leaking dead references – cak3_lover Jan 30 '23 at 09:15
  • 1
    @cak3_lover Don't do it, all the timers from there on end up firing too early. My hypothesis is that it is keeping the dead reference, and then another timer is allocated in the same memory, and then its time counts down twice as fast because it is twice in the list. *I did try allocating other things to see if I could get corrupted memory, but my attempts didn't work.* – Theraot Jan 30 '23 at 09:41
0

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