Smithery Logo
MCPsSkillsDocsPricing
Login
Smithery Logo

Accelerating the Agent Economy

Resources

DocumentationPrivacy PolicySystem Status

Company

PricingAboutBlog

Connect

© 2026 Smithery. All rights reserved.

    ericgrill

    unity-ecs-patterns

    ericgrill/unity-ecs-patterns
    Coding
    5

    About

    SKILL.md

    Install

    Install via Skills CLI

    or add to your agent
    • Claude Code
      Claude Code
    • Codex
      Codex
    • OpenClaw
      OpenClaw
    • Cursor
      Cursor
    • Amp
      Amp
    • GitHub Copilot
      GitHub Copilot
    • Gemini CLI
      Gemini CLI
    • Kilo Code
      Kilo Code
    • Junie
      Junie
    • Replit
      Replit
    • Windsurf
      Windsurf
    • Cline
      Cline
    • Continue
      Continue
    • OpenCode
      OpenCode
    • OpenHands
      OpenHands
    • Roo Code
      Roo Code
    • Augment
      Augment
    • Goose
      Goose
    • Trae
      Trae
    • Zencoder
      Zencoder
    • Antigravity
      Antigravity
    ├─
    ├─
    └─

    About

    Master Unity ECS (Entity Component System) with DOTS, Jobs, and Burst for high-performance game development.

    SKILL.md

    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

    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)

    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

    [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)

    // 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)

    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

    // 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)

    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

    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

    // 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
    Recommended Servers
    Vercel Grep
    Vercel Grep
    Repository
    ericgrill/agents-skills-plugins
    Files