Capstone: Building a Complete Small Game

Godot Fundamentals

Chapter 10 · Capstone: Building a Complete Small Game

Nine chapters have each built one working piece in isolation — a moving sprite, a signal, a health bar, a sound effect. This capstone wires every one of them together into a single, genuinely playable small game: Coin Dash — move a player around a small arena, collect every coin while avoiding a hazard that damages you on contact, and win once every coin is gone (or lose if your health reaches zero first).

ChapterWhat it contributes to Coin Dash
1 — Game LoopThe overall Node/Scene structure; _physics_process for every moving piece
2 — GDScript BasicsTyped variables/functions and control flow throughout every script below
3 — Nodes & ScenesThe Coin scene, preload/instantiate()/add_child() to spawn several coins, custom signals
4 — 2D FundamentalsSprite2D, position/flip_h for the player facing its movement direction
5 — Input & MovementThe Input Map and Input.get_vector() driving the player
6 — Physics & CollisionCharacterBody2D + move_and_slide() for the player; Area2D + body_entered for the Coin and Hazard
7 — Signals & EventsThe Health system's health_changed/died signals; a decoupled score system
8 — UI & HUDA CanvasLayer HUD with a ProgressBar health bar, a score Label, and a pause menu
9 — Audio & PolishA coin pickup sound (await ... .finished), a damage-flash AnimationPlayer animation, a win-condition particle burst

Scene Structure

Main.tscn (Node2D) ├── Player (CharacterBody2D) - Ch5, Ch6 │ ├── Sprite2D - Ch4 │ ├── CollisionShape2D - Ch6 │ ├── AnimationPlayer ("flash") - Ch9 │ └── Health (Node) - Ch7 ├── Hazard (Area2D) - Ch6 │ ├── Sprite2D │ └── CollisionShape2D ├── HUD (CanvasLayer) - Ch8 │ ├── HealthBar (ProgressBar) │ ├── ScoreLabel (Label) │ └── PauseMenu (Control, hidden) └── CoinSpawner (Node2D) - Ch3 Coin.tscn (Area2D) - Ch3, Ch6, Ch9 ├── Sprite2D ├── CollisionShape2D └── AudioStreamPlayer2D

The Player

Movement is Chapter 5's four-directional controller, running through Chapter 6's CharacterBody2D so it respects the Hazard's collision, with Chapter 4's flip_h facing added on top.

player.gd (attached to Player)
extends CharacterBody2D const SPEED = 200.0 func _physics_process(delta: float) -> void: var direction := Input.get_vector("move_left", "move_right", "move_up", "move_down") velocity = direction * SPEED move_and_slide() if direction.x < 0: $Sprite2D.flip_h = true elif direction.x > 0: $Sprite2D.flip_h = false

The Player is tagged into the "player" group (Chapter 6), so both the Coin and Hazard can recognize it by group membership rather than by name.

health.gd (attached to the Health child node)
extends Node signal health_changed(current: int, max_health: int) signal died var current: int = 100 var max_health: int = 100 func take_damage(amount: int) -> void: current = max(current - amount, 0) health_changed.emit(current, max_health) if current == 0: died.emit()

The Hazard

An Area2D that damages the player on contact — the same body_entered pattern as the coin from Chapter 6, applied to harming rather than collecting.

hazard.gd (attached to Hazard)
extends Area2D func _ready() -> void: body_entered.connect(_on_body_entered) func _on_body_entered(body: Node2D) -> void: if body.is_in_group("player"): body.get_node("Health").take_damage(20)

The Coin

The full Chapter 9 version — hide and disable immediately on collection, play the pickup sound, and only queue_free() once that sound has finished — plus a custom coin_collected signal (Chapter 3/7) that Main.tscn listens for.

