Introduction: The Performance Revolution
Series Overview: This is Part 10 of our 16-part Unity Game Engine Series. We tackle Unity's most significant architectural evolution — DOTS — which fundamentally changes how games process data, leverage modern CPUs, and achieve performance levels impossible with traditional MonoBehaviour approaches.
1
Unity Basics & Interface
Editor overview, assets, prefabs, architecture
2
C# Scripting Fundamentals
MonoBehaviour, coroutines, input systems, patterns
3
GameObjects & Components
Transforms, renderers, custom components
4
Physics & Collisions
Rigidbody, colliders, raycasting, forces
5
UI Systems
Canvas, uGUI, UI Toolkit, responsive design
6
Animation & State Machines
Animator, blend trees, IK, Timeline
7
Audio & Visual Effects
AudioSource, particles, VFX Graph, post-processing
8
Building & Publishing
Build pipeline, optimization, platforms, monetization
9
Rendering Pipelines
URP, HDRP, Shader Graph, lighting systems
10
Data-Oriented Tech Stack
ECS, Jobs System, Burst Compiler
You Are Here
11
AI & Gameplay Systems
NavMesh, FSMs, behavior trees, procedural gen
12
Multiplayer & Networking
Netcode, RPCs, latency, prediction
13
Tools & Editor Scripting
Custom editors, debug tools, CI/CD
14
Architecture & Clean Code
Service locators, DI, ScriptableObject architecture
15
Performance Optimization
CPU/GPU profiling, memory, object pooling
16
Production & Industry Practices
Git, Agile, asset pipelines, debugging at scale
DOTS (Data-Oriented Technology Stack) is Unity's answer to a fundamental problem: modern CPUs have gotten dramatically faster at processing sequential data in memory, but traditional object-oriented game code scatters data across the heap, causing constant cache misses that waste 90%+ of the CPU's potential throughput.
DOTS consists of three pillars: the Entity Component System (ECS) for cache-friendly data layout, the C# Jobs System for safe multithreading across all CPU cores, and the Burst Compiler for compiling C# to highly optimized native code with SIMD vectorization. Together, they enable performance improvements of 10-100x over equivalent MonoBehaviour code.
Key Insight: DOTS is not a replacement for MonoBehaviour in all cases. It's a specialized tool for performance-critical systems: massive entity counts (crowds, particles, bullets), simulation-heavy games (city builders, RTS), and any scenario where you're CPU-bound. Most games use a hybrid approach — DOTS for performance-critical systems, MonoBehaviour for gameplay logic, UI, and game flow.
1. Why DOTS?
1.1 OOP Limitations for Games
Traditional Unity code using MonoBehaviour and GameObjects has inherent performance limitations rooted in how object-oriented programming interacts with modern hardware:
| Problem |
OOP (MonoBehaviour) |
DOTS (ECS) |
| Cache Misses |
Objects scattered across heap. Iterating 10,000 enemies = 10,000 cache misses (each object at random memory address) |
Components stored contiguously in memory chunks. Iterating 10,000 entities = sequential memory access, 0 cache misses |
| GC Pressure |
Every MonoBehaviour is a managed object on the GC heap. Creating/destroying causes garbage collection stalls (10-50ms spikes) |
Entities and components use unmanaged memory. No GC involvement. Smooth, predictable frame times |
| Single-Threaded |
Update() runs on main thread only. 8-core CPU? You're using 1 core for gameplay logic |
Jobs System distributes work across ALL CPU cores automatically. 8-core CPU = 8x throughput potential |
| Virtual Calls |
MonoBehaviour methods use virtual dispatch — CPU can't predict branch targets, pipeline stalls |
Systems process data directly — no virtual calls, CPU pipeline stays full |
| Data Layout |
AoS (Array of Structs): each object contains ALL its data together, even if you only need position |
SoA (Struct of Arrays): components of the same type packed together. Read only what you need |
1.2 The Data-Oriented Paradigm
Analogy: Imagine you're a librarian who needs to check the publication year of 10,000 books. In the OOP approach, each book is in a different room of a massive building — you walk to room 1, open the book, note the year, walk to room 2, and so on. Each trip takes 100 steps. In the DOTS approach, all the publication years are written on a single continuous scroll — you stand in one place and read them sequentially at the speed your eyes can move. Same data, same result, but 1,000x faster access.
The Numbers: A modern CPU can access L1 cache in ~1ns (nanosecond), L2 in ~3ns, L3 in ~10ns, but main RAM in ~100ns. That's a 100x penalty for cache misses. When you iterate over MonoBehaviours scattered across the heap, nearly every access is a cache miss. ECS organizes data so that sequential iteration stays in L1/L2 cache — making the CPU 10-100x more efficient for the same operations.
2. Entity Component System (ECS)
2.1 Entities & Components
In ECS, the three concepts have precise, distinct roles:
| Concept |
What It Is |
OOP Equivalent |
Key Properties |
| Entity |
A unique ID (integer) — nothing more |
GameObject (but with zero overhead) |
No data, no behavior. Just an identifier that links components |
| Component |
A pure data struct (IComponentData) |
MonoBehaviour fields (data only, no methods) |
Blittable structs, no references, no methods, unmanaged memory |
| System |
Behavior processor — reads/writes components |
MonoBehaviour Update() methods |
Processes all entities matching a query, runs on main thread or scheduled as Jobs |
// ECS Components — pure data structs, no behavior
using Unity.Entities;
using Unity.Mathematics;
// Position data for every entity in the world
public struct Position : IComponentData
{
public float3 Value;
}
// Velocity for moving entities
public struct Velocity : IComponentData
{
public float3 Value;
}
// Health for damageable entities
public struct Health : IComponentData
{
public float Current;
public float Max;
}
// Tag component — zero-size, used for filtering
// "This entity is an enemy" — no data needed, just the tag
public struct EnemyTag : IComponentData { }
// Shared component — entities with same value share memory
// Good for faction/team grouping
public struct FactionShared : ISharedComponentData
{
public int FactionId;
}
// Buffer element — variable-length data per entity
// Like a List<T> but ECS-compatible
[InternalBufferCapacity(8)]
public struct DamageBufferElement : IBufferElementData
{
public float Amount;
public Entity Source;
}
2.2 Systems & Entity Queries
// ECS System — processes all entities with matching components
using Unity.Entities;
using Unity.Mathematics;
using Unity.Burst;
using Unity.Transforms;
// ISystem (unmanaged, Burst-compatible, preferred)
[BurstCompile]
public partial struct MovementSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
// Require these components to exist before system runs
state.RequireForUpdate<Position>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
float deltaTime = SystemAPI.Time.DeltaTime;
// Process every entity that has both Position and Velocity
// This is automatically parallelized across CPU cores!
foreach (var (transform, velocity) in
SystemAPI.Query<RefRW<LocalTransform>, RefRO<Velocity>>())
{
transform.ValueRW.Position += velocity.ValueRO.Value * deltaTime;
}
}
}
// System that processes damage buffers
[BurstCompile]
public partial struct DamageProcessingSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
// EntityCommandBuffer for deferred structural changes
var ecb = new EntityCommandBuffer(Unity.Collections.Allocator.Temp);
foreach (var (health, damageBuffer, entity) in
SystemAPI.Query<RefRW<Health>, DynamicBuffer<DamageBufferElement>>()
.WithEntityAccess())
{
// Sum all damage this frame
float totalDamage = 0;
foreach (var dmg in damageBuffer)
{
totalDamage += dmg.Amount;
}
// Apply damage
health.ValueRW.Current -= totalDamage;
// Clear the buffer for next frame
damageBuffer.Clear();
// Destroy entity if dead (deferred — safe during iteration)
if (health.ValueRO.Current <= 0)
{
ecb.DestroyEntity(entity);
}
}
ecb.Playback(state.EntityManager);
ecb.Dispose();
}
}
2.3 Archetypes & Chunks — The Memory Model
Understanding archetypes and chunks is key to understanding why ECS is fast:
| Concept |
Description |
Analogy |
| Archetype |
A unique combination of component types. All entities with [Position, Velocity, Health] share one archetype |
A filing cabinet label: "Employees with Name + Salary + Department" |
| Chunk |
A 16KB block of contiguous memory holding entities of the same archetype |
One drawer in the filing cabinet, holding as many employee records as fit |
| Structural Change |
Adding/removing components changes an entity's archetype, requiring it to move to a different chunk |
Moving an employee's file from one cabinet to another when they change departments |
Performance Warning: Structural changes (adding/removing components, creating/destroying entities) are expensive because they move data between chunks. Avoid them during hot loops. Instead, use EntityCommandBuffer to batch structural changes and apply them all at once at a sync point. Use enable/disable components (IEnableableComponent) instead of add/remove when toggling behavior.
3. Jobs System
3.1 Job Types & Scheduling
The C# Jobs System enables safe multithreading without the typical dangers of shared memory concurrency (race conditions, deadlocks). Unity's job safety system validates at compile time that no two jobs access the same data simultaneously.
| Job Type |
Parallelism |
Use Case |
| IJob |
Single worker thread |
One task that must run off main thread (file I/O, complex calculation) |
| IJobParallelFor |
Work split across all cores |
Process large arrays — each element independently (positions, velocities) |
| IJobEntity |
Parallel across entities |
ECS integration — process matching entities across all chunks and cores |
| IJobChunk |
Parallel across chunks |
Low-level chunk access — maximum control over iteration |
// IJobParallelFor — process a large array across all CPU cores
using Unity.Jobs;
using Unity.Collections;
using Unity.Mathematics;
using Unity.Burst;
using UnityEngine;
public class ParticleSimulation : MonoBehaviour
{
private NativeArray<float3> positions;
private NativeArray<float3> velocities;
private const int PARTICLE_COUNT = 100000;
private void Start()
{
positions = new NativeArray<float3>(PARTICLE_COUNT, Allocator.Persistent);
velocities = new NativeArray<float3>(PARTICLE_COUNT, Allocator.Persistent);
// Initialize particles
var random = new Unity.Mathematics.Random(42);
for (int i = 0; i < PARTICLE_COUNT; i++)
{
positions[i] = random.NextFloat3(-50, 50);
velocities[i] = random.NextFloat3Direction() * random.NextFloat(1, 5);
}
}
private void Update()
{
// Create the job
var moveJob = new MoveParticlesJob
{
Positions = positions,
Velocities = velocities,
DeltaTime = Time.deltaTime,
Bounds = 50f
};
// Schedule it — splits 100,000 items across all cores
// innerLoopBatchCount of 256 = each thread processes 256 particles at a time
JobHandle handle = moveJob.Schedule(PARTICLE_COUNT, 256);
// Complete before we read results (or pass handle to dependent job)
handle.Complete();
}
private void OnDestroy()
{
if (positions.IsCreated) positions.Dispose();
if (velocities.IsCreated) velocities.Dispose();
}
}
[BurstCompile]
struct MoveParticlesJob : IJobParallelFor
{
public NativeArray<float3> Positions;
[ReadOnly] public NativeArray<float3> Velocities;
public float DeltaTime;
public float Bounds;
public void Execute(int index)
{
float3 pos = Positions[index];
pos += Velocities[index] * DeltaTime;
// Wrap around bounds
pos = math.fmod(pos + Bounds, Bounds * 2) - Bounds;
Positions[index] = pos;
}
}
// IJobEntity — ECS + Jobs integration
using Unity.Entities;
using Unity.Transforms;
using Unity.Mathematics;
using Unity.Burst;
[BurstCompile]
public partial struct EnemyMovementJob : IJobEntity
{
public float DeltaTime;
public float3 PlayerPosition;
// Execute runs for each entity matching the query
// Query is inferred from parameters: needs LocalTransform, Velocity, and EnemyTag
void Execute(ref LocalTransform transform, in Velocity velocity, in EnemyTag tag)
{
// Move toward player
float3 direction = math.normalize(PlayerPosition - transform.Position);
transform.Position += direction * math.length(velocity.Value) * DeltaTime;
}
}
// Schedule the job from a system
[BurstCompile]
public partial struct EnemyMovementSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
// Get player position (simplified — assumes single player entity)
float3 playerPos = float3.zero;
foreach (var (transform, tag) in
SystemAPI.Query<RefRO<LocalTransform>, RefRO<PlayerTag>>())
{
playerPos = transform.ValueRO.Position;
}
var job = new EnemyMovementJob
{
DeltaTime = SystemAPI.Time.DeltaTime,
PlayerPosition = playerPos
};
// ScheduleParallel distributes entities across all CPU cores
job.ScheduleParallel();
}
}
public struct PlayerTag : IComponentData { }
3.2 NativeContainer Types
NativeContainers are the thread-safe, unmanaged-memory collections that Jobs and ECS use instead of managed C# collections:
| Container |
C# Equivalent |
Thread Safety |
Use Case |
| NativeArray<T> |
T[] |
ReadOnly parallel, ReadWrite single |
Fixed-size data: positions, velocities, results |
| NativeList<T> |
List<T> |
ReadOnly parallel, ReadWrite single |
Dynamic-size data: collected results, filtered entities |
| NativeHashMap<K,V> |
Dictionary<K,V> |
ParallelWriter for parallel add |
Spatial hashing, entity lookup tables, caching |
| NativeQueue<T> |
Queue<T> |
ParallelWriter for parallel enqueue |
Event queues, work items, producer-consumer patterns |
| NativeMultiHashMap<K,V> |
Dictionary<K, List<V>> |
ParallelWriter for parallel add |
Spatial partitioning, grouping entities by cell/region |
Allocator Choices: Every NativeContainer requires an Allocator: Temp (one frame, auto-disposed), TempJob (4 frames, must dispose — use for jobs that complete same frame), Persistent (lives forever, must manually dispose — use for long-lived data). Using the wrong allocator causes memory leaks (Persistent not disposed) or use-after-free crashes (Temp used across frames).
4. Burst Compiler
4.1 Burst Basics & Benchmarks
The Burst Compiler translates C# Job code into highly optimized native machine code using LLVM. It's the "secret sauce" that makes DOTS performance possible — without Burst, the Jobs System is merely multithreaded. With Burst, it's multithreaded and running at C/C++ speeds.
| Operation |
Managed C# |
Burst-Compiled |
Speedup |
| Vector3 addition (1M ops) |
~8ms |
~0.08ms |
100x |
| Matrix multiplication (100K) |
~12ms |
~0.5ms |
24x |
| Array sum (10M floats) |
~15ms |
~0.3ms |
50x |
| Particle simulation (100K) |
~45ms (single-thread) |
~0.6ms (Burst + parallel) |
75x |
| Pathfinding (A*, 10K agents) |
~200ms |
~5ms (Burst + parallel) |
40x |
// Burst compilation — just add the attribute!
using Unity.Burst;
using Unity.Jobs;
using Unity.Collections;
using Unity.Mathematics;
[BurstCompile(CompileSynchronously = true)] // Compile immediately, not lazy
struct GravitySimulationJob : IJobParallelFor
{
[ReadOnly] public NativeArray<float3> Positions;
[ReadOnly] public NativeArray<float> Masses;
public NativeArray<float3> Forces;
public float GravitationalConstant;
public void Execute(int i)
{
float3 totalForce = float3.zero;
float3 posI = Positions[i];
float massI = Masses[i];
for (int j = 0; j < Positions.Length; j++)
{
if (i == j) continue;
float3 direction = Positions[j] - posI;
float distSq = math.max(math.lengthsq(direction), 0.01f);
float forceMagnitude = GravitationalConstant * massI * Masses[j] / distSq;
totalForce += math.normalize(direction) * forceMagnitude;
}
Forces[i] = totalForce;
}
}
// Burst restrictions:
// - No managed types (string, class, delegates)
// - No try/catch (exception handling is managed)
// - No virtual method calls
// - No LINQ
// - Must use Unity.Mathematics instead of UnityEngine.Mathf
// In return: 10-100x performance
4.2 Mathematics Library & SIMD
Burst uses the Unity.Mathematics library instead of UnityEngine.Mathf. This library is designed for SIMD (Single Instruction, Multiple Data) vectorization — processing 4 or 8 floats in a single CPU instruction:
| UnityEngine |
Unity.Mathematics |
Why |
Vector3 |
float3 |
Value type, SIMD-friendly, no heap allocation |
Quaternion |
quaternion |
Same math, Burst-compatible |
Matrix4x4 |
float4x4 |
SIMD matrix operations, column-major |
Mathf.Sin() |
math.sin() |
Burst intrinsic — compiles to hardware sin instruction |
Random.Range() |
Random.NextFloat() |
Deterministic, thread-safe, seedable |
// SIMD vectorization example with Burst
using Unity.Burst;
using Unity.Mathematics;
[BurstCompile]
public static class MathHelpers
{
// Burst will auto-vectorize this to process 4 floats at once (SSE)
// or 8 floats at once (AVX2) depending on CPU
[BurstCompile]
public static void NormalizeArray(ref NativeArray<float3> vectors, int count)
{
for (int i = 0; i < count; i++)
{
// math.normalize compiles to SIMD rsqrt + multiply
vectors[i] = math.normalize(vectors[i]);
}
// On AVX2: processes 8 components per cycle instead of 1
// Result: ~8x speedup from vectorization alone, on top of Burst's other optimizations
}
}
5. Migration from MonoBehaviour to DOTS
5.1 Hybrid Approach & SubScenes
You don't have to rewrite your entire game in ECS. The hybrid approach lets MonoBehaviour and ECS coexist in the same project:
| System Type |
Use MonoBehaviour |
Use ECS |
| Game Flow |
Menu navigation, game states, cutscenes |
|
| UI |
Canvas, UI Toolkit, menus |
|
| Player Controller |
Input handling, camera control (1 entity) |
|
| Enemies/NPCs |
|
Movement, AI, pathfinding for 1000+ entities |
| Projectiles |
|
Bullets, particles, effects (high entity count) |
| World Simulation |
|
Terrain, vegetation, physics, weather systems |
SubScenes are the bridge between the two worlds. A SubScene converts GameObjects (with authoring components) into ECS entities at build time or load time:
// SubScene workflow: Author in GameObjects, run in ECS
// 1. Create a SubScene (right-click in Hierarchy > New Sub Scene)
// 2. Place GameObjects inside the SubScene
// 3. Attach Baker components that convert to ECS at bake time
// Step 1: Define your ECS component
public struct EnemyData : IComponentData
{
public float Speed;
public float DetectionRange;
public int FactionId;
}
// Step 2: Create an authoring component (MonoBehaviour, for editor)
public class EnemyAuthoring : MonoBehaviour
{
public float Speed = 5f;
public float DetectionRange = 20f;
public int FactionId = 1;
}
// Step 3: Create a Baker that converts authoring to ECS
public class EnemyBaker : Baker<EnemyAuthoring>
{
public override void Bake(EnemyAuthoring authoring)
{
var entity = GetEntity(TransformUsageFlags.Dynamic);
AddComponent(entity, new EnemyData
{
Speed = authoring.Speed,
DetectionRange = authoring.DetectionRange,
FactionId = authoring.FactionId
});
AddComponent(entity, new EnemyTag());
}
}
// Now: Place EnemyAuthoring on GameObjects in a SubScene.
// At bake time, Unity converts them to ECS entities with EnemyData + EnemyTag.
// Systems process them with full DOTS performance.
5.2 Baking: Authoring to Runtime
The Baking Pipeline: Baking is the process of converting "authoring" GameObjects (what you see in the editor) into "runtime" ECS entities (what runs in the game). This happens automatically when you open/build a SubScene. The Baker class is your converter — it reads MonoBehaviour data and creates IComponentData equivalents. This separation means you can use Unity's familiar editor workflow while getting ECS performance at runtime.
6. Real-World Performance
6.1 Handling 100K+ Entities
DOTS enables entity counts that are impossible with MonoBehaviour. Here's a practical comparison:
| Entity Count |
MonoBehaviour (Update) |
DOTS (ECS + Jobs + Burst) |
| 1,000 |
~2ms (fine for most games) |
~0.02ms |
| 10,000 |
~20ms (struggling at 60fps) |
~0.2ms |
| 50,000 |
~100ms (unplayable) |
~1.0ms |
| 100,000 |
~200ms (slideshow) |
~2.0ms (smooth 60fps) |
| 500,000 |
Not feasible |
~10ms (playable at 60fps) |
6.2 Spatial Partitioning & LOD with DOTS
// Spatial hashing for efficient neighbor queries in DOTS
using Unity.Entities;
using Unity.Mathematics;
using Unity.Collections;
using Unity.Burst;
[BurstCompile]
public partial struct SpatialHashingSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
int entityCount = SystemAPI.QueryBuilder()
.WithAll<LocalTransform, EnemyTag>().Build().CalculateEntityCount();
// Create spatial hash map: cell -> entities in that cell
var spatialMap = new NativeMultiHashMap<int2, Entity>(
entityCount, Allocator.TempJob);
float cellSize = 10f; // 10 unit grid cells
// Phase 1: Build spatial hash (parallel)
var buildJob = new BuildSpatialHashJob
{
SpatialMap = spatialMap.AsParallelWriter(),
CellSize = cellSize
};
buildJob.ScheduleParallel(state.Dependency).Complete();
// Phase 2: Query neighbors for each entity
// Example: find all enemies within 15 units of the player
// Hash the player position, check surrounding cells (3x3 grid)
// This turns O(n^2) distance checks into O(n) lookups!
spatialMap.Dispose();
}
}
[BurstCompile]
partial struct BuildSpatialHashJob : IJobEntity
{
public NativeMultiHashMap<int2, Entity>.ParallelWriter SpatialMap;
public float CellSize;
void Execute(Entity entity, in LocalTransform transform, in EnemyTag tag)
{
int2 cell = new int2(
(int)math.floor(transform.Position.x / CellSize),
(int)math.floor(transform.Position.z / CellSize)
);
SpatialMap.Add(cell, entity);
}
}
Case Study
Megacity Demo — 4.5 Million Mesh Renderers
Unity's Megacity demo is the flagship DOTS showcase. It renders a sprawling futuristic cityscape with:
- 4.5 million mesh renderers — buildings, vehicles, infrastructure, all as ECS entities
- 200,000 dynamic objects — flying cars with streaming audio, lights, and navigation
- 100,000 audio sources — spatialized ambient sound from each vehicle and environment element
- Running at 60fps on mid-range hardware using ECS + Jobs + Burst + GPU instancing
Without DOTS, this scene would be impossible — MonoBehaviour would run at <1fps with this entity count. Megacity demonstrates that DOTS isn't just an optimization; it enables entirely new categories of games.
4.5M Renderers
200K Dynamic
60fps
Open World
Case Study
Latios Framework — Community-Driven DOTS Ecosystem
The Latios Framework is an open-source collection of DOTS packages that extends Unity's ECS with production-ready features the core package doesn't provide:
- Kinemation — skeletal animation system for ECS (thousands of animated characters)
- Psyshock — DOTS-native spatial query and collision detection system
- Myri — ECS audio system supporting thousands of simultaneous sources
- Demonstrates that the DOTS community is actively building the ecosystem needed for production use
Open Source
Community
Production-Ready
Animation + Audio
7. History: DOTS Preview to ECS 1.0
| Year |
Milestone |
Impact |
| 2018 |
DOTS announced at GDC, ECS preview packages |
Paradigm shift announcement. Early adopters experiment with unstable APIs. "Pure ECS" vs "Hybrid ECS" confusion |
| 2019 |
Burst Compiler 1.0 stable, Jobs System stable |
First production-ready DOTS component. Jobs + Burst usable without full ECS commitment |
| 2020-2021 |
Major ECS API rewrites, SubScene workflow |
API instability frustrates early adopters. Baking pipeline replaces conversion workflow. Community patience tested |
| 2022 |
Entities 1.0 release candidate |
ISystem (unmanaged) replaces SystemBase as preferred approach. API stabilizes significantly |
| 2023 |
ECS 1.0 stable with Unity 2022 LTS |
First production-stable ECS release. Teams begin adopting for shipped titles. Megacity multiplayer demo |
| 2024-2026 |
Unity 6, expanded DOTS ecosystem |
DOTS Physics, DOTS Animation improvements, NetCode for Entities, growing asset store support. DOTS moves from experimental to standard practice for performance-critical systems |
Exercises & Self-Assessment
Exercise 1
ECS Fundamentals Lab
Build a basic ECS simulation from scratch:
- Create a new Unity project and install the Entities package (
com.unity.entities)
- Define components:
Position, Velocity, LifeTime (float, decrements each frame)
- Create a spawner system that creates 10,000 entities with random positions and velocities
- Create a
MovementSystem that updates positions based on velocities
- Create a
LifeTimeSystem that destroys entities when their lifetime reaches zero (use EntityCommandBuffer)
- Profile: how many ms does the update take for 10K, 50K, and 100K entities?
Exercise 2
Jobs + Burst Performance Comparison
Measure the real impact of Jobs and Burst:
- Create an array of 1,000,000 float3 values representing particle positions
- Implement a gravity simulation that updates all positions each frame
- Version 1: Regular C# for loop on main thread — measure time
- Version 2: IJobParallelFor without Burst — measure time
- Version 3: IJobParallelFor with [BurstCompile] — measure time
- Create a comparison table showing the speedup at each stage
Exercise 3
Hybrid Migration Exercise
Practice migrating a MonoBehaviour system to ECS:
- Start with a MonoBehaviour "BoidSimulation" — 500 boids with alignment, cohesion, and separation
- Profile the MonoBehaviour version — note frame time
- Migrate to ECS: create BoidData component, BoidSystem, and BoidAuthoring + Baker
- Use a SubScene for the boid entities, keep the camera controller as a MonoBehaviour
- Scale up to 10,000 boids — compare frame times between the two versions
Exercise 4
Reflective Questions
- Explain why cache locality matters more than raw computational speed for modern game performance. What hardware trend makes this increasingly important?
- You have a city builder game with 50,000 citizens. Each citizen has health, hunger, job, home, and path data. Design the ECS component layout — what are the archetypes, and why would you split data into separate components vs combining them?
- What happens if two parallel jobs try to write to the same NativeArray element? How does Unity's job safety system prevent this, and what's the performance implication?
- Burst cannot compile code that uses managed types (strings, classes, delegates). Explain why this restriction exists and how it enables the performance gains Burst provides.
- A game uses MonoBehaviour for the player, UI, and game flow, but ECS for 20,000 enemies and 50,000 projectiles. How would you communicate between the two worlds (e.g., player takes damage from an ECS projectile)?
Conclusion & Next Steps
You now understand Unity's most powerful performance framework. Here are the key takeaways from Part 10:
- DOTS solves fundamental hardware problems — cache locality, multithreading, and native code compilation. These aren't theoretical benefits; they deliver 10-100x real-world speedups
- ECS separates data from behavior — entities are IDs, components are pure data structs, systems process matching entities. This layout maximizes cache coherence
- The Jobs System provides safe multithreading — schedule work across all CPU cores without race conditions or deadlocks. The safety system catches errors at compile time
- Burst Compiler is the performance multiplier — it compiles C# to native code with SIMD vectorization. Always add [BurstCompile] to Jobs and Systems
- Use NativeContainers (NativeArray, NativeList, NativeHashMap) instead of managed collections for zero-GC, thread-safe data access
- Hybrid approach is practical — use MonoBehaviour for player, UI, and game flow. Use DOTS for performance-critical mass-entity systems
- Baking (SubScenes) bridges editor authoring and runtime performance — design with GameObjects, run with entities
Next in the Series
In Part 11: AI & Gameplay Systems, we build intelligent game agents using NavMesh pathfinding, finite state machines, behavior trees, and procedural generation — the gameplay systems that make your game world feel alive and responsive.
Continue the Series
Part 11: AI & Gameplay Systems
NavMesh navigation, finite state machines, behavior trees, and procedural generation techniques.
Read Article
Part 12: Multiplayer & Networking
Netcode for GameObjects and Entities, RPCs, client-side prediction, and lag compensation.
Read Article
Part 9: Rendering Pipelines
URP, HDRP, Shader Graph, HLSL shaders, and comprehensive lighting systems.
Read Article