Back
WAVE SPAWNER ECS
92% · First Class

What is Wave Spawner ECS?

Wave spawner cluster gameplay // Cluster behaviour — enemies grouping in gameplay

Wave Spawner ECS is the second assessment for GDEV50004 Advanced Mechanics Design at Staffordshire University (Level 5, Year 2). While the Tank Controller explored physics simulation with a traditional MonoBehaviour architecture, this project explored a completely different paradigm: Unity's Entity Component System (ECS) architecture via Unity.Entities 1.4.4 with Burst compilation.

The assignment was to build an enemy wave spawner. Rather than a simple array of prefab spawns, the solution builds a full data pipeline: wave configuration defined in human-readable .TomBen text files → parsed at authoring time → baked from managed C# into unmanaged NativeCollections → driven by an ISystem-based ECS runtime that manages wave progression, population caps, spawn delays, and enemy AI entirely in ECS — no MonoBehaviour in the hot path.

The project includes a real-time ECS debug panel (UI Toolkit) displaying live FPS, entity count, enemy count, wave progress, rule index, and next spawn delay. Enemy seek AI navigates toward the player via ECS transforms. Collision damage is processed through ECS physics events. Five .TomBen example files demonstrate edge cases in the parser: Simple, Mixed, Optional, Partial, and Duplicate.

  • Awarded 92% — First Class for GDEV50004 Advanced Mechanics Design (shared with Tank Controller, same module)
  • Unity ECS architecture: ISystem, EntityCommandBuffer, NativeHashMap, NativeList for all wave data — no MonoBehaviour runtime
  • Custom .TomBen file format: human-readable wave definition language with _Tom/_Ben delimiters and !? field separators
  • WaveSpawnerDataAuthoring bakes managed C# wave data into unmanaged ECS components via WaveSpawnerCreateSingletonSystem
  • SpawnerDataSingleton pattern — single authoritative entity holds all wave state with guard against duplicate singletons
  • Three entity definition types: Unit (health/speed/damage), Cluster (unit group rules), Wave (ordered cluster/type spawn rules)
  • Five example .TomBen files: Simple, Mixed, Optional, Partial, Duplicate — demonstrating parser edge cases
  • ECS debug panel (UI Toolkit): live FPS, entity count, enemy count, wave progress, wave name, status, rule index, next spawn delay, pop cap

Managed → Unmanaged Pipeline

ECS editor view // Unity editor — ECS authoring view at 50% wave

The core design challenge in ECS is that managed C# objects — class arrays, strings, reference types — cannot live inside ECS components. Everything must be blittable, unmanaged, and safe for Burst compilation. The project solves this with a two-stage authoring and runtime pipeline.

Stage 1 — Authoring (managed): WaveSpawnerDataAuthoring is a standard MonoBehaviour that holds managed arrays of C# wave, cluster, and unit definitions. These are populated either directly in the inspector or by loading a .TomBen file via the custom WaveSpawnerDataAuthoringEditor. At bake time, Unity's ECS baker converts this component into a lightweight wrapper.

// WaveSpawnerCreateSingletonSystem.OnStartRunning // Reads the baked SpawnerDataWrapper, converts all arrays to NativeHashMaps var unitMap = new NativeHashMap<int, UnitData> (units.Length, Allocator.Persistent); var clusterMap = new NativeHashMap<int, ClusterData>(clusters.Length, Allocator.Persistent); var waveMap = new NativeHashMap<int, WaveData> (waves.Length, Allocator.Persistent); var waveOrder = new NativeArray<int>(orderedWaveIds, Allocator.Persistent); // All packed into SpawnerDataSingleton on a single authoritative entity EntityManager.SetComponentData(singleton, new SpawnerDataSingleton { Units = unitMap, Clusters = clusterMap, Waves = waveMap, WaveOrder = waveOrder });

Stage 2 — Runtime (unmanaged): SpawnerDataSingleton is guarded — if a second instance is ever created, the system immediately throws a UnityException. A separate WaveProgress entity tracks the current wave index. The original WaveSpawnerSystem.cs — fully written — was commented out and superseded by this singleton-driven approach, with a comment from the author: "my mum said to make this script obsolete".

Key ECS systems built:

WaveSpawnerCreateSingletonSystem
Runs OnStartRunning to convert all managed wave data into persistent NativeHashMaps. Creates SpawnerDataSingleton entity. Guards against duplicate creation with hard exception.
WaveSpawnerSystem
Main ISystem runtime. Reads SpawnerDataSingleton, iterates wave rules, applies spawn delays, enforces population caps, advances WaveProgress entity on wave completion.
EnemySeekSystem
Pure ECS enemy AI. Queries all entities with SeekTarget component, reads player transform via ECS LocalTransform, applies velocity toward target. No MonoBehaviour, no NavMesh.
CollisionDamageSystem
Maps Unity Physics collision events to ECS damage buffer entries. Processes StatefulCollisionEvent components to detect hits and write DamageBufferElement to target entities.
WaveSpawnerDataAuthoring
MonoBehaviour baker. Holds managed Unit[], Cluster[], Wave[] arrays. Baker transfers data to ECS via SpawnerDataWrapper. Supports TomBen asset drag-in via custom editor.
TomBenParser
Standalone C# parser class. Tokenises .TomBen files on _Tom/_Ben/_!? boundaries. Populates WaveSpawnerDataAuthoring arrays at edit time. Emits structured console warnings on invalid entries.

.TomBen Wave Definition Language

TomBen is a custom human-readable data format invented for this project to define enemy wave configurations as plain text files. It uses three structural tokens: _Tom (block open), _Ben (block close), and !? (field separator). Three definition types exist — type, cluster, and wave — and they can appear in any order in the file; the parser resolves references by ID.

