Noobie having trouble with timer

Godot Version

v4.2.2.stable.official [15073afe3] (Copied straight from Godot)

Question

I don’t know if this issue has been posted before, given it’s simple nature, but a fairly thorough search seems to suggest otherwise. Feel free to let me know if I am incorrect.

It seems like a simple to fix issue, but I’ve been wrestling with it for a couple days now, but to no avail. Like my last post, I’ve been trying to create a custom terminal-like-interface, so naturally, I need a simple API to interface with it. Everything else works perfectly fine, it’s just one final issue that I have with an otherwise pretty forgettable function.

If I want to add a delay between each call to the custom print function, which I know I’ll need at some point, I need a sleep function. I can’t just use

OS.delay_msec()

or,

OS.delay_usec()

because that locks up my entire program, so I need something else, like a timer to act as the delay.

Alas, this leads us to my current problem. For some reason, my implementation doesn’t work, and it seems to be because of the fact that the timer isn’t counting down after explicitly being requested to do so.

My program is setup in a modular fashion, with child scripts being loaded dynamically at runtime and executed, using the aforementioned API that was previously set up for them.

My current implementation uses a timer called “sleepTimer” hooked up as a child of the node that the parent script is linked to. Here is my code:

func sleep(secs: float) -> void:
	sleepTimer.wait_time = secs
	sleepTimer.start()
	await sleepTimer.timeout

The child script then calls it like this:

await parent.sleep(2.0) # or any other delay you want

Trying to print out sleepTimer’s “time_left” parameter always stays what was passed into it (in this example, 2.0), and never counts down. Any idea why this might be happening?

Timers need to be in the scene graph to properly work.

add_child(yourTimer)

That should trigger the timer if everything is fine.

1 Like

It is already a child and a part of the scene graph. I explicitly chose not to create timers in code, but to create them beforehand in the graph so I wouldn’t run into issues regarding that. I would post an image to prove it, but I don’t know how.

Regardless, I tried your idea anyway, and godot returned an error saying:

E 0:00:01:0255   terminal.gd:97 @ sleep(): Can't add child 'sleepTimer' to 'Text', already has a parent 'Text'.
  <C++ Error>    Condition "p_child->data.parent" is true.
  <C++ Source>   scene/main/node.cpp:1416 @ add_child()
  <Stack Trace>  terminal.gd:97 @ sleep()
                 -9223372007813413658.gd:17 @ story()
                 terminal.gd:136 @ load_and_execute_script()

which essentially proves my last point.

When this function sleep is calling, you need to add await like await sleep()

This won’t work because the sleep function isn’t returning a signal which is what “await” needs.

Instead of calling a function, just use:
await get_tree().create_timer(seconds, true, false, true).timeout
where seconds is the timer duration. This instantiates and adds a timer to the tree then free’s the timer on timeout.

If you want it in like an autoload, use this:

## Custom wait function
func wait(seconds: float) -> Signal:
	return get_tree().create_timer(seconds, true, false, true).timeout

Your script works for me

extends Node2D
@onready var timer: Timer = $Timer

func _ready() -> void:
	print("ready!")
	await timer.sleep(3)
	print("3 secs done")
	await timer.sleep(1)
	print("1 more")

# Timer script
extends Timer

func sleep(sec: float) -> void:
	wait_time = sec
	start()
	await timeout

This prints in succession after 3 then 1 second. Is your sleepTimer being destroyed? Are you calling sleep rapidly like in _process? could you show more of the surrounding scripts?

As far as I know, my sleepTimer isn’t being destroyed, and isn’t interacted with besides initialization until the first call of the sleep() function.

sleep() also isn’t being called multiple times in quick succession, it’s only called once, but that one time is enough to lock it up.

To answer your final question, here is as much of my code that I think is fit to share for the current context:

extends Label

# Path to module
@onready var module_path = "res://resources/stories/test.gd"

# Make nodes be accessed via a variable, because I don't feel like typing up a dollar sign every time.
@onready var charTimer = $charTimer
@onready var cursorTimer = $cursorTimer
@onready var sleepTimer = $sleepTimer

# Making NO_DELAY 0.0 breaks Godot timers for some reason. That or my code is shit
const NO_DELAY = 0.0001

# Character that will become the ""cursor""
var cursor_char = "_"

# Cursor loop
func _on_cursor_timer_timeout():
	cursor_visible = !cursor_visible
	update_cursor()
	pass

