2024 Game Development GitHub ↗

Overview

Depot Gato is a 2D tower defense game built in Godot 4 using GDScript. Waves of enemies follow a navigation path toward a central depot, and the player places cat-themed towers along buildable tiles to intercept and eliminate them before they arrive. The project was built from scratch to explore Godot's scene composition model, signal-based decoupled architecture, and NavigationAgent2D-driven pathfinding.

Core systems include a wave spawning manager driven by configurable WaveData resources, three distinct tower types with independent attack ranges and damage profiles, enemy pathfinding via NavigationAgent2D with a live HP bar and animated states, a currency economy that rewards kills and gates tower placement, and a central GameState autoload singleton that brokers win and lose conditions through Godot signals.

Tech Stack

  • Godot 4 — game engine, scene tree, physics layers, CanvasLayer UI
  • GDScript — all game logic: towers, enemies, wave manager, game state
  • NavigationAgent2D — dynamic pathfinding and steering for enemies
  • TileMap — path tiles and buildable zones; grid snapping for tower placement
  • Area2D — tower detection radius; projectile hit detection
  • AnimationPlayer — enemy walk, hurt, and death animations
  • Aseprite — pixel art sprite sheets, exported as PNG sprite atlases
  • Git — version control

Scene & System Architecture

The game is structured around Godot's composable scene tree. The root Main.tscn owns the TileMap, the HUD CanvasLayer, and the WaveManager node. Each gameplay entity (enemies and towers) is a self-contained scene instanced at runtime. A GameState autoload singleton acts as a shared data bus — no node directly queries another node's state; instead, all components emit and subscribe to signals.

graph TD A["Main.tscn (Root)"]:::primary A --> B["HUD.tscn (CanvasLayer)"]:::secondary B --> B1["Currency Label"]:::secondary B --> B2["Lives Label"]:::secondary B --> B3["Wave Counter"]:::secondary B --> B4["Tower Palette"]:::secondary A --> C["TileMap"]:::secondary C --> C1["Path Tiles (layer 0)"]:::secondary C --> C2["Buildable Tiles (layer 1)"]:::secondary A --> D["WaveManager.gd"]:::primary D --> E["Enemy.tscn (instanced per spawn)"]:::primary E --> E1["NavigationAgent2D"]:::secondary E --> E2["HP ProgressBar"]:::secondary E --> E3["AnimationPlayer"]:::secondary E --> E4["Enemy.gd"]:::secondary A --> F["Tower Variants"]:::primary F --> F1["BasicTower.tscn"]:::success F --> F2["SniperTower.tscn"]:::success F --> F3["SplashTower.tscn"]:::success subgraph Systems G["TowerPlacement.gd"]:::primary H["Tower.gd (base)"]:::primary I["Enemy.gd"]:::primary J["GameState.gd (autoload)"]:::primary end G -->|"check buildable tile"| C2 G -->|"deduct currency"| J G -->|"instance tower scene"| F H -->|"Area2D detects enemies"| E H -->|"select nearest target"| I H -->|"fire Projectile"| K["Projectile.tscn"]:::secondary I -->|"follow navigation path"| E1 I -->|"on reach depot"| J J -->|"tracks lives, currency, wave"| B J -->|"emits game_over / victory"| A classDef primary fill:#1a1a2e,stroke:#00d4ff,color:#e0e0e0 classDef secondary fill:#181818,stroke:#1e1e1e,color:#888 classDef success fill:#1a1a2e,stroke:#00ff88,color:#e0e0e0

