A project, not a new idea: escape the maze before the chaser reaches you. Assembles Top-Down Movement 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. Two things you already built are assembled into one game: reach the green goal, and do not let the chaser touch you.
The maze around them is nothing new either. Every wall is a copy of block.tscn, the same block the floors and platforms are built from in Platformer Movement: a Polygon2D you see and a CollisionPolygon2D you bump into, cut from the same corners. Position places a copy, Scale sizes it, and dragging one in the editor moves both halves together.
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.
None of the earlier lessons use these. Each one shows the line where it appears here.
Turns a node's _physics_process() off with false, freezing it, or back on with true.
player.set_physics_process(false)■ 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
# 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: Player = $Player
@onready var chaser: Enemy = $Enemy
@onready var catch_zone: Area2D = $Enemy/CatchZone
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 Enemy node. The level is
# the parent of both enemy and player, so it reaches DOWN to connect,
# the same two-step pattern game_events.gd explains.
catch_zone.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
# "Player" is the class_name at the top of player.gd. Without this
# check, the chaser could be "caught" by a wall.
if body is Player:
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 Enemy
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
## How close the chaser gets before it stops pushing.
@export var stop_distance: float = 30.0
# Who to chase. ".." means "go up to whoever holds me", which is the level, so
# this reads: from the level above me, grab the player sitting beside me.
@onready var _target: CharacterBody2D = $"../Player"
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 Player:
# Two files are listening: game.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
@onready var home_button: Button = %HomeButton
func _ready() -> void:
GameEvents.goal_reached.connect(_on_goal_reached)
GameEvents.player_died.connect(_on_player_died)
home_button.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 Player
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
@onready var sprite: Sprite2D = $Sprite2D
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:
sprite.flip_h = direction.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
# Health and Damage lesson
signal player_died