One script, three scenes. Each level fills in where to go next, so the same code sends you somewhere different.
Loading the game engine. This takes a moment the first time.
One script, three scenes. The same file is attached to all three, so the code exists in one place. What makes the levels behave differently is the value each scene stores in next_level_path: a script can be shared, but every scene keeps its own copy of the @export values you fill in.
■ UI node · ■ 2D node · % unique name · .tscn instanced scene
extends Node2D
## Which scene to load when the player reaches the goal.
## Leave this EMPTY on the last level. Empty means "you finished them all".
@export_file("*.tscn") var next_level_path: String = ""
func _ready() -> void:
# GoalArea is the green zone. We put the name "_on_goal_reached" inside
# ".connect()". That is the function Godot runs when it gets touched.
%GoalArea.body_entered.connect(_on_goal_reached)
# "body" is whatever touched the goal.
func _on_goal_reached(body: Node2D) -> void:
# "LevelPlayer" is the class_name at the top of player.gd.
if body is LevelPlayer:
# An empty path means there is no next level, so this was the last one.
if next_level_path != "":
# Throw this whole scene away and load the next one. Anything not
# saved somewhere outside the scene is gone for good.
get_tree().change_scene_to_file(next_level_path)
else:
# The last level is done. hud.gd connects to "goal_reached" and
# puts the finish message on screen.
GameEvents.goal_reached.emit()extends CanvasLayer
# "%LevelLabel" means "the node named LevelLabel in this scene."
@onready var level_label: Label = %LevelLabel
func _ready() -> void:
# Only the LAST level emits "goal_reached". See level_base.gd. The
# earlier levels load the next scene instead of emitting anything.
GameEvents.goal_reached.connect(_on_all_levels_complete)
%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")
func _on_all_levels_complete() -> void:
level_label.text = "All levels complete!"class_name LevelPlayer
extends CharacterBody2D
## How fast the player moves. "@export" puts "speed" at the top of the
## Inspector panel, so you can change it there instead of in this file.
@export var speed: float = 200.0
# "_physics_process()" runs about 60 times a second. Godot calls it for you.
func _physics_process(_delta: float) -> void:
# "Input.get_vector(...)" checks all four keys at once and hands back
# an arrow pointing where to go. The four names in quotes are ours.
# See them under Project > Project Settings > Input Map.
var direction: Vector2 = Input.get_vector(
"move_left", "move_right", "move_up", "move_down"
)
# "direction" is only WHICH WAY (its length is always 1).
# "direction * speed" stretches it, so "velocity" is also HOW FAST.
velocity = direction * speed
# Nothing moves until "move_and_slide()" runs.
# "Slide" means we slide along walls instead of sticking to them.
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