Two zones, two endings, one full-screen panel. Uses a flag so the game can only end once.
Loading the game engine. This takes a moment the first time.
Two zones. Green wins, red loses. Either one ends the game and brings up the full screen panel.
■ UI node · ■ 2D node · % unique name · .tscn instanced scene
extends Node2D
# "$GameOverPanel" is the node named GameOverPanel, sitting under this one.
@onready var game_over_panel: GameOverPanel = $GameOverPanel
## Once the game is over, ignore any more zone touches.
var is_game_over: bool = false
func _ready() -> void:
%WinZone.body_entered.connect(_on_win_zone_entered)
%LoseZone.body_entered.connect(_on_lose_zone_entered)
func _on_win_zone_entered(body: Node2D) -> void:
# Two reasons to quit early: the game already ended, or the thing that
# touched the zone was not the player. "not (...)" flips true and false.
if is_game_over or not (body is WinLosePlayer):
return
# Set the flag FIRST, so a second touch cannot get past the line above.
is_game_over = true
# GameOverPanel is our child, so we skip signals and call its function.
game_over_panel.show_win()
func _on_lose_zone_entered(body: Node2D) -> void:
if is_game_over or not (body is WinLosePlayer):
return
is_game_over = true
game_over_panel.show_lose()class_name GameOverPanel
extends CanvasLayer
@onready var message_label: Label = %MessageLabel
func _ready() -> void:
# Start hidden. The level shows it only when the game ends.
%Overlay.hide()
%PlayAgainButton.pressed.connect(_on_play_again_pressed)
%HomeButton.pressed.connect(_on_home_pressed)
## Called BY win_and_lose_screen.gd when the player reaches the green zone.
func show_win() -> void:
_end_game("You Win!", Color(0.4, 1.0, 0.5))
## Called BY win_and_lose_screen.gd when the player reaches the red zone.
func show_lose() -> void:
_end_game("You Lose!", Color(1.0, 0.4, 0.4))
# Both show_win() and show_lose() do the same three things, so the shared
# work lives here and they each just pass in their own words and colour.
func _end_game(message: String, color: Color) -> void:
message_label.text = message
# "modulate" tints the label. Color(red, green, blue), 0.0 to 1.0 each.
message_label.modulate = color
%Overlay.show()
# Freeze the whole game behind the panel.
get_tree().paused = true
# "grab_focus()" puts the keyboard on this button, so pressing Enter
# works without touching the mouse.
%PlayAgainButton.grab_focus()
func _on_play_again_pressed() -> void:
# ALWAYS unpause before leaving, or the next scene opens frozen solid.
get_tree().paused = false
# "reload_current_scene()" builds this level again from scratch.
get_tree().reload_current_scene()
func _on_home_pressed() -> void:
get_tree().paused = false
get_tree().change_scene_to_file("res://shared/ui/lesson_select.tscn")class_name WinLosePlayer
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