Search code examples
timergodotgdscript

Godot 4.0. how stop a auto call SceneTreeTimer?


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?


Solution

  • 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(mod_value):
            if mod_value and not cancelled:
                cancelled = true
                emit_signal("timeout")
    
    func _ready() -> void:
        one_shot = true
        connect("timeout", queue_free, [], CONNECT_ONESHOT)
        start()
    
    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.cancelled = true
    

    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.cancelled = true
    
    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.