type
Defines an enemy unit archetype. Fields: health, speed, damage. Assigned a numeric ID and optional name.
cluster
Defines a named group of unit types with spawn counts. References type IDs with colon-count syntax: 1:3 = 3× type 1.
wave
Ordered list of spawn rules. Each rule is either a Cluster reference C<id> or Type reference T<id>, with optional population cap or delay in angle brackets.

Syntax reference:

type - <id> (<name>) _Tom health=><n>!?speed=><n>!?damage=><n>!? _Ben cluster - <id> (<name>) _Tom <unitId>:<count>!? _Ben wave - <id> (<name>) _Tom C<clusterId>[<popCap>]!?T<typeId>[<delay>]!? _Ben

Example — AMD_ECS_Mixed_E.TomBen:

wave - 1 (start) _Tom C1<1>!?C1<5>!?C2<5>!? _Ben type - 1 (small) _Tom health=>10!?speed=>5!?damage=>1!? _Ben wave - 2 (boss) _Tom C2<1>!?C2<1>!?C2<1>!? _Ben cluster - 2 (elite) _Tom 1:1!?2:2!? _Ben type - 2 (big) _Tom health=>20!?speed=>5!?damage=>1!? _Ben cluster - 1 (simple) _Tom 1:3!? _Ben

Definitions are intentionally order-independent — the parser makes two passes, first registering all IDs, then resolving cross-references. This allows wave definitions to reference clusters that appear later in the file, mirroring how a level designer would think (waves first, then unit details).

The parser (TomBenParser.cs) tokenises on _Tom / _Ben / !? boundaries. It populates WaveSpawnerDataAuthoring arrays which are then baked into ECS component data via WaveSpawnerCreateSingletonSystem on OnStartRunning.

Five example files provided:

AMD_ECS_Simple_E.TomBen AMD_ECS_Mixed_E.TomBen AMD_ECS_Optional_E.TomBen AMD_ECS_Partial_E.TomBen AMD_ECS_Duplicate_E.TomBen
WaveRuleType System
Each rule entry in a wave block resolves to one of three rule types at parse time: Cluster — spawn all units defined by a cluster ID; Type — spawn a single unit type directly by ID; Optional — a population-cap triggered rule that only activates when the current enemy count falls below the cap threshold. This allows passive "refill" rules to run alongside primary spawn sequences.

ECS Debug Panel & Authoring Editor

The ECS Debug Panel (built with UI Toolkit) displays real-time ECS state directly in the game view. It reads from the SpawnerDataSingleton and WaveProgress entities each frame to surface the full wave runtime state without any MonoBehaviour involvement in the data path.

FPS
Live frames-per-second from ECS SystemState.WorldUnmanaged timing.
Entity Count
Total ECS entity count in the World, read from EntityManager.
Enemy Count
Count of entities with EnemyTag component — direct NativeList query.
Wave Progress
Current wave index / total wave count from WaveProgress entity.
Wave Name
FixedString64Bytes wave name read from SpawnerDataSingleton.Waves map.
Spawn Status
Current state: Waiting / Spawning / Combat / Complete — from WaveProgress.State enum.
Rule Index
Current rule being evaluated within the active wave's rule list.
Next Spawn Delay
Remaining seconds until next spawn event — from WaveProgress.SpawnTimer.
Pop Cap
Active population cap value for Optional rules, 0 if no cap is set on current rule.

The WaveSpawnerDataAuthoringEditor is a custom Unity editor (Editor/ folder) that provides an in-editor wave preview and a TomBen asset drag-in button. Designers can drag a .TomBen TextAsset into the authoring component and click Load to immediately populate all wave, cluster, and unit arrays — no play mode required.

What Went Wrong

ECS null-reference style errors don't work the same as OOP. Unmanaged structs have no null state — a missing component causes structural query failures, not NullReferenceExceptions. This required learning RequireForUpdate and TryGetSingleton patterns before any runtime system could safely access singleton data. Early iteration frequently crashed the editor rather than throwing a catchable exception.

NativeCollection lifetime management. Every NativeHashMap and NativeList allocated with Allocator.Persistent in OnCreate must be manually disposed in OnDestroy. Forgetting a single disposal causes Unity to emit a memory leak warning on every recompile until the project is restarted — a persistent and confusing error that was traced back to the NativeArray waveOrder allocation.

Managed-to-unmanaged conversion. C# class arrays cannot live in ECS components. Every field in the authoring data required explicit translation: string names to FixedString64Bytes, List<int> to NativeList<int>, Dictionary to NativeHashMap. The SpawnRuleData struct required three separate blittable fields to encode what was originally a polymorphic C# class hierarchy.

The original WaveSpawnerSystem was fully written then abandoned. Once the singleton-driven architecture proved correct, the original system — which queried entities directly rather than reading from a centralised singleton — was commented out in its entirety. The comment left in source: "my mum said to make this script obsolete". The singleton pattern is now the sole runtime authority.

TomBenParser CS8632 nullable annotation warnings. The TomBen parser used nullable reference types (string?) which required the C# 8 nullable context. Enabling it project-wide would cascade warnings across all existing Unity-generated files. Resolved by enabling the nullable context only on TomBenParser.cs via a #nullable enable pragma at the file level.

Architecture lesson The managed → unmanaged pipeline is the central ECS design constraint. Every data structure decision must be made with its ECS representation in mind from the start — retrofitting a class-based design into unmanaged structs is significantly more work than designing for ECS from the outset. The SpawnerDataSingleton pattern, once established, made the rest of the runtime straightforward to build.