_process() timer countdown inconsistent

Godot Version

4.3

Question

The _process() function I have on a child node isn’t being called consistently (or at least isn’t doing what I think it should be doing).

From my understanding the _process() function attempts to run every frame, but will sometimes not, depending on resources available to it.

I have two classes, a BaseAbility class and a child dash class.

The BaseAbility class is as follows:

class_name BaseAbility
extends Node2D

@export var base_cooldown: float = 1.0
@export var sprite: Sprite2D
@export var ability_name: String

var is_usable: bool = true
var timer := Timer.new()
var time_left: String

func _init():
	get_parent().add_child(timer)
	timer.one_shot = true
	timer.timeout.connect(_on_timer_timeout)
	self._ready()
	
func _process(delta):
	if !is_usable:
		time_left = "%3.1f" % timer.time_left
        print(time_left)

func use_ability(entity): 
	pass
	
func start_cooldown() -> void:
	timer.start()
	is_usable = false
	
func _on_timer_timeout() -> void:
	print(ability_name)
	print('ready')
	is_usable = true

And its child method dash:

class_name DashAbility
extends BaseAbility

@export var base_dash_distance: Vector2 = Vector2(40.0, 40.0)
	
func _ready():
	ability_name = "Dash"
	base_cooldown = 5.0
	timer.wait_time = base_cooldown

func use_ability(entity):
	if is_usable:
		var current_position = entity.global_position
		var facing_direction = entity.last_facing_direction
		var new_dash_position = current_position + (base_dash_distance * facing_direction)
		entity.global_position = new_dash_position
		start_cooldown()
	else:
		print('on cd')

I’m instantiating a new dash instance on my player, and attempting to call the use_ability on it, which should teleport the character forward in the direction they’re facing.

The ability works, and has a base cooldown of 5 second as is expected, but the timer.time_left is counting down normally for the first 2-3~ seconds, then suddenly stops working.

FWIW, the ability cooldown continues to work like normal, only the timer isn’t working the way I would expect it too.

Additionally, _physics_process() in place of _process() appears to work perfectly fine, so its something of an interaction between _process() that is causing the timer.time_left to behave weirdly.

I actually wouldn’t say “weirdly” because _process() is one of those, if there is enough time do it calls.

Most game engines have this slop and why they pass delta time slices per second so you can make up that slop logically.

A Timer isn’t even guaranteed to do the right thing either because of the
same threading issues.

Audio engines use DSP time and that is in a thread of it’s own and you can still mess that tight callback up with allocations or other time sensitive operations.

Just my 2cents.

1 Like

Notes

  • Process will run every frame. It can also make the frame much longer than you want it to be.
  • Timers run based on delta.
  • The timeout may be triggered before the next call to DashAbility._process(). Making the last time_left print look like there’s still time left.
  • Try printing the time left in timeout. What does it print?
  • Try putting a breakpoint in _on_timer_timeout() and inspect the timer’s values.

Offtopic Code Review

_ready() is part of a Node’s lifecycle and will be called by Godot. It should not be called by the programmer. Even more so in _init() where the node is not ready.

Don’t interact with the scene tree in _init(). Use _ready(). Generally, you should add timer as a child of self rather than reaching up (because the parent may not be ready for another child).

2 Likes

Interesting to know about avoiding _init(). I’ve been treating it as: When the node is initialized, it’ll execute this code and at one point I’ve been adding a parameter like _init(parent_entity) which then calls parent_entity.add_child(...) which I would hope gets around issues where we’re initializing a timer when the parent isn’t ready to create a child node.

As for putting a breakpoint in the code, oddly my Godot game appears to crash.

If I were to put a breakpoint at:

func _on_timer_timeout() -> void:
	print(ability_name) <--- breakpoint
	print('ready')
	is_usable = true

The game window freezes indefinitely, and exiting it doesn’t kill the game, instead the project runs headless without a display.

When I change the _process() to _physics_process() and let the game reach the _on_timer_timeout() breakpoint I see the base timer with a 5s cooldown. Not sure what I should be looking for, but I’m also unable to trigger a breakpoint that is checking for the timer.is_stopped being false, maybe because the BaseAbility class is being extended by the dash class?

But Timer should be operating separately from the _process() right? Or am I misunderstanding how this works.

From what I thought the timer.time_left printout might be skipped a couple times because _process() doesn’t have time to get to that execution, but that should still leave the timer running normally right? I might expect printouts to skip from like 4.1 seconds to 3.8 randomly because the inner values from 4.0, 3.9 got skipped, but not the entire process dying.

The other thing is, the Timer clearly is still working, the ability goes on cooldown and doesn’t allow the player to blink until the 5 seconds has passed.

This is the expected behavior. You should be using _physics_process() for anything that relies on the physics engine (i.e. your Dash).

TLDR: Use _physics_process() for gameplay. Use _process() for UI.

TLDR: Use _physics_process() for gameplay. Use _process() for UI.

FWIW: The actual timer and the CD on the dash appears to be working fine, the issue comes from the UI element I want to tie to the dash (i.e. a icon with a number that contains the cooldown of the skill). Does that icon still fall under something that should be run under _physics_process()?

Yes. _physics_process() is guaranteed to run 60 times/second (or whatever you change it to in project settings.) _process() has no such guarantee.

Ah. Understood, I had read somewhere that you should try to avoid putting too much stuff into _physics_process() since it might slow down the game, but interesting to note.

Thanks for the help.

1 Like

That is a true statement, but moving it to _process() doesn’t help. Instead they’re talking about things like input processing. You can throw most of that into _unhandled_input() without affecting performance, and then you’re not checking for every possible button press 60 times a second. Movement being the obvious exception. But for example, your Dash button can go there.

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.