Wave Spawner ECS
An enemy wave spawner built on Unity's Entity Component System — waves written in a custom .TomBen text format, baked into unmanaged NativeCollections and run by ISystem code with no MonoBehaviour in the hot path.
- Type
- University Module
- Module
- GDEV50004
- Level
- 5 · Year 2
- Engine
- Unity 6 ECS
- Period
- Jan – Mar 2026
- Team
- Solo
- ECS Package
- Entities 1.4.4
- File Format
- .TomBen
What is Wave Spawner ECS?
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.
// Cluster behaviour — enemies grouping in gameplay
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.
Managed → Unmanaged Pipeline
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.
// Unity editor — ECS authoring view at 50% wave
// 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
BootstrapWaveSpawnerSystem
ISystemEnemySeekSystem
AICollisionDamageSystem
PhysicsWaveSpawnerDataAuthoring
BakerTomBenParser
Parser.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
Definitioncluster
Definition1:3 = 3× type 1.wave
DefinitionC<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
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.
// Runtime — 70% wave spawned
| Readout | Source |
|---|---|
| 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
-
Issue 01
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
RequireForUpdateandTryGetSingletonpatterns before any runtime system could safely access singleton data. Early iteration frequently crashed the editor rather than throwing a catchable exception. -
Issue 02
NativeCollection lifetime management.
Every
NativeHashMapandNativeListallocated withAllocator.PersistentinOnCreatemust be manually disposed inOnDestroy. 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. -
Issue 03
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> toNativeList<int>, Dictionary toNativeHashMap. The SpawnRuleData struct required three separate blittable fields to encode what was originally a polymorphic C# class hierarchy. -
Issue 04
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.
-
Issue 05
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 onTomBenParser.csvia a#nullable enablepragma at the file level.
Trophies Earned
_Tom/_Ben delimiters and
!? field separators








