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.
None of the earlier lessons use these. Each one shows the line where it appears here.
The bigger of the numbers given. min() gives the smaller.
time_remaining = max(time_remaining - delta, 0.0)■ UI node · ■ 2D node · % unique name · .tscn instanced scene
game_events.gd
Switch it on once, under Project > Project Settings, on the Globals tab. Not made yet? Press Create Script to make it right there. Already made it? Press Select Script/Scene, pick it, and press Add. Its Name column should say GameEvents, which Godot fills in for you. That's the name your code uses.
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
@onready var goal_area: Area2D = %GoalArea
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.
goal_area.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
# "Player" is a group ticked on the player's root node (Node dock >
# Groups). Both versions of the player are in it, so both can win.
if body.is_in_group("Player"):
race_over = true
GameEvents.goal_reached.emit()extends CanvasLayer
@onready var timer_label: Label = %TimerLabel
@onready var message_label: Label = %MessageLabel
@onready var home_button: Button = %HomeButton
func _ready() -> void:
# All three of these are emitted by game.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)
home_button.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 Player
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
@onready var sprite: Sprite2D = $Sprite2D
# "_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:
sprite.flip_h = direction.x < 0.0extends CharacterBody2D
## How fast the player runs left and right.
@export var speed: float = 200.0
## 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
@onready var sprite: Sprite2D = $Sprite2D
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
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:
sprite.flip_h = velocity.x < 0.0extends Node
# All of the game's signals are declared below, grouped by the lesson
# that uses them. A signal with brackets, like coin_collected(value: int),
# passes that value to every function connected to it.
# This shows only the signals this lesson uses. The full file declares
# every lesson's signals.
# Platformer Movement lesson
signal goal_reached
# Timer Countdown lesson
signal timer_updated(time_remaining: float)
signal timer_expired