Godot 4.0. how stop a auto call SceneTreeTimer?

Viewed 454

I have this code :

extends Area2D

func _ready() -> void : auto_call()
    
func auto_call() : 
    await get_tree().create_timer(1, false).timeout
    print( "area autocall" )
    auto_call()

func stop_auto_call() : 
    auto_call = null # how stop?

I know, I can use two options : create a normal Timer or save the references in a global array to later stop it/them, but... it could take a lot of refactoring in several nodes.

I'm tring desactive or pause the node and the code works... less the auto_call calls. I try using unreference(), null, stoping : process, input and physics. But nothing. I see a little hope with get_tree().get_processed_tweens() but it see than don't exist any for SceneTreeTimer. Anyone knows what to do, alternatives or ideas?

2 Answers

Godot has a pause system. You can pause the SceneTree by setting its paused to true:

get_tree().paused = true

Which scripts will execute when paused or not depends on process_mode.

Furthermore, any SceneTreeTimer created with always_process set to false (which you specify as second argument of create_timer), will not run while the scene tree is paused.

However, this approach will also affect anything else working the pause system and does not give you the granularity of pausing only some of these timers.


You could also use Timers:

var timer := Timer.new()
add_child(timer)
timer.wait_time = 1.0
timer.one_shot = true
timer.start()
await timer.timeout
timer.queue_free()

Or like this:

var timer := Timer.new()
add_child(timer)
timer.wait_time = 1.0
timer.one_shot = true
timer.connect("timeout", timer.queue_free, [], CONNECT_ONESHOT)
timer.start()
await timer.timeout

Then to pause them, you can get each Timer children, that is not queued for deletion, and pause it:

for node in get_children():
    var timer:Timer = node as Timer
    if !is_instance_valid(timer) or timer.is_queued_for_deletion():
        continue

    timer.paused = true

Note that this will take all children Timers. And that might be more than you want to.

This will allow you to only pause the timers of a specific Node.


We can create an Autoload. I'll call it TimerRoot. With a script with similar code as described above:

extends Node

func delay(wait_time:float) -> void:
    var timer := Timer.new()
    add_child(timer)
    timer.wait_time = 1.0
    timer.one_shot = true
    timer.start()
    await timer.timeout
    timer.queue_free()

func set_paused(pause:bool) -> void:
    for node in get_children():
        var timer:Timer = node as Timer
        if !is_instance_valid(timer) or timer.is_queued_for_deletion():
            continue

        timer.paused = pause

Which you can the use like this await TimerRoot.delay(1.0) and pause them all like this TimerRoot.set_paused(true).


Alternatively you can return the Timer:

func delay(wait_time:float) -> Timer:
    var timer := Timer.new()
    add_child(timer)
    timer.wait_time = 1.0
    timer.one_shot = true
    timer.connect("timeout", timer.queue_free, [], CONNECT_ONESHOT)
    timer.start()
    return Timer

And use it like this: await TimerRoot.delay(1.0).timeout.


Another variant is to add a parent parameter, and have delay add the timer as a child to it. Which gives you back the granularity of pausing only the Timers of a Node.


By the way, in this code we are pausing. You could implement a cancel mechanism, where you could add an if where you call delay to check if it was cancelled. If you are returning the Timer, you could have a script attached (which also helps us identify which are the correct timers) with a cancelled property. This suggest us to change from an Autoload, to a class:

class_name OneShotTimer extends Timer

@export
var cancelled:bool:
    get: return cancelled
    set(_value): pass

func _ready() -> void:
    one_shot = true
    connect("timeout", queue_free, [], CONNECT_ONESHOT)
    start()

func set_cancelled() -> void:
    cancelled = true
    emit_signal("timeout")

static func delay(parent:Node, time:float) -> bool:
    if !is_instance_valid(parent):
        push_error("Parent Node is not valid.")

    if !parent.is_inside_tree():
        push_error("Parent Node is not in the scene tree.")

    var timer := OneShotTimer.new()
    timer.wait_time = time
    parent.add_child(timer)
    await timer.timeout
    return !timer.cancelled

static func set_paused(parent:Node, pause:bool) -> void:
    for node in parent:
        var timer:OneShotTimer = node as OneShotTimer
        if !is_instance_valid(timer) or timer.is_queued_for_deletion():
            continue
        
        timer.paused = pause

static func cancel(parent:Node) -> void:
    for node in parent:
        var timer:OneShotTimer = node as OneShotTimer
        if !is_instance_valid(timer) or timer.is_queued_for_deletion():
            continue
        
        timer.set_cancelled()

Which you can use like this:

if await OneShotTimer.delay(self, 1.0):
    print("OK")
else:
    print("Aborted operation")

And pause all the timers of a node like this OneShotTimer.set_paused(self, true). And to cancel them you do OneShotTimer.cancel(self)

If you want both the granularity but also the ability to globally pause or cancel them all, we can bring back the Autoload TimerRoot with a different code:

extends Node

var _timers:Array

func register(timer:OneShotTimer) -> void:
    if is_instance_valid(timer) and !timer.is_queued_for_deletion():
        _timers.append(timer)
        timer.connect("timeout", func(): _timers.erase(timer), [], CONNECT_ONESHOT)

func cancel() -> void:
    for timer in _timers:
        timer.set_cancelled()

func set_paused(pause:bool) -> void:
    for timer in _timers:
        timer.paused = pause

Then add TimerRoot.register(self) on _ready on OneShotTimer so they are all registered. And then you can pause all with TimerRoot.set_paused(true) or cancel all with TimerRoot.cancel().


But that is cancel. Which is not really stopping. Stopping is an issue, because you have some code awaiting for it. If you are Ok with them waiting indefinitely for the signal, you can simply remove the timers with queue_free.

Consider just using tween? Would be nice if timer had the same capabilities but...

extends Node

var _tween: Tween

func _ready() -> void:
    # every 1 second call _auto_call
    _tween = create_tween().set_loops()
    _tween.tween_callback(_auto_call).set_delay(1)
    # cancel after 10 seconds
    await get_tree().create_timer(10).timeout
    _stop_auto_call()

func _auto_call():
    print( "area autocall" )

func _stop_auto_call() :
    _tween.kill()
Related