Build Process

  1. 01

    Project Setup and Physics Layers

    Created a new Godot 4 project with a 2D scene root. Configured three named physics collision layers in Project Settings: Layer 1 for enemies, Layer 2 for towers, and Layer 3 for projectiles. This separation ensures that Area2D nodes on towers only detect the enemy layer, preventing false collision signals. Set pixel art import defaults globally: Filter set to Nearest, mipmaps disabled, and Snap 2D Transforms to Pixel enabled under Rendering settings to eliminate sub-pixel blurring on scaled sprites.

  2. 02

    TileMap and NavigationRegion2D Pathfinding

    Designed the TileMap with two layers: a path layer (tiles enemies traverse) and a buildable layer (tiles where towers may be placed). Added a NavigationRegion2D node covering the path layer and baked the navigation mesh. A known pitfall here is that NavigationRegion2D.bake_navigation_polygon() must be deferred to the next frame after the scene is fully loaded — calling it on _ready() directly caused enemies to spawn before the mesh was valid, placing them inside walls. The fix: call_deferred("_bake_nav").

  3. 03

    Enemy Scene

    Each enemy is a CharacterBody2D scene with three child nodes: a NavigationAgent2D for steering, a ProgressBar positioned above the sprite for the HP bar, and an AnimationPlayer with three named animations — walk, hurt, and death. The Enemy.gd script calls navigation_agent.get_next_path_position() each physics frame and drives movement with move_and_slide(). On reaching the depot (checked via NavigationAgent2D.is_navigation_finished()), the script emits a reached_depot signal caught by GameState.gd.

  4. 04

    Tower System

    Tower.gd is a base script inherited by all three tower variants. Each tower has an Area2D child sized to its detection radius; the body_entered and body_exited signals maintain a live targets_in_range: Array. A Timer node fires on the attack cooldown interval; the handler selects the first valid target with is_instance_valid(target) and not target.is_queued_for_deletion(), then instances a Projectile.tscn at a Marker2D spawn point with velocity aimed at the target's position. BasicTower has the shortest range and fastest fire rate; SniperTower has maximum range and high single-target damage; SplashTower uses an Area2D on the projectile itself to hit all enemies within an explosion radius on impact.

  5. 05

    Wave Manager

    Wave data is stored as WaveData resources — custom Godot Resource subclasses with exported fields: an array of dictionaries specifying enemy scene path, count, and spawn interval. WaveManager.gd iterates through the current wave's entries using a spawn Timer. When all enemies in a wave are dead (tracked by a counter decremented via the enemy_died signal), the manager emits wave_complete. If no more waves remain, it emits victory; otherwise it pauses briefly and begins the next wave. This resource-driven approach makes adding new waves a data-only change without touching code.

  6. 06

    UI and HUD

    The HUD is a separate CanvasLayer scene instanced under Main. It holds four Label nodes (currency, lives, wave number, next-wave countdown) and a tower placement palette built from TextureButton nodes. Labels are updated exclusively through GameState signals — currency_changed(new_val), lives_changed(new_val), wave_changed(wave_num) — so the HUD has zero direct references to gameplay nodes. Clicking a palette button sets a selected tower type variable in TowerPlacement.gd, which then listens for mouse input on unbuildable vs. buildable cells.

Game Loop Workflow

The following diagram traces the runtime flow of a single round: from the player placing a tower through enemy spawning, combat resolution, and terminal win/lose branching.

flowchart TD P["Player clicks buildable tile"]:::primary PC{"TowerPlacement:\nenough currency?"}:::primary PD["Deduct cost\nInstance Tower scene"]:::success PE["Show: insufficient funds"]:::secondary W["WaveManager Timer fires"]:::primary WS["Spawn Enemy at start node"]:::primary WN["Enemy navigates path via\nNavigationAgent2D"]:::primary TA{"Tower Area2D:\nenemy in range?"}:::primary TF["Tower targets enemy\nfire Projectile"]:::success PH{"Projectile hits Enemy"}:::primary HD["Deal damage\nreduce Enemy HP"]:::primary EK{"HP <= 0?"}:::primary ED["Enemy dies\nPlayer gains currency"]:::success EC["Enemy continues on path"]:::secondary ER{"Enemy reaches Depot?"}:::primary EL["Deduct life from GameState"]:::secondary GL{"Lives == 0?"}:::primary GO["Emit game_over signal\nShow Game Over screen"]:::secondary WD{"All wave enemies dead?"}:::primary LW{"Last wave?"}:::primary NW["Start next wave"]:::primary VC["Emit victory signal\nShow Victory screen"]:::success P --> PC PC -->|Yes| PD PC -->|No| PE PD --> TA W --> WS --> WN --> TA TA -->|Yes| TF --> PH --> HD --> EK TA -->|No| WN EK -->|Yes| ED --> WD EK -->|No| EC --> ER ER -->|Yes| EL --> GL ER -->|No| TA GL -->|Yes| GO GL -->|No| WD WD -->|Yes| LW WD -->|No| WN LW -->|No| NW --> W LW -->|Yes| VC classDef primary fill:#1a1a2e,stroke:#00d4ff,color:#e0e0e0 classDef secondary fill:#181818,stroke:#1e1e1e,color:#888 classDef success fill:#1a1a2e,stroke:#00ff88,color:#e0e0e0

