| name | godot-game-loop-waves |
| description | Expert patterns for managing combat waves, difficulty scaling, and automated enemy spawning in Godot 4. Use when building wave-based shooters, tower defense, or arena games. |
Architectural Thinking: The "Wave-State" Pattern
A Master implementation treats waves as Data-Driven Transitions. Instead of hardcoding spawn counts, use a WaveResource to define "Encounters" that the WaveManager processes sequentially.
Core Responsibilities
- Manager: Orchestrates the timeline. Handles delays between waves and tracks "Victory" conditions (all enemies dead).
- Spawner: Decoupled nodes that provide spatial context for where enemies appear (
wave_spawner.gd / wave_weighted_spawner.gd).
- Resource: Immutable data containers that allow designers to rebalance the game without touching code.
Density Decision Tree (pick scale before coding)
wave_manager.gd is the SceneTree golden path (deferred add_child, group/signal clear counts, optional pool). Treat it as prototype→mid-scale — for RID swarms, follow wave_loop_patterns.gd instead of instantiating thousands of nodes.
Composition Golden Path
- Author a wave_resource.gd composition table.
- Place wave_spawner.gd Markers (or wave_weighted_spawner.gd when variety weights matter).
- Point wave_manager.gd
spawner at that Marker; manager defers spawn and clears via &"enemies" group + signals.
- When weights replace fixed counts, call
WaveWeightedSpawner.spawn_enemy() from the composition loop (or set manager spawner to the weighted node).
Expert Code Patterns
1. The Async Wave Trigger
Use await timers in wave_manager.gd — MANDATORY read before writing a custom timeline. Spawns use call_deferred(&"add_child", …); clear via signals/groups (not get_children() scans).
2. Composition-Based Spawning
Define variety in wave_resource.gd; place units with wave_spawner.gd / wave_weighted_spawner.gd. Do not hardcode scene paths in the manager.
Master Decision Matrix: Progression
| Pattern | Best For | Logic |
|---|
| Linear | Story missions | Hand-crafted list of WaveResource. |
| Endless | Survival modes | Code-generated WaveResource with multiplier math. |
| Triggered | RPG Encounters | Wave starts only when player enters an Area3D. |
NEVER Do
- NEVER iterate through get_children() to find all enemies — This is extremely slow. Always add enemies to an "enemies" group and use
get_tree().get_nodes_in_group(&"enemies") for efficient access.
- NEVER constantly instantiate() and queue_free() hundreds of enemies — This causes garbage collection stutters. Use an object pool to reuse existing enemy instances.
- NEVER spawn thousands of separate MeshInstance3D nodes for swarms — This will tank your draw calls. Use
MultiMeshInstance3D to batch thousands of meshes into a single GPU call.
- NEVER calculate pathfinding for hundreds of agents on the main thread — This will freeze your game. Enable
use_async_iterations on your navigation regions or use NavigationServer3D.query_path().
- NEVER forget to check is_inside_tree() before adding a child — If the spawner is queued for deletion, adding a child will crash. Always verify the spawner is still active in the tree.
- NEVER assign a preloaded resource (like stats.tres) directly to spawned mobs — They will all share the exact same health/stats. Always call
base_stats.duplicate_deep() to give each mob its own unique data.
- NEVER use standard strings for high-frequency group calls — Always use
StringName (&"enemies", &"take_damage") for optimal hash performance and to avoid unnecessary string allocations.
- NEVER spawn entities directly inside physics callbacks synchronously — Instantiating nodes during physics steps can corrupt the physics state. Always use
call_deferred(&"add_child", enemy).
- NEVER leave CollisionShapes on dead enemies active — Corpses will block towers and navigation. Use
set_deferred("disabled", true) immediately upon death.
- NEVER synchronize complex Object types via MultiplayerSynchronizer — It only supports primitive types. For complex data, sync a UID or ID and look up the data locally on the client.
- NEVER auto-start waves without player feedback — Always provide a UI countdown, a visual "Wave Incoming" effect, or a start button to maintain player agency.
- NEVER hardcode spawn positions at (0,0,0) — Use
Marker3D nodes in the editor so you can visually adjust spawn points without digging into code.
- NEVER check wave completion by counting children every frame — It's too expensive. Maintain a local counter or use a signal-based system to track active enemy counts.
Available Scripts
MANDATORY: Read the appropriate script before implementing the corresponding pattern.
10 Expert patterns: MultiMesh swarms, async pathfinding, background preloading, and server-side physics mobs.
Orchestrates the timeline, delays between waves, and tracks clear via group counts + signals. Uses call_deferred add_child; optional pool via use_pool / recycle_enemy.
Data containers for wave compositions and difficulty settings.
Marker3D spatial portal — get_spawn_position() with optional radius jitter. Wire as WaveManager.spawner.
Weighted random enemy selection at a Marker. Use when composition variety is probability-driven rather than fixed counts.
Expert Wave Patterns
1. Occlusion Culling for Swarms
To optimize performance with hundreds of enemies, enable Occlusion Culling.
- Setup: Add an
OccluderInstance3D to your arena and bake it.
- Result: Enemies completely hidden behind walls/pillars won't be processed by the GPU, significantly boosting FPS.
2. Wave UI Architecture
Decouple your wave data from the UI using a CanvasLayer and signals.
- Wave Counter: Display current/total waves.
- Health Bars: Use a
TextureProgressBar on a CanvasLayer for bosses, or Sprite3D with a viewport texture for individual enemy health bars.
Expert knowledge (on demand)
LLM-ignorance rule: If a general agent would not know it before reading, load the reference — never delete expert deltas.
Reference
Progressive disclosure: open Official Documentation links only when researching a specific API; load Related Skills when routing to a peer domain — do not preload the whole lattice.
Official Documentation
- Resources —
WaveResource compositions and delays stay designer-editable without hardcoding spawn tables in managers.
- Nodes and scene instances —
PackedScene.instantiate() plus add_child / call_deferred is the safe spawn path for wave enemies.
- Groups — track live mobs with
StringName groups and get_node_count_in_group instead of scanning children every frame.
- Idle and Physics Processing — keep pacing on timers/
await; never instantiate mid-physics callback without deferring.
- SceneTreeTimer — pre-wave delays and spawn-rate gaps via
create_timer without a forever _process countdown.
- Using signals —
wave_started / wave_cleared / all_waves_complete decouple UI, audio, and combat from the manager timeline.
- Background loading —
ResourceLoader.load_threaded_request bosses/heavy waves so first spawn does not hitch.
- Random number generation — weighted composition and spawn jitter with
RandomNumberGenerator.rand_weighted.
- Using MultiMesh — batch swarm visuals when hundreds of minions would explode draw calls.
- Occlusion culling — hide off-camera arena mobs so dense waves stay GPU-affordable.
- Navigation introduction (3D) — async agent paths and separate maps for flying vs walking wave units.
- Using Servers — PhysicsServer3D/NavigationServer3D RID swarms when SceneTree nodes cannot scale.
Related Skills
Prerequisites
Complements
- godot-signal-architecture — ownership of wave/UI/combat signals so countdown and clear events stay leak-free.
- godot-combat-system — hitboxes, death, and damage that decrement active-enemy counters when a wave unit dies.
- godot-navigation-pathfinding — NavigationServer queries, avoidance masks, and maps for pathing hundreds of wave agents.
- godot-scene-management — pools, threaded loads, and safe add/remove when waves churn PackedScenes.
- godot-performance-optimization — MultiMesh, occlusion, and server-side bodies for swarm density budgets.
- godot-monte-carlo-balancer — style-matrix sampling of spawn counts, rates, and difficulty curves before shipping endless modes.
Downstream / consumers
Master
- godot-master — library router and mirrored module entry for cross-skill discovery.