Ten coins, twenty seconds, no second chances. Assembles Top-Down Movement, Collect Coins, and Timer Countdown.
Loading the game engine. This takes a moment the first time.
This is a project, not a lesson: nothing in this folder is new. Ten coins are scattered across the field and the clock gives you twenty seconds.
Timer Countdown: the clock, counting down in _process.Two lessons already knew how to end a game; here they finally disagree. The coins want you to win, the clock wants you to lose, and the race_over flag is the referee: whichever fires first wins the argument, and the flag stops the other one from firing at all.
■ UI node · ■ 2D node · % unique name · .tscn instanced scene
extends Node2D
## How many seconds the player gets.
@export var countdown_time: float = 20.0
## How many coins are in the scene. Change this if you add coins!
@export var total_coins: int = 10
var time_remaining: float
var collected: int = 0
var race_over: bool = false
@onready var player: ClockPlayer = $ClockPlayer
func _ready() -> void:
time_remaining = countdown_time
# coin.gd emits on the bus; the HUD and this file both connect. One
# emit, two reactions: the Collect Coins pattern, unchanged.
GameEvents.coin_collected.connect(_on_coin_collected)
func _process(delta: float) -> void:
if race_over:
return
time_remaining = max(time_remaining - delta, 0.0)
# hud.gd connects to this and redraws the clock, ~60 times a second.
GameEvents.timer_updated.emit(time_remaining)
if time_remaining <= 0.0:
# Set the flag FIRST, so this block can never run twice.
race_over = true
player.set_physics_process(false)
# hud.gd shows the lose message.
GameEvents.timer_expired.emit()
_restart_soon()
func _on_coin_collected(_value: int) -> void:
if race_over:
return
collected += 1
if collected >= total_coins:
race_over = true
player.set_physics_process(false)
# hud.gd shows the win message.
GameEvents.goal_reached.emit()
_restart_soon()
func _restart_soon() -> void:
var timer: SceneTreeTimer = get_tree().create_timer(2.0)
timer.timeout.connect(_on_restart_timeout)
func _on_restart_timeout() -> void:
get_tree().reload_current_scene()extends Area2D
## How many points this coin is worth.
@export var value: int = 1
func _ready() -> void:
body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node2D) -> void:
# "ClockPlayer" is the class_name at the top of player.gd.
if body is ClockPlayer:
# Emit first, erase second: erased nodes cannot send messages.
GameEvents.coin_collected.emit(value)
queue_free()extends CanvasLayer
## Keep in step with "total_coins" on the level.
@export var total_coins: int = 10
var _collected: int = 0
@onready var timer_label: Label = %TimerLabel
@onready var score_label: Label = %ScoreLabel
@onready var result_label: Label = %ResultLabel
func _ready() -> void:
GameEvents.timer_updated.connect(_on_timer_updated)
GameEvents.timer_expired.connect(_on_timer_expired)
GameEvents.coin_collected.connect(_on_coin_collected)
GameEvents.goal_reached.connect(_on_goal_reached)
%HomeButton.pressed.connect(_on_home_pressed)
_update_score()
func _on_timer_updated(time_left: float) -> void:
# "%.1f" rounds to one digit after the dot, so the clock reads 7.4
# instead of 7.38294916.
timer_label.text = "Time: %.1f" % time_left
func _on_coin_collected(value: int) -> void:
_collected += value
_update_score()
func _on_timer_expired() -> void:
result_label.text = "Out of time!"
func _on_goal_reached() -> void:
result_label.text = "You win!"
func _update_score() -> void:
score_label.text = "Coins: %d / %d" % [_collected, total_coins]
# Loads the lesson list screen, which replaces this whole scene.
func _on_home_pressed() -> void:
get_tree().change_scene_to_file("res://shared/ui/lesson_select.tscn")class_name ClockPlayer
extends CharacterBody2D
## How fast the player moves. Try 260 if twenty seconds feels impossible,
## but the real fix is a better route, not a faster player.
@export var speed: float = 200.0
func _physics_process(_delta: float) -> void:
var direction: Vector2 = Input.get_vector(
"move_left", "move_right", "move_up", "move_down"
)
velocity = direction * speed
move_and_slide()
# The warrior picture only faces right. Walking left mirrors it.
if direction.x != 0.0:
$Sprite2D.flip_h = direction.x < 0.0