Lesson 24

Don't Get Caught

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.

BUILD IT FROM

  1. Top-Down Movement: the player, copied straight in.
  2. Enemy Follows Player: the chaser, copied in and left alone.

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.

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.

New code in this lesson

None of the earlier lessons use these. Each one shows the line where it appears here.

set_physics_process()

Turns a node's _physics_process() off with false, freezing it, or back on with true.

game.gd
player.set_physics_process(false)

The scenes

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

game.tscn
  • Game Node2D game.gd
  • Player CharacterBody2D player.tscn
  • Enemy CharacterBody2D enemy.tscn
  • Goal Area2D goal.tscn
  • HUD CanvasLayer hud.tscn
  • Walls Node2D
  • Wall1 StaticBody2D block.tscn
  • Wall2 StaticBody2D block.tscn
  • Wall3 StaticBody2D block.tscn
  • Wall4 StaticBody2D block.tscn
  • Wall5 StaticBody2D block.tscn
enemy.tscn
  • Enemy CharacterBody2D enemy.gd
  • Polygon2D
  • CollisionShape2D
  • CatchZone Area2D
  • CollisionShape2D
goal.tscn
  • Goal Area2D goal.gd
  • Polygon2D
  • CollisionShape2D
hud.tscn
  • HUD CanvasLayer hud.gd
  • MarginContainer
  • MessageLabel Label
  • ResultLabel Label %
player.tscn
  • Player CharacterBody2D player.gd
  • Sprite2D
  • CollisionShape2D
block.tscn
  • Block StaticBody2D
  • Polygon2D
  • CollisionPolygon2D

FileSystem

  • game.tscn
  • game.gd
  • enemy.tscn
  • enemy.gd
  • goal.tscn
  • goal.gd
  • hud.tscn
  • hud.gd
  • player.tscn
  • player.gd
  • block.tscn

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.

The code

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()