Fall in a pit, lose a life, reappear at the start. Run out of lives and it is game over.
Loading the game engine. This takes a moment the first time.
kill_zone.gd emits life_lost AND calls body.respawn().life_lost, subtracts one, and emits lives_updated so the HUD can redraw.player_died.One signal leads to another. That is called a chain.
■ UI node · ■ 2D node · % unique name · .tscn instanced scene
extends Node2D
## How many lives the player begins with.
@export var starting_lives: int = 3
# How many lives are left right now. "_ready()" fills it in.
var lives: int
func _ready() -> void:
lives = starting_lives
# kill_zone.gd emits "life_lost". We put "_on_life_lost" inside
# ".connect()". That is the function Godot runs when it arrives.
GameEvents.life_lost.connect(_on_life_lost)
func _on_life_lost() -> void:
lives -= 1
# hud.gd connects to "lives_updated" and redraws the number.
GameEvents.lives_updated.emit(lives)
if lives <= 0:
# hud.gd connects to "player_died" too, and shows Game Over.
GameEvents.player_died.emit()extends CanvasLayer
# "%LivesLabel" means "the node named LivesLabel in this scene."
@onready var lives_label: Label = %LivesLabel
func _ready() -> void:
# lives_system.gd emits both of these signals.
GameEvents.lives_updated.connect(_on_lives_updated)
GameEvents.player_died.connect(_on_player_died)
%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")
# "remaining" is the number lives_system.gd sent with the signal.
func _on_lives_updated(remaining: int) -> void:
lives_label.text = "Lives: %d" % remaining
func _on_player_died() -> void:
lives_label.text = "Game Over!"extends Area2D
func _ready() -> void:
body_entered.connect(_on_body_entered)
# "body" is whatever touched the zone.
func _on_body_entered(body: Node2D) -> void:
if body is LivesPlayer:
# Two different ways of talking, sitting right next to each other.
# This zone cannot reach the HUD, so it emits on the bus:
GameEvents.life_lost.emit()
# But "body_entered" already handed us the player as "body", so we
# just call a function on it. No signal needed.
body.respawn()class_name LivesPlayer
extends CharacterBody2D
## How fast the player moves.
@export var speed: float = 200.0
# Where the player was standing when the level began.
var spawn_position: Vector2
func _ready() -> void:
# Save the starting spot ONCE, at the very beginning. If we saved it
# every frame, "respawn()" would just drop us where we already are.
spawn_position = global_position
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
## Called BY kill_zone.gd, not by anything in this file.
func respawn() -> void:
# Snap straight back to the saved spot. No sliding, no animation.
global_position = spawn_positionextends LivesPlayer
## 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 LivesPlayer.
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