How to make a countdown timer that works across scenes?

Godot Version

4.7

Question

Before we get started, let me address that please do not tell me to just use singletons. That is what I am trying to do. I am trying to use singletons to create a countdown timer that keeps counting down even when you switch to a different scene. Here is the code inside my global TimerData script:

extends Timer

func _process(_delta):
	var _time_remaining = Timer.time_left

Here is the code for my timer itself: ```extends Node2D

var explosion: PackedScene = load(“res://Scenes/Explosion.tscn”)

@onready var label = $MarginContainer/VisibleCountdown
@onready var timer = $Timer

func _ready():
timer.start()

func _time_left_until_expired_food_explodes():
var time_left = TimerData._time_remaining
var minute = floor(time_left / 60)
var second = int(time_left) % 60
return[minute, second]
print(TimerData.time_remaining)

func _process(_delta):
label.text = “%02d:%02d” % _time_left_until_expired_food_explodes()

func _on_timer_timeout():
get_tree().change_scene_to_packed(explosion)```

gd:4 @ _process()

Whenever I press F5 to run the project the debugger returns this error for the process function in my global TimerData script: _process: Invalid access to property or key ‘time_left’ on a base object of type ‘Timer’.

It looks like you create the variable every frame in the global script and keep it in local scope. You will not be able to access it outside of the process function.

You could change it to

var _time_remaining: float

extends Timer

func _process(_delta):

\_time_remaining = Timer.time_left

It still will probably not work but its one or two issues gone.

This did not fix the error but yes, that is better than creating the vairable every frame.

Your problem is you are not using the Timer correctly. Your error is because you are referencing a variable in _process() before it is declared. There are a couple of solutions for this.

Please format your code correctly so that it is readable. This post will help you with that:

Then we can help you make your code work.

Transform the timer wait_time into a global Variable and play it.

extends Node2D

var explosion: PackedScene = load("res://Scenes/Explosion.tscn")

@onready var label = $MarginContainer/VisibleCountdown
@onready var timer = $Timer

func _ready():
	timer.start()

func _time_left_until_expired_food_explodes():
	var time_left = TimerData._time_remaining
	var minute = floor(time_left / 60)
	var second = int(time_left) % 60
	return[minute, second]
	print(TimerData.time_remaining)

func _process(_delta):
	label.text = "%02d:%02d" % _time_left_until_expired_food_explodes()

func _on_timer_timeout():
	get_tree().change_scene_to_packed(explosion)

Sorry about that. The double backticks don’t seem to work for me. Anyway, here is the Timer code in a readable format. Here is the global TimerData code once more:

extends Timer

var time_remaining

func _ready():
	pass

func _process(_delta):
	time_remaining = Timer.time_left

I updated it so that I am not creating the variable every frame but I still get the error. The error is this: _process: Invalid access to property or key ‘time_left’ on a base object of type ‘Timer’.

When you write Timer.time_left you try to access the base class “Timer”, not a specific instance of it. You need to reference and access the specific timer you want.

If you want to access the timer the script is attached to, just write time_left. It will use time_left on itself. It is the same as self.time_left.

I also want to add that the issue before, with keeping the variable inside process is that you will not be able to access it from outside of the function. You would only have been able to access it in the process function of that script. If you want a variable to be accessible in the way you tried to access it, you have to declare it outside of any function, without indenting it. Otherwise time_left will return null.

so like I did in my revision with declaring the variable

So your global TimerData object doesn’t need to have any code attached to it at all.
TimerData

extends Timer

Then for your code that uses it, you actually need to use that timer and not create another one.

extends Node2D

const EXPLOSION = preload("res://Scenes/Explosion.tscn")

@onready var label = $MarginContainer/VisibleCountdown

func _ready():
	TimerData.start()
	TimerData.timeout.connect(_on_timer_timeout())

func _time_left_until_expired_food_explodes():
	var time_left = TimerData.time_left
	var minute = floor(time_left / 60)
	var second = int(time_left) % 60
	return[minute, second]
	print("Time remaining: "%02d:%02d"" % [minute, second])


func _process(_delta):
	label.text = "%02d:%02d" % _time_left_until_expired_food_explodes()


