All lessons
Lesson 23

Don't Get Caught

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.

BUILD IT FROM

  1. Top-Down Movement: the player, copied straight in.
  2. Walls and Collisions: the maze. StaticBody2D walls block everyone.
  3. Enemy Follows Player: the chaser. One export points it at the player.

WHAT THE GLUE DOES

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.

The scenes

UI node · 2D node · % unique name · .tscn instanced scene

dont_get_caught.tscn
  • DontGetCaught Node2D dont_get_caught.gd
  • EscapePlayer CharacterBody2D player.tscn
  • Chaser CharacterBody2D chaser.tscn
  • Goal Area2D goal.tscn
  • HUD CanvasLayer hud.tscn
  • Walls Node2D
  • Wall1 StaticBody2D
  • ColorRect
  • CollisionShape2D
  • Wall2 StaticBody2D
  • ColorRect
  • CollisionShape2D
  • Wall3 StaticBody2D
  • ColorRect
  • CollisionShape2D
  • Wall4 StaticBody2D
  • ColorRect
  • CollisionShape2D
  • Wall5 StaticBody2D
  • ColorRect
  • CollisionShape2D
chaser.tscn
  • Chaser CharacterBody2D chaser.gd
  • ColorRect
  • CollisionShape2D
  • CatchZone Area2D
  • CollisionShape2D
goal.tscn
  • Goal Area2D goal.gd
  • ColorRect
  • CollisionShape2D
hud.tscn
  • HUD CanvasLayer hud.gd
  • MarginContainer
  • MessageLabel Label
  • ResultLabel Label %
  • HomeButton Button %
player.tscn
  • EscapePlayer CharacterBody2D player.gd
  • Sprite2D
  • CollisionShape2D
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()