Enemy State Machine

Each enemy instance transitions through four states managed in Enemy.gd. The state variable gates which AnimationPlayer track plays and whether movement logic runs each physics frame.

stateDiagram-v2 [*] --> Idle : scene instantiated Idle --> Walking : navigation path set\n(on spawn) Walking --> Attacking : reached depot\nor in attack range Walking --> Dead : HP <= 0\n(projectile hit) Attacking --> Dead : HP <= 0 Dead --> [*] : queue_free()\nenemy removed

In the Idle state the enemy waits for NavigationAgent2D.set_target_position() to be called by WaveManager immediately after spawning. Once the path is ready the state flips to Walking and the walk animation loops. Attacking is entered if the enemy reaches the depot node or steps into a melee-range trigger (reserved for a melee enemy variant). Dead plays the death animation and then calls queue_free(), which is caught by WaveManager's enemy counter to track wave completion.

Challenges & Solutions

  • NavigationAgent2D jitter at path corners. Enemies visibly oscillated when rounding tight corners because the agent re-targeted the same waypoint on alternating frames. Fixed by increasing both path_desired_distance and target_desired_distance on the agent — giving the steering system a wider acceptance radius so it advances to the next waypoint cleanly.
  • Towers targeting enemies about to die. When two towers selected the same low-HP enemy simultaneously, one projectile was wasted on a body already queued for deletion. Solved by wrapping all target reads in is_instance_valid(target) and not target.is_queued_for_deletion() before firing, and clearing the target slot when an enemy emits its died signal.
  • Pixel art appearing blurry after scaling. Godot 4's default texture filter is Linear, which interpolates between pixels and softens low-resolution sprites. Fixed globally by setting the project-wide texture import default to Filter: Nearest and enabling Rendering > 2D > Snap 2D Transforms to Pixel, which locks sprite positions to whole pixel coordinates and eliminates sub-pixel shimmer during movement.
  • Enemies spawning inside walls after navigation bake. Calling NavigationRegion2D.bake_navigation_polygon() synchronously in _ready() completed before the TileMap nodes had fully initialized their collision shapes for the frame. Solved with call_deferred("_bake_nav") so the bake runs at the start of the next frame after all nodes are settled, ensuring enemies always spawn on a valid navigation mesh.

What I Learned

  • Godot 4 scene composition — designing reusable, self-contained scenes and instancing them at runtime vs. placing them statically in the editor tree.
  • NavigationAgent2D pathfinding and steering — baking navigation meshes, setting target positions, reading next path positions per physics frame, and tuning desired-distance thresholds.
  • Signal-based decoupled architecture — emitting named signals from gameplay nodes and subscribing in the HUD and game state manager, so no node holds a hard reference to another's internal state.
  • Autoload singletons for shared state — using Godot's autoload system to create a globally accessible GameState that owns lives, currency, and wave data without coupling scene nodes to each other.
  • Pixel art workflow from Aseprite to Godot — authoring animations in Aseprite, exporting sprite sheets with JSON metadata, and configuring Godot's SpriteFrames resource to map sheet regions to named animation tracks.
  • Tower defense game loop fundamentals — balancing economy (kill reward vs. tower cost), tuning attack cooldown and range per tower class, and designing wave data to create escalating difficulty without hardcoding values.
Godot 4 GDScript Tower Defense Pixel Art Game Development NavigationAgent2D 2D Game