func _on_timer_timeout():
	get_tree().change_scene_to_packed(EXPLOSION)

This is fine (a couple small errors but got fixed) but now the timer starts at 0 seconds, even though I have it set to 90 seconds.

Definitely not. You still tried to access “Timer.time_left”. That will not work as Timer is not an actively running timer but the class Timer. You can’t even name a variable “Timer” as that would shadow the class. And time_remaining doesn’t exist in that class.

Try to read replies through a bit more closely.

I am not good at Godot, which is why I am having this issue in the first place. I have been trying to read these very closely, I spent several hours looking at singletons on docs to try to figure out the issue, but to no avail. I also do not see the variable “timer” that you seem to be referring to. I know a basic python, but when it comes to how Godot works I am mostly clueless aside from how to make a player move and scene switching and basic things like that. The script I was given was very similar to my previous script. I would like to learn these, and if you could help, that would be much appreciated. Not saying you arent trying to help, but some of the things do not make complete sense like why I can’t use timer.time_left because I set it to autostart so it should be actively running, no? Or is it not because the timer itself is never told to start in code? Needless to say i am a bit confused

Here’s a fully functional project doing what you asked for. I don’t understand why you decided this needed to be a global Timer, but this is how I would do it. It works, I tested it.

Global Autoload

image

extends Node

@onready var timer: Timer = $Timer

Time Left Label

extends Label

const EXPLOSION = preload("uid://c0tfnlbfj8c65")

func _ready():
	Global.timer.start()
	Global.timer.timeout.connect(_on_timer_timeout)


func _time_left_until_expired_food_explodes() -> String:
	var time_left: float = Global.timer.time_left
	var minute: float = floor(time_left / 60)
	var second: float = int(time_left) % 60
	return "Time remaining: %02d:%02d" % [minute, second]


func _process(_delta):
	text = _time_left_until_expired_food_explodes()


func _on_timer_timeout():
	get_tree().change_scene_to_packed(EXPLOSION)

The project works. Let me know what questions you have about it.

I will try it. The only reason I decided to try and use a global timer was because I had learned about singletons and assumed that that was the only way to do it lol

Whenever I try to run the game it returns this error: Invalid access to property or key ‘on_trigger_player_spawn’ on a base object of type ‘Nil’. My player script is this:

extends CharacterBody2D

class_name Player
@export var speed := 340
@onready var animated_sprite = $AnimatedSprite2D
var panicked = false

func _ready():
	Global.on_trigger_player_spawn.connect(_on_spawn)

func _process(_delta):
	pass

# movement script

func _physics_process(_delta):
	var direction = Input.get_axis("left", "right")
	if direction:
		velocity.x = direction * speed
		animated_sprite.play("walk_not_panicked")
		animated_sprite.flip_h = direction < 0
	else:
		velocity.x = move_toward(velocity.x, 0, speed)
		animated_sprite.play("idle_not_panicked")
	
	move_and_slide()

func _on_spawn(Position: Vector2, _direction: String):
	global_position = Position

And the script from the project you told me to try is this:

extends Label

var EXPLOSION: PackedScene = load("res://Scenes/Explosion.tscn")

func _ready():
	GloTimer.timer.start()
	GloTimer.timer.timeout.connect(_on_timer_timeout)


func _time_left_until_expired_food_explodes() -> String:
	var time_left: float = GloTimer.timer.time_left
	var minute: float = floor(time_left / 60)
	var second: float = int(time_left) % 60
	return "Time remaining: %02d:%02d" % [minute, second]


func _process(_delta):
	text = _time_left_until_expired_food_explodes()


func _on_timer_timeout():
	get_tree().change_scene_to_packed(EXPLOSION)

And the global script is this:

extends Node

@onready var timer: Timer = $Timer

My two globals are named different things. Do I not understand how globals work? What is going wrong? The reason I ask is because this error seems to be related to the timer script because it did not happen before writing that script.

Does Global exist as an autoload?

For some reason the Global script had no path. I do not know why, but now the game runs. Albeit, there are some other issues. I will get back with these issues shortly, for now I am out of the house for a couple hours, but I will debug when I get back. One of the issues however is this: Cannot call method ‘start’ on a null value. That only started after instentiating the timer in my level scene