Claude Agent Skill · by Wshobson

Unity Ecs Patterns

This breaks down Unity's DOTS architecture with actual production patterns you'll need when moving from GameObject-based code to ECS. It covers the core shift f

Install
Terminal · npx
$npx skills add https://github.com/wshobson/agents --skill unity-ecs-patterns
Works with Paperclip

How Unity Ecs Patterns fits into a Paperclip company.

Unity Ecs Patterns drops into any Paperclip agent that handles this kind of work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.

S
SaaS FactoryPaired

Pre-configured AI company — 18 agents, 18 skills, one-time purchase.

$27$59
Explore pack
Source file
SKILL.md622 lines
Expand
---name: unity-ecs-patternsdescription: 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 componentsWorld: Container for entitiesArchetype: Unique combination of componentsChunk: Memory block for same-archetype entities``` ## Patterns ### Pattern 1: Basic ECS Setup ```csharpusing Unity.Entities;using Unity.Mathematics;using Unity.Transforms;using Unity.Burst;using Unity.Collections; // Component: Pure data, no methodspublic 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) ```csharpusing 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) ```csharpusing Unity.Entities;using Unity.Transforms;using Unity.Mathematics; // Aspect: Groups related components for cleaner codepublic 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 componentpublic 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 creationpublic 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) ```csharpusing 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 dependenciespublic 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 ```csharpusing 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 possiblestate.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/removepublic 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