Back
WAVE SPAWNER ECS
Explored 0%
Sphere enemies clustered around magenta cube player 4 sphere enemies seeking dark cube target ECS runtime Wave 1/2 spawning 120 FPS Runtime 30% Runtime 90% ECS game view sphere cube enemies spawning
Unity 6 ECS · Solo · University

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
Unity 6 C# Unity ECS Unity.Entities 1.4.4 Burst Compiler ISystem WorldSubsystem TomBen Parser ScriptableObjects NativeHashMap NativeList Data-Driven Design AMD
Final Grade 0%
1.4.4
Entities Package
.TomBen
Wave Format
Burst
Compiler
ECS
Architecture
Overview

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.

Wave spawner cluster gameplay // 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.

Architecture

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.

ECS editor view // 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

Bootstrap
Runs OnStartRunning to convert all managed wave data into persistent NativeHashMaps. Creates SpawnerDataSingleton entity. Guards against duplicate creation with hard exception.

WaveSpawnerSystem

ISystem
Main ISystem runtime. Reads SpawnerDataSingleton, iterates wave rules, applies spawn delays, enforces population caps, advances WaveProgress entity on wave completion.

EnemySeekSystem

AI
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

Physics
Maps Unity Physics collision events to ECS damage buffer entries. Processes StatefulCollisionEvent components to detect hits and write DamageBufferElement to target entities.

WaveSpawnerDataAuthoring

Baker
MonoBehaviour baker. Holds managed Unit[], Cluster[], Wave[] arrays. Baker transfers data to ECS via SpawnerDataWrapper. Supports TomBen asset drag-in via custom editor.

TomBenParser

Parser
Standalone C# parser class. Tokenises .TomBen files on _Tom/_Ben/_!? boundaries. Populates WaveSpawnerDataAuthoring arrays at edit time. Emits structured console warnings on invalid entries.
Custom Format

.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

Definition
Defines an enemy unit archetype. Fields: health, speed, damage. Assigned a numeric ID and optional name.

cluster

Definition
Defines a named group of unit types with spawn counts. References type IDs with colon-count syntax: 1:3 = 3× type 1.

wave

Definition
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:

WaveRuleType SystemEach 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.
Debug Tools

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% // Runtime — 70% wave spawned
ReadoutSource
FPSLive frames-per-second from ECS SystemState.WorldUnmanaged timing.
Entity CountTotal ECS entity count in the World, read from EntityManager.
Enemy CountCount of entities with EnemyTag component — direct NativeList query.
Wave ProgressCurrent wave index / total wave count from WaveProgress entity.
Wave NameFixedString64Bytes wave name read from SpawnerDataSingleton.Waves map.
Spawn StatusCurrent state: Waiting / Spawning / Combat / Complete — from WaveProgress.State enum.
Rule IndexCurrent rule being evaluated within the active wave's rule list.
Next Spawn DelayRemaining seconds until next spawn event — from WaveProgress.SpawnTimer.
Pop CapActive 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.

Challenges

What Went Wrong

  1. 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 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.

  2. Issue 02

    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.

  3. 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> to NativeList<int>, Dictionary to NativeHashMap. The SpawnRuleData struct required three separate blittable fields to encode what was originally a polymorphic C# class hierarchy.

  4. 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.

  5. 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 on TomBenParser.cs via a #nullable enable pragma at the file level.

Architecture lessonThe 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.
Milestones

Trophies Earned

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