unity-ecs-patterns ยท diff

git:20260307.47a5dbc to git:20260522.be57c0b

2 added, 562 removed. Audit A to A.

---
name: unity-ecs-patterns
description: Master Unity ECS (Entity Component System) with DOTS, Jobs, and Burst for high-performance game development. Use when building data-oriented games, optimizing performance, or working with large entity counts.
---
# Unity ECS Patterns
Production patterns for Unity's Data-Oriented Technology Stack (DOTS) including Entity Component System, Job System, and Burst Compiler.
## When to Use This Skill
- Building high-performance Unity games
- Managing thousands of entities efficiently
- Implementing data-oriented game systems
- Optimizing CPU-bound game logic
- Converting OOP game code to ECS
- Using Jobs and Burst for parallelization
## Core Concepts
### 1. ECS vs OOP
| Aspect | Traditional OOP | ECS/DOTS |
| ----------- | ----------------- | --------------- |
| Data layout | Object-oriented | Data-oriented |
| Memory | Scattered | Contiguous |
| Processing | Per-object | Batched |
| Scaling | Poor with count | Linear scaling |
| Best for | Complex behaviors | Mass simulation |
### 2. DOTS Components
```
Entity: Lightweight ID (no data)
Component: Pure data (no behavior)
System: Logic that processes components
World: Container for entities
Archetype: Unique combination of components
Chunk: Memory block for same-archetype entities
```
- ## Patterns
-
- ### Pattern 1: Basic ECS Setup
-
- ```csharp
- using Unity.Entities;
- using Unity.Mathematics;
- using Unity.Transforms;
- using Unity.Burst;
- using Unity.Collections;
-
- // Component: Pure data, no methods
- public struct Speed : IComponentData
- {
- public float Value;
- }
-
- public struct Health : IComponentData
- {
- public float Current;
- public float Max;
- }
-
- public struct Target : IComponentData
- {
- public Entity Value;
- }
-
- // Tag component (zero-size marker)
- public struct EnemyTag : IComponentData { }
- public struct PlayerTag : IComponentData { }
-
- // Buffer component (variable-size array)
- [InternalBufferCapacity(8)]
- public struct InventoryItem : IBufferElementData
- {
- public int ItemId;
- public int Quantity;
- }
-
- // Shared component (grouped entities)
- public struct TeamId : ISharedComponentData
- {
- public int Value;
- }
- ```
-
- ### Pattern 2: Systems with ISystem (Recommended)
-
- ```csharp
- using Unity.Entities;
- using Unity.Transforms;
- using Unity.Mathematics;
- using Unity.Burst;
-
- // ISystem: Unmanaged, Burst-compatible, highest performance
- [BurstCompile]
- public partial struct MovementSystem : ISystem
- {
- [BurstCompile]
- public void OnCreate(ref SystemState state)
- {
- // Require components before system runs
- state.RequireForUpdate<Speed>();
- }
-
- [BurstCompile]
- public void OnUpdate(ref SystemState state)
- {
- float deltaTime = SystemAPI.Time.DeltaTime;
-
- // Simple foreach - auto-generates job
- foreach (var (transform, speed) in
- SystemAPI.Query<RefRW<LocalTransform>, RefRO<Speed>>())
- {
- transform.ValueRW.Position +=
- new float3(0, 0, speed.ValueRO.Value * deltaTime);
- }
- }
-
- [BurstCompile]
- public void OnDestroy(ref SystemState state) { }
- }
-
- // With explicit job for more control
- [BurstCompile]
- public partial struct MovementJobSystem : ISystem
- {
- [BurstCompile]
- public void OnUpdate(ref SystemState state)
- {
- var job = new MoveJob
- {
- DeltaTime = SystemAPI.Time.DeltaTime
- };
-
- state.Dependency = job.ScheduleParallel(state.Dependency);
- }
- }
-
- [BurstCompile]
- public partial struct MoveJob : IJobEntity
- {
- public float DeltaTime;
-
- void Execute(ref LocalTransform transform, in Speed speed)
- {
- transform.Position += new float3(0, 0, speed.Value * DeltaTime);
- }
- }
- ```
-
- ### Pattern 3: Entity Queries
-
- ```csharp
- [BurstCompile]
- public partial struct QueryExamplesSystem : ISystem
- {
- private EntityQuery _enemyQuery;
-
- public void OnCreate(ref SystemState state)
- {
- // Build query manually for complex cases
- _enemyQuery = new EntityQueryBuilder(Allocator.Temp)
- .WithAll<EnemyTag, Health, LocalTransform>()
- .WithNone<Dead>()
- .WithOptions(EntityQueryOptions.FilterWriteGroup)
- .Build(ref state);
- }
-
- [BurstCompile]
- public void OnUpdate(ref SystemState state)
- {
- // SystemAPI.Query - simplest approach
- foreach (var (health, entity) in
- SystemAPI.Query<RefRW<Health>>()
- .WithAll<EnemyTag>()
- .WithEntityAccess())
- {
- if (health.ValueRO.Current <= 0)
- {
- // Mark for destruction
- SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>()
- .CreateCommandBuffer(state.WorldUnmanaged)
- .DestroyEntity(entity);
- }
- }
-
- // Get count
- int enemyCount = _enemyQuery.CalculateEntityCount();
-
- // Get all entities
- var enemies = _enemyQuery.ToEntityArray(Allocator.Temp);
-
- // Get component arrays
- var healths = _enemyQuery.ToComponentDataArray<Health>(Allocator.Temp);
- }
- }
- ```
-
- ### Pattern 4: Entity Command Buffers (Structural Changes)
-
- ```csharp
- // Structural changes (create/destroy/add/remove) require command buffers
- [BurstCompile]
- [UpdateInGroup(typeof(SimulationSystemGroup))]
- public partial struct SpawnSystem : ISystem
- {
- [BurstCompile]
- public void OnUpdate(ref SystemState state)
- {
- var ecbSingleton = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
- var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);
-
- foreach (var (spawner, transform) in
- SystemAPI.Query<RefRW<Spawner>, RefRO<LocalTransform>>())
- {
- spawner.ValueRW.Timer -= SystemAPI.Time.DeltaTime;
-
- if (spawner.ValueRO.Timer <= 0)
- {
- spawner.ValueRW.Timer = spawner.ValueRO.Interval;
-
- // Create entity (deferred until sync point)
- Entity newEntity = ecb.Instantiate(spawner.ValueRO.Prefab);
-
- // Set component values
- ecb.SetComponent(newEntity, new LocalTransform
- {
- Position = transform.ValueRO.Position,
- Rotation = quaternion.identity,
- Scale = 1f
- });
-
- // Add component
- ecb.AddComponent(newEntity, new Speed { Value = 5f });
- }
- }
- }
- }
-
- // Parallel ECB usage
- [BurstCompile]
- public partial struct ParallelSpawnJob : IJobEntity
- {
- public EntityCommandBuffer.ParallelWriter ECB;
-
- void Execute([EntityIndexInQuery] int index, in Spawner spawner)
- {
- Entity e = ECB.Instantiate(index, spawner.Prefab);
- ECB.AddComponent(index, e, new Speed { Value = 5f });
- }
- }
- ```
-
- ### Pattern 5: Aspect (Grouping Components)
-
- ```csharp
- using Unity.Entities;
- using Unity.Transforms;
- using Unity.Mathematics;
-
- // Aspect: Groups related components for cleaner code
- public readonly partial struct CharacterAspect : IAspect
- {
- public readonly Entity Entity;
-
- private readonly RefRW<LocalTransform> _transform;
- private readonly RefRO<Speed> _speed;
- private readonly RefRW<Health> _health;
-
- // Optional component
- [Optional]
- private readonly RefRO<Shield> _shield;
-
- // Buffer
- private readonly DynamicBuffer<InventoryItem> _inventory;
-
- public float3 Position
- {
- get => _transform.ValueRO.Position;
- set => _transform.ValueRW.Position = value;
- }
-
- public float CurrentHealth => _health.ValueRO.Current;
- public float MaxHealth => _health.ValueRO.Max;
- public float MoveSpeed => _speed.ValueRO.Value;
-
- public bool HasShield => _shield.IsValid;
- public float ShieldAmount => HasShield ? _shield.ValueRO.Amount : 0f;
-
- public void TakeDamage(float amount)
- {
- float remaining = amount;
-
- if (HasShield && _shield.ValueRO.Amount > 0)
- {
- // Shield absorbs damage first
- remaining = math.max(0, amount - _shield.ValueRO.Amount);
- }
-
- _health.ValueRW.Current = math.max(0, _health.ValueRO.Current - remaining);
- }
-
- public void Move(float3 direction, float deltaTime)
- {
- _transform.ValueRW.Position += direction * _speed.ValueRO.Value * deltaTime;
- }
-
- public void AddItem(int itemId, int quantity)
- {
- _inventory.Add(new InventoryItem { ItemId = itemId, Quantity = quantity });
- }
- }
-
- // Using aspect in system
- [BurstCompile]
- public partial struct CharacterSystem : ISystem
- {
- [BurstCompile]
- public void OnUpdate(ref SystemState state)
- {
- float dt = SystemAPI.Time.DeltaTime;
-
- foreach (var character in SystemAPI.Query<CharacterAspect>())
- {
- character.Move(new float3(1, 0, 0), dt);
-
- if (character.CurrentHealth < character.MaxHealth * 0.5f)
- {
- // Low health logic
- }
- }
- }
- }
- ```
-
- ### Pattern 6: Singleton Components
-
- ```csharp
- // Singleton: Exactly one entity with this component
- public struct GameConfig : IComponentData
- {
- public float DifficultyMultiplier;
- public int MaxEnemies;
- public float SpawnRate;
- }
-
- public struct GameState : IComponentData
- {
- public int Score;
- public int Wave;
- public float TimeRemaining;
- }
-
- // Create singleton on world creation
- public partial struct GameInitSystem : ISystem
- {
- public void OnCreate(ref SystemState state)
- {
- var entity = state.EntityManager.CreateEntity();
- state.EntityManager.AddComponentData(entity, new GameConfig
- {
- DifficultyMultiplier = 1.0f,
- MaxEnemies = 100,
- SpawnRate = 2.0f
- });
- state.EntityManager.AddComponentData(entity, new GameState
- {
- Score = 0,
- Wave = 1,
- TimeRemaining = 120f
- });
- }
- }
-
- // Access singleton in system
- [BurstCompile]
- public partial struct ScoreSystem : ISystem
- {
- [BurstCompile]
- public void OnUpdate(ref SystemState state)
- {
- // Read singleton
- var config = SystemAPI.GetSingleton<GameConfig>();
-
- // Write singleton
- ref var gameState = ref SystemAPI.GetSingletonRW<GameState>().ValueRW;
- gameState.TimeRemaining -= SystemAPI.Time.DeltaTime;
-
- // Check exists
- if (SystemAPI.HasSingleton<GameConfig>())
- {
- // ...
- }
- }
- }
- ```
-
- ### Pattern 7: Baking (Converting GameObjects)
-
- ```csharp
- using Unity.Entities;
- using UnityEngine;
-
- // Authoring component (MonoBehaviour in Editor)
- public class EnemyAuthoring : MonoBehaviour
- {
- public float Speed = 5f;
- public float Health = 100f;
- public GameObject ProjectilePrefab;
-
- class Baker : Baker<EnemyAuthoring>
- {
- public override void Bake(EnemyAuthoring authoring)
- {
- var entity = GetEntity(TransformUsageFlags.Dynamic);
-
- AddComponent(entity, new Speed { Value = authoring.Speed });
- AddComponent(entity, new Health
- {
- Current = authoring.Health,
- Max = authoring.Health
- });
- AddComponent(entity, new EnemyTag());
-
- if (authoring.ProjectilePrefab != null)
- {
- AddComponent(entity, new ProjectilePrefab
- {
- Value = GetEntity(authoring.ProjectilePrefab, TransformUsageFlags.Dynamic)
- });
- }
- }
- }
- }
-
- // Complex baking with dependencies
- public class SpawnerAuthoring : MonoBehaviour
- {
- public GameObject[] Prefabs;
- public float Interval = 1f;
-
- class Baker : Baker<SpawnerAuthoring>
- {
- public override void Bake(SpawnerAuthoring authoring)
- {
- var entity = GetEntity(TransformUsageFlags.Dynamic);
-
- AddComponent(entity, new Spawner
- {
- Interval = authoring.Interval,
- Timer = 0f
- });
-
- // Bake buffer of prefabs
- var buffer = AddBuffer<SpawnPrefabElement>(entity);
- foreach (var prefab in authoring.Prefabs)
- {
- buffer.Add(new SpawnPrefabElement
- {
- Prefab = GetEntity(prefab, TransformUsageFlags.Dynamic)
- });
- }
-
- // Declare dependencies
- DependsOn(authoring.Prefabs);
- }
- }
- }
- ```
-
- ### Pattern 8: Jobs with Native Collections
-
- ```csharp
- using Unity.Jobs;
- using Unity.Collections;
- using Unity.Burst;
- using Unity.Mathematics;
-
- [BurstCompile]
- public struct SpatialHashJob : IJobParallelFor
- {
- [ReadOnly]
- public NativeArray<float3> Positions;
-
- // Thread-safe write to hash map
- public NativeParallelMultiHashMap<int, int>.ParallelWriter HashMap;
-
- public float CellSize;
-
- public void Execute(int index)
- {
- float3 pos = Positions[index];
- int hash = GetHash(pos);
- HashMap.Add(hash, index);
- }
-
- int GetHash(float3 pos)
- {
- int x = (int)math.floor(pos.x / CellSize);
- int y = (int)math.floor(pos.y / CellSize);
- int z = (int)math.floor(pos.z / CellSize);
- return x * 73856093 ^ y * 19349663 ^ z * 83492791;
- }
- }
-
- [BurstCompile]
- public partial struct SpatialHashSystem : ISystem
- {
- private NativeParallelMultiHashMap<int, int> _hashMap;
-
- public void OnCreate(ref SystemState state)
- {
- _hashMap = new NativeParallelMultiHashMap<int, int>(10000, Allocator.Persistent);
- }
-
- public void OnDestroy(ref SystemState state)
- {
- _hashMap.Dispose();
- }
-
- [BurstCompile]
- public void OnUpdate(ref SystemState state)
- {
- var query = SystemAPI.QueryBuilder()
- .WithAll<LocalTransform>()
- .Build();
-
- int count = query.CalculateEntityCount();
-
- // Resize if needed
- if (_hashMap.Capacity < count)
- {
- _hashMap.Capacity = count * 2;
- }
-
- _hashMap.Clear();
-
- // Get positions
- var positions = query.ToComponentDataArray<LocalTransform>(Allocator.TempJob);
- var posFloat3 = new NativeArray<float3>(count, Allocator.TempJob);
-
- for (int i = 0; i < count; i++)
- {
- posFloat3[i] = positions[i].Position;
- }
-
- // Build hash map
- var hashJob = new SpatialHashJob
- {
- Positions = posFloat3,
- HashMap = _hashMap.AsParallelWriter(),
- CellSize = 10f
- };
-
- state.Dependency = hashJob.Schedule(count, 64, state.Dependency);
-
- // Cleanup
- positions.Dispose(state.Dependency);
- posFloat3.Dispose(state.Dependency);
- }
- }
- ```
-
- ## Performance Tips
-
- ```csharp
- // 1. Use Burst everywhere
- [BurstCompile]
- public partial struct MySystem : ISystem { }
-
- // 2. Prefer IJobEntity over manual iteration
- [BurstCompile]
- partial struct OptimizedJob : IJobEntity
- {
- void Execute(ref LocalTransform transform) { }
- }
-
- // 3. Schedule parallel when possible
- state.Dependency = job.ScheduleParallel(state.Dependency);
-
- // 4. Use ScheduleParallel with chunk iteration
- [BurstCompile]
- partial struct ChunkJob : IJobChunk
- {
- public ComponentTypeHandle<Health> HealthHandle;
-
- public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex,
- bool useEnabledMask, in v128 chunkEnabledMask)
- {
- var healths = chunk.GetNativeArray(ref HealthHandle);
- for (int i = 0; i < chunk.Count; i++)
- {
- // Process
- }
- }
- }
+ ## Detailed patterns and worked examples
- // 5. Avoid structural changes in hot paths
- // Use enableable components instead of add/remove
- public struct Disabled : IComponentData, IEnableableComponent { }
- ```
+ Detailed pattern documentation lives in `references/details.md`. Read that file when the navigation tier above is insufficient.
## Best Practices
### Do's
- **Use ISystem over SystemBase** - Better performance
- **Burst compile everything** - Massive speedup
- **Batch structural changes** - Use ECB
- **Profile with Profiler** - Identify bottlenecks
- **Use Aspects** - Clean component grouping
### Don'ts
- **Don't use managed types** - Breaks Burst
- **Don't structural change in jobs** - Use ECB
- **Don't over-architect** - Start simple
- **Don't ignore chunk utilization** - Group similar entities
- **Don't forget disposal** - Native collections leak