A project, not a new idea: escape the maze before the chaser reaches you. Assembles Top-Down Movement, Walls and Collisions, and Enemy Follows Player.
Loading the game engine. This takes a moment the first time.
This is a project, not a lesson: there is no new idea anywhere in this folder. Three things you already built are assembled into one game: reach the green goal, and do not let the chaser touch you.
StaticBody2D walls block everyone.The only code that is NEW is this file, and it only does endings. The goal emits goal_reached on the bus when you arrive. The chaser has an Area2D stuck to its front like a name tag. This file connects to it, and a touch from the player means caught. Either way the game freezes, the HUD shows the result, and the scene reloads for another run.
■ UI node · ■ 2D node · % unique name · .tscn instanced scene
extends Node2D
# Becomes true the moment the round ends, either way. Both endings check it,
# so escaping and being caught on the same frame cannot both happen.
var game_over: bool = false
@onready var player: EscapePlayer = $EscapePlayer
@onready var chaser: EscapeChaser = $Chaser
func _ready() -> void:
# goal.gd emits this on the bus when the player arrives.
GameEvents.goal_reached.connect(_on_goal_reached)
# The chaser's catch area is a child of the Chaser node. The level is
# the parent of both chaser and player, so it reaches DOWN to connect,
# the same two-step pattern game_events.gd explains.
$Chaser/CatchZone.body_entered.connect(_on_catch_zone_entered)
func _on_goal_reached() -> void:
if game_over:
return
game_over = true
_freeze()
# hud.gd connects to "goal_reached" too and shows the message. This file
# never touches a label: endings and screens stay separate jobs.
_restart_soon()
func _on_catch_zone_entered(body: Node2D) -> void:
if game_over:
return
# "EscapePlayer" is the class_name at the top of player.gd. Without this
# check, the chaser could be "caught" by a wall.
if body is EscapePlayer:
game_over = true
# hud.gd connects to "player_died" and shows the caught message.
GameEvents.player_died.emit()
_freeze()
_restart_soon()
# Stop both movers exactly where they are, so the end stays on screen
# instead of the chaser shoving the player around during the message.
func _freeze() -> void:
player.set_physics_process(false)
chaser.set_physics_process(false)
func _restart_soon() -> void:
# A one-time timer. When it finishes, reload the scene for another run.
var timer: SceneTreeTimer = get_tree().create_timer(1.5)
timer.timeout.connect(_on_restart_timeout)
func _on_restart_timeout() -> void:
get_tree().reload_current_scene()class_name EscapeChaser
extends CharacterBody2D
## NOTE: "motion_mode" is set to "Floating" in the scene file (.tscn),
## because the default "Grounded" mode is built for side-scrollers.
## How fast the chaser moves toward the player.
@export var speed: float = 150.0
## Which node to chase. You pick this in Godot by dragging the player in.
@export var target_path: NodePath
## How close the chaser gets before it stops pushing.
@export var stop_distance: float = 30.0
# Holds the real player node once we look it up. Starts out empty.
var _target: Node2D
func _ready() -> void:
_target = get_node_or_null(target_path)
func _physics_process(_delta: float) -> void:
if _target == null:
return
var distance: float = global_position.distance_to(_target.global_position)
if distance > stop_distance:
# Subtracting two positions gives an arrow pointing from us to them.
var direction: Vector2 = (_target.global_position - global_position).normalized()
velocity = direction * speed
else:
velocity = Vector2.ZERO
move_and_slide()extends Area2D
func _ready() -> void:
body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node2D) -> void:
if body is EscapePlayer:
# Two files are listening: dont_get_caught.gd freezes the game and
# hud.gd shows the message. One emit, two reactions: the exact
# pattern from the Collect Coins lesson.
GameEvents.goal_reached.emit()
queue_free()extends CanvasLayer
@onready var result_label: Label = %ResultLabel
func _ready() -> void:
GameEvents.goal_reached.connect(_on_goal_reached)
GameEvents.player_died.connect(_on_player_died)
%HomeButton.pressed.connect(_on_home_pressed)
func _on_goal_reached() -> void:
result_label.text = "You escaped!"
func _on_player_died() -> void:
result_label.text = "Caught! Try again."
# 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 EscapePlayer
extends CharacterBody2D
## How fast the player moves. The chaser's speed is set lower than this in
## the scene. You can always outrun it in a straight line. The maze is
## what makes it close.
@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