Race the clock. Reach the green goal in time and you win; let the timer hit zero and you lose.
Loading the game engine. This takes a moment the first time.
Race the clock. Reach the green goal in time and you win. Let the timer hit zero and you lose.
■ UI node · ■ 2D node · % unique name · .tscn instanced scene
extends Node2D
## How many seconds the player gets.
@export var countdown_time: float = 10.0
# How many seconds are left. "_ready()" fills it in.
var time_remaining: float
# Becomes true once the race ends, either way. It stops the countdown so
# the result stays frozen on screen instead of ticking past.
var race_over: bool = false
func _ready() -> void:
time_remaining = countdown_time
# GoalArea is the green zone. We put "_on_goal_reached" inside
# ".connect()". That is the function Godot runs when it is touched.
%GoalArea.body_entered.connect(_on_goal_reached)
# "_process()" runs every frame. Godot calls it for you.
func _process(delta: float) -> void:
# Race already finished, so do nothing. "return" leaves early.
if race_over:
return
# Take away the time that just passed. "max(..., 0.0)" means "use the
# bigger of these two", which stops the clock going below zero.
time_remaining = max(time_remaining - delta, 0.0)
# hud.gd connects to "timer_updated" and redraws the clock. This one
# gets emitted about 60 times a second, once per frame.
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
GameEvents.timer_expired.emit()
# "body" is whatever touched the goal zone.
func _on_goal_reached(body: Node2D) -> void:
# If the clock already ran out, reaching the goal is too late.
if race_over:
return
# "TimerPlayer" is the class_name at the top of player.gd.
if body is TimerPlayer:
race_over = true
GameEvents.goal_reached.emit()extends CanvasLayer
@onready var timer_label: Label = %TimerLabel
@onready var message_label: Label = %MessageLabel
func _ready() -> void:
# All three of these are emitted by timer_countdown.gd.
# One HUD can connect to as many signals as it needs.
GameEvents.timer_updated.connect(_on_timer_updated)
GameEvents.timer_expired.connect(_on_timer_expired)
GameEvents.goal_reached.connect(_on_goal_reached)
%HomeButton.pressed.connect(_on_home_pressed)
# 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")
# Runs about 60 times a second while the race is going.
func _on_timer_updated(time_left: float) -> void:
# "%.1f" is a blank spot for a decimal number, rounded to ONE digit
# after the dot. Without the ".1" you would see 7.38294916 on screen.
timer_label.text = "Time: %.1f" % time_left
func _on_timer_expired() -> void:
message_label.text = "You Lose!"
func _on_goal_reached() -> void:
message_label.text = "You Win!"class_name TimerPlayer
extends CharacterBody2D
## How fast the player moves. "@export" puts "speed" at the top of the
## Inspector panel, so you can change it there instead of in this file.
@export var speed: float = 200.0
# "_physics_process()" runs about 60 times a second. Godot calls it for you.
func _physics_process(_delta: float) -> void:
# "Input.get_vector(...)" checks all four keys at once and hands back
# an arrow pointing where to go. The four names in quotes are ours.
# See them under Project > Project Settings > Input Map.
var direction: Vector2 = Input.get_vector(
"move_left", "move_right", "move_up", "move_down"
)
# "direction" is only WHICH WAY (its length is always 1).
# "direction * speed" stretches it, so "velocity" is also HOW FAST.
velocity = direction * speed
# Nothing moves until "move_and_slide()" runs.
# "Slide" means we slide along walls instead of sticking to them.
move_and_slide()
# The warrior picture only faces right. Walking left mirrors it.
if direction.x != 0.0:
$Sprite2D.flip_h = direction.x < 0.0extends TimerPlayer
## How hard the player jumps. Negative is up.
@export var jump_velocity: float = -620.0
## How hard gravity pulls down.
@export var gravity: float = 1800.0
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity.y += gravity * delta
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = jump_velocity
# "speed" is inherited from TimerPlayer.
velocity.x = Input.get_axis("move_left", "move_right") * speed
move_and_slide()
# The warrior picture only faces right. Walking left mirrors it.
if velocity.x != 0.0:
$Sprite2D.flip_h = velocity.x < 0.0