func update_cursor() -> void:
	if cursor_visible:
		text = text + cursor_char
	elif text.ends_with(cursor_char) == true:
		text = text.erase(text.length() - 1, 1)
	pass

# Start of my basic """"API""""
# ------------------------------------------------------------------
func set_delay_between_chars(delay_between_chars: float) -> void:
	charTimer.wait_time = delay_between_chars
	emit_signal("ready")
	pass
	
func print_to_terminal(input: String, omit_new_line_char: bool, clear_screen: bool) -> void:
	if clear_screen == true:
		lines_skipped += 1
	# Stops the cursor from sneaking in
	if text.ends_with(cursor_char) == true:
		text = text.erase(text.length() - 1, 1)
		visible_characters -= 1
	text += input
	if omit_new_line_char == false:
		text += "\n> "
	charTimer.start()
	pass

func _on_char_timer_timeout():
	if visible_characters < text.length():
		visible_characters += 1
	else:
		charTimer.stop()
	pass

# Doesn't work :(
func sleep(seconds: float) -> Signal:
	return get_tree().create_timer(seconds).timeout
# ------------------------------------------------------------------
# End of my basic """"API""""

# Setup function
func _ready():
	size = get_viewport_rect().size
	text = "> "
	visible_characters = 3
	load_and_execute_module(module_path)
	pass

# Load and execute module specified in the parameters
func load_and_execute_module(file_path: String) -> void:
	var file = FileAccess.open(file_path, FileAccess.READ)
	
	# If the file exists, do stuff with it
	if file:
		var script_content = file.get_as_text()
		file.close()

		# Create a new GDScript instance
		var script_resource = GDScript.new()
		script_resource.source_code = script_content
		
		# Make sure the module doesn't suck
		var err = script_resource.reload()
		if err != OK:
			print_to_terminal("Error reloading module: " + err, false, false)
			return

		# Create an instance of the module
		var script_instance = script_resource.new()
		
		# Pass the reference of the parent (this node) to the module instance
		script_instance.parent = self
		
		# Execute the function, if it exists. Complain to the user if it doesn't.
		await get_tree().create_timer(0.7).timeout
		if script_instance and script_instance.has_method("module"):
			script_instance.module()
		else:
			set_delay_between_chars(NO_DELAY)
			print_to_terminal("ERROR: function call failed: function 'story' not found", false, true)
	else:
		# Annoy the user if the file doesn't exist
		print_to_terminal("ERROR: file does not exist", false, true)

# The following code is from the child script that is loaded at runtime.
extends RefCounted

var parent

func module() -> void:
	char_delay(0.2)
	term_print("This is a test", false, false)
	await parent.sleep(2.0)
	term_print("This is another test", false, false)
	term_print("This is yet another test", false, false)
	pass

# Simplify the printing process
func term_print(input_string: String, omit_new_line_char: bool, clear_screen_before_print: bool) -> void:
	await parent.print_to_terminal(input_string, omit_new_line_char, clear_screen_before_print)
	pass

func char_delay(delay_between_chars: float) -> void:
	await parent.set_delay_between_chars(delay_between_chars)
	pass

# Doesn't work :(
func clear_term() -> void:
	parent.text = ""
	parent.text += "> "

I tried your suggestion, but still, it locks up and never finishes. I don’t know how or why it isn’t working. I tried it on my home computer, which runs Linux, and a computer at my school, which runs Windows, and it still doesn’t work, so even across platforms, it refuses to work. The code is all correct, which makes me think it might be a bug, although I’m hesitant to submit a bug report because I still don’t know whether it is indeed a bug, or just terrible code. I will be submitting more of my code to help answer @gertkeno’s question, so if you need more context, refer to that.

Are u possibly slowing the Engine.time_scale?? If it’s set to 0, I don’t think the timer will count down. You could test by setting your script’s Process as Always

how much of this prints? are there no errors? It seems like you are awaiting a lot of functions that never waits, print_to_terminal for instance. You also tell the label to emit “ready” this seems odd I do not know how other nodes might react to this sudden ready-ing.

Ah, I forgot that emit_signal() function was there back from when I was trying anything out to try and get it to work. I never noticed it was there because it never caused any problems for me.

As far as I know, there aren’t any errors when printing, the whole thing does come out fine, it’s just the sleep function that’s causing the problems.

I added a few print statements throughout the code to print the Engine.time_scale value, and it always prints 1, so I don’t think this is the problem. I tried setting the script’s process_mode value to always, and it didn’t change the behaviour. I would try it in the child script, but it doesn’t work because it extends RefCounted and not a regular node class.