Welcome to the fourth lesson. We have a flying bird and generating pipes, but there are no consequences or rewards yet. In this module, we will implement collision detection for crashing, track the player's score using custom canvas elements, and build a Game Over loop.
1. Building the User Interface#
To ensure our UI always stays on top of the screen and doesn't scroll with the camera or game world, we use a CanvasLayer.
- Open your
Mainscene. - Add a CanvasLayer node and name it
UI. - Add a Control node as a child of
UIand set its anchors to "Full Rect" (spanning the whole screen). - Add a Label for the score. Name it
ScoreLabel. In the Inspector, under Theme Overrides > Fonts, assign a retro font and set the size to48. Center it at the top of the screen. - Add a PanelContainer named
GameOverScreencontaining a "Game Over" Label and a "Restart" Button. Set its visibility tohiddenby default.
2. Detecting Crashes#
Our bird needs to know when it hits a pipe or the floor.
- Open the
Birdscene. - In the
_physics_processfunction,move_and_slide()automatically handles body collisions. We can check if a collision occurred right after calling it.
Update your Bird.gd script:
extends CharacterBody2D
class_name Bird
signal crashed
# ... existing variables ...
var is_alive: bool = true
func _physics_process(delta: float) -> void:
if not is_alive:
# Just fall if dead
velocity.y += gravity * delta
move_and_slide()
return
# ... existing gravity and flap logic ...
move_and_slide()
# Check for collisions with pipes (StaticBody2D)
for i in get_slide_collision_count():
var collision = get_slide_collision(i)
if collision.get_collider() is StaticBody2D:
die()
func die() -> void:
is_alive = false
crashed.emit()
3. Tying it Together: The Game Manager#
Now, let's orchestrate the score, the UI, and the game-over state from our Main.gd script.
- In your
Mainscene, connect thecrashedsignal from yourBirdnode to theMainscript. - Update
Main.gdto handle the score and game over logic.
extends Node2D
@onready var score_label: Label = $UI/Control/ScoreLabel
@onready var game_over_screen: PanelContainer = $UI/Control/GameOverScreen
@onready var spawn_timer: Timer = $SpawnTimer
@onready var bird: Bird = $Bird
var score: int = 0
func _ready() -> void:
# Ensure the background shader moves when we start
RenderingServer.global_shader_parameter_set("game_speed_multiplier", 1.0)
# ... existing spawn logic ...
func add_score() -> void:
score += 1
score_label.text = str(score)
func _on_bird_crashed() -> void:
# Stop spawning pipes
spawn_timer.stop()
# Stop the scrolling background shader we created in Lesson 2
RenderingServer.global_shader_parameter_set("game_speed_multiplier", 0.0)
# Stop existing pipes from moving (assuming pipes check the global multiplier or have a script)
get_tree().call_group("pipes", "set_process", false)
# Show UI
game_over_screen.show()
Connecting the Score: Don't forget to connect the scored signal from the ScoreZone Area2D in your pipes to the add_score function in Main.gd when you instantiate them!
4. Restarting the Game#
Finally, connect the pressed signal of your Restart button inside the GameOverScreen to your Main.gd script:
func _on_restart_button_pressed() -> void:
# Reload the current scene to start fresh
get_tree().reload_current_scene()
You now have a fully playable, scoring, and loopable core gameplay experience!
.webp&w=1920&q=75)
