All Questions

Community

ltsdw

A question about timers and its "one shot" property

So I was coding the Sword scene, took me 3 days to get it right, well... at least I think it's right.


I added a raycast2d to it as I did with the shield so it can't "see" the robot through other collisionshape. Also I made it so it'll continue to search for the robot (it'll search 4 times, it's tunable by max_walk_cycle exported variable), the location the sword chooses is random, if the robot appears while the sword still searching for him, the sword will rush at it, if within range, and chase it if not. I've made the raycast bigger than the detection area because I thought it would make more sense as once the sword detected the robot it wouldn't "unsee" it. So once the sword detect the player the only way to make it "forget" is wait until it stops searching for the robot or get out of the raycast range.


So, my question, we used timers throughout this course and I thought setting the timer to oneshot would allow me to run the timer, for example, in _procress() without restarting the timer. But I created a timer for keeping track of the time the sword is "dashing/flying" towards the target, and set it up at a function that would get called multiple times in the _physics_process, and even though I had the oneshot checked, it would still restart everytime the function was called, my solution was check if the timer was stopped before starting the timer.

...
func _physics_process(delta: float) -> void:
    _on_physics_process(delta)

func _on_physics_process(delta: float) -> void:
...
    if _target_within_range:
        _prepare_to_attack()

        if _should_rush:
            rush_at_target()
...
func rush_at_target() -> void:
    _velocity = _first_pos_target * rush_speed
    _velocity = move_and_slide(_velocity)

    if _rush_timer.is_stopped():
        _rush_timer.start()
...

this way it stopped restarting the timer, so is it the expected behavior and I understood the one_shot wrong, or I was doing something wrong, or both?

  • Nathan Lovato replied
    Solution

    If you're calling start() on a timer it'll restart it, so if you do that every frame, you're indeed restarting the timer every frame.

    One shot just makes it so when the timer reaches zero seconds, it stops. Otherwise, it will set it's time back to the desired weight time and start another countdown.

    You will restart the timer with the start() function regardless of its one shot property.

    Then what I see in your code is that perhaps _should_rush should not be true every frame while the sword is charging at the player.

    You could instead have one boolean named _is_rushing and:

    • If _is_rushing is false but the sword gets into the right condition to charge at the player
    • set _is_rushing to true
    • start the timer
    • else, if _is_rushing is true, run your rush_at_target, and remove the timer part from it

    That way you will only call the timer's start() function once.

  • ltsdw replied

    So like this? It works!

    ...
    if _should_rush:
            if not _is_rushing:
                _is_rushing = true
                _rush_timer.start()

            if _is_rushing:
                rush_at_target()

            return
    ...
    func _on_RushTimer_timeout() -> void:
    _should_do_rush(false)
    _is_rushing = false

    _cooldown_timer.start()
    ...

    But, like, isn't the _rush_timer keeping track of the "rushing" state of the sword already on my code? So this one isn't good then?

    func _on_physics_process(delta: float) -> void:
    ...
    if _should_rush:
    if _rush_timer.is_stopped():
    _rush_timer.start()
    else:
    rush_at_target()

    return
    ...

    or yet, to have a meaningful name? is this one good?

    func _on_physics_process(delta: float) -> void:
    ...
    var is_rushing: bool = not _rush_timer.is_stopped()
    ...
    if _should_rush:
            if is_rushing:
                rush_at_target()

                return

            _rush_timer.start()

            return
    ...

    the _should_rush is set to true on the beforeattack timeout, and it is set to false again at the _rush_timer timeout (or when it hits any body, a wall, another mob, etc), and once it is false doesn't matter if there's still time left on the _rush_timer (in case it hit something before the timeout), it will stop rushing.

    maybe I should give the _should_rush another name? as it seems to be doing more than telling if it should rush or not, like when the sword is already rushing. _is_state_rushing, _do_rushing_state or something like that?

    also I changed the code that rushes at the targeted position of place, now it reflects better the behavior that once it's flying it will not do another thing.

    ...
    var is_rushing: bool = not _rush_timer.is_stopped()

    if _should_rush:
    if is_rushing:
    rush_at_target()

    return

    _rush_timer.start()

    return

    if _before_attack_timer.is_stopped():
    ...
    orbit_target()

    point_to_target()

    if _target_within_range:
    _prepare_to_attack()

    return
    else:
    follow(_target.global_position)
    ...


    But the most important part is that now I know for sure that if I tell to a timer start it will start XD. Thank you!

  • Nathan Lovato replied

    Using the timer to check for the state and avoiding an extra variable is a good practice. I do recommend that as long as you're comfortable reading that and it makes sense to you.

     Then the details of the code aren't too important at this point as long as you have good control of the sword's behavior and no bugs.

    If you wanted to add multiple extra states to the sword,  you would probably want to move to a different code structure, what we call a state machine.