coin.gd (attached to Coin.tscn's root)
extends Area2D signal coin_collected func _ready() -> void: body_entered.connect(_on_body_entered) func _on_body_entered(body: Node2D) -> void: if body.is_in_group("player"): $CollisionShape2D.set_deferred("disabled", true) $Sprite2D.hide() coin_collected.emit() $AudioStreamPlayer2D.play() await $AudioStreamPlayer2D.finished queue_free()

The HUD

A health bar wired to Health.health_changed, a score label, and a pause menu — Chapter 8's three pieces, unchanged in shape.

health_bar.gd (attached to HUD/HealthBar)
extends ProgressBar func _ready() -> void: var health = get_node("/root/Main/Player/Health") health.health_changed.connect(_on_health_changed) min_value = 0 max_value = health.max_health value = health.current func _on_health_changed(current: int, max_health: int) -> void: value = current
pause_menu.gd (attached to HUD/PauseMenu)
extends Control func _ready() -> void: hide() func _process(delta: float) -> void: if Input.is_action_just_pressed("pause"): visible = not visible

Spawning Coins & Wiring the Win/Lose Conditions

This is the chapter's own central piece: Main.tscn's root script instances several Coins at runtime (Chapter 3), listens for every one of their coin_collected signals to update the score and check the win condition, and listens for the Player's Health.died signal for the lose condition.

main.gd (attached to Main.tscn's root)
extends Node2D const CoinScene: PackedScene = preload("res://coin.tscn") const COIN_POSITIONS = [ Vector2(100, 100), Vector2(300, 150), Vector2(500, 100), Vector2(200, 350), Vector2(450, 380), ] var score: int = 0 var coins_remaining: int = 0 func _ready() -> void: $Player/Health.died.connect(_on_player_died) for pos in COIN_POSITIONS: var coin = CoinScene.instantiate() $CoinSpawner.add_child(coin) coin.position = pos coin.coin_collected.connect(_on_coin_collected) coins_remaining += 1 func _on_coin_collected() -> void: score += 10 coins_remaining -= 1 $HUD/ScoreLabel.text = "Score: " + str(score) if coins_remaining == 0: $WinParticles.restart() print("You collected every coin - you win!") func _on_player_died() -> void: print("Game over") get_tree().paused = true
Every one of these connections happens at runtime, on instanced or already-placed nodesmain.gd is the one script in the whole game that actually knows the Player, the HUD, and every Coin all exist. None of them know about each other or about main.gd itself. Health doesn't know a HUD exists; Coin doesn't know a score system exists; Player doesn't know Coin exists at all. This is Chapter 3's and Chapter 7's own decoupling principle, scaled up to a full game: every piece is independently reusable, and main.gd's only job is wiring already-independent pieces together.

Playing Coin Dash

With everything above in place: move the player with the arrow keys or WASD, walk into a coin to collect it (with a sound and a score update), avoid the hazard (or don't, and watch the health bar drop with a flash), press Escape to pause, and collect all five coins to trigger the win-condition particle burst.

Coding Challenges — Extending Coin Dash

Challenge 1
Add a second Hazard that patrols back and forth between two points using _physics_process and position (Chapter 4's own movement pattern), rather than sitting still. It should still damage the player on contact exactly like the first Hazard.
→ Solution
Challenge 2
Extend main.gd's _on_player_died so that, instead of only printing "Game over," it also shows a Control-based "Game Over" screen (built the same way as the PauseMenu) with the final score displayed on a Label.
→ Solution
Challenge 3
Give each Coin a random chance (e.g. 20%) of being a "big coin" worth 50 points instead of 10, decided once when it's instanced in main.gd. Have the coin's own coin_collected signal pass its point value as an argument so _on_coin_collected can add the correct amount.
→ Solution

Where to Go From Here

Coin Dash deliberately stays small — one screen, no level transitions, no save system, no enemy AI beyond a simple patrol. Every one of those is a natural next step once the fundamentals in this course feel solid: autoloads/singletons for global game state (mentioned briefly in Chapter 8), multiple scenes and level transitions, tilemaps for building larger levels, and enemy AI beyond simple back-and-forth movement are all genuine Intermediate/Advanced territory. A Unity/C# sibling course under this same Game Development subject remains a real future option too, reusing this site's own existing C# Fundamentals and C# Intermediate/Advanced courses as a running start.

Quick Reference — What Coin Dash Demonstrates

  • CharacterBody2D player movement (Ch5, Ch6) with facing (Ch4)
  • Area2D hazard and coins, both using body_entered (Ch6)
  • Runtime scene instancing for multiple coins (Ch3)
  • A decoupled Health system and a decoupled coin-collection system (Ch7)
  • A HUD reacting to signals with zero direct references back into it (Ch8)
  • A pickup sound that finishes before its node is freed, and a win-condition particle burst (Ch9)
  • One "wiring" script (main.gd) as the only node that knows every other piece exists