mirror of
https://github.com/wshobson/agents.git
synced 2026-03-18 09:37:15 +00:00
629 lines
16 KiB
Markdown
629 lines
16 KiB
Markdown
---
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
// 5. Avoid structural changes in hot paths
|
|
// Use enableable components instead of add/remove
|
|
public struct Disabled : IComponentData, IEnableableComponent { }
|
|
```
|
|
|
|
## 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
|
|
|
|
## Resources
|
|
|
|
- [Unity DOTS Documentation](https://docs.unity3d.com/Packages/com.unity.entities@latest)
|
|
- [Unity DOTS Samples](https://github.com/Unity-Technologies/EntityComponentSystemSamples)
|
|
- [Burst User Guide](https://docs.unity3d.com/Packages/com.unity.burst@latest)
|