Unity Ecs Patterns

Build blazing-fast Unity games with data-oriented ECS architecture

✨ The solution you've been looking for

Verified
Tested and verified by our team
25450 Stars

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 dots performance game-engine burst jobs parallelization
Repository

See It In Action

Interactive preview & real-world examples

Live Demo
Skill Demo Animation

AI Conversation Simulator

See how users interact with this skill

User Prompt

I need to optimize my Unity game that has 10,000+ moving enemies. The frame rate is dropping badly with traditional GameObjects. Help me convert to ECS.

Skill Processing

Analyzing request...

Agent Response

Complete ECS implementation with systems for movement, health, and spawning that can handle thousands of entities at 60+ FPS

Quick Start (3 Steps)

Get up and running in minutes

1

Install

claude-code skill install unity-ecs-patterns

claude-code skill install unity-ecs-patterns
2

Config

3

First Trigger

@unity-ecs-patterns help

Commands

CommandDescriptionRequired Args
@unity-ecs-patterns high-performance-entity-managementEfficiently manage thousands of game entities using Unity's ECS architectureNone
@unity-ecs-patterns data-oriented-system-designConvert object-oriented game logic to performant ECS patternsNone
@unity-ecs-patterns parallel-job-implementationImplement CPU-intensive game logic using Unity's Job System and Burst compilerNone

Typical Use Cases

High-Performance Entity Management

Efficiently manage thousands of game entities using Unity's ECS architecture

Data-Oriented System Design

Convert object-oriented game logic to performant ECS patterns

Parallel Job Implementation

Implement CPU-intensive game logic using Unity's Job System and Burst compiler

Overview

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

AspectTraditional OOPECS/DOTS
Data layoutObject-orientedData-oriented
MemoryScatteredContiguous
ProcessingPer-objectBatched
ScalingPoor with countLinear scaling
Best forComplex behaviorsMass 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

 1using Unity.Entities;
 2using Unity.Mathematics;
 3using Unity.Transforms;
 4using Unity.Burst;
 5using Unity.Collections;
 6
 7// Component: Pure data, no methods
 8public struct Speed : IComponentData
 9{
10    public float Value;
11}
12
13public struct Health : IComponentData
14{
15    public float Current;
16    public float Max;
17}
18
19public struct Target : IComponentData
20{
21    public Entity Value;
22}
23
24// Tag component (zero-size marker)
25public struct EnemyTag : IComponentData { }
26public struct PlayerTag : IComponentData { }
27
28// Buffer component (variable-size array)
29[InternalBufferCapacity(8)]
30public struct InventoryItem : IBufferElementData
31{
32    public int ItemId;
33    public int Quantity;
34}
35
36// Shared component (grouped entities)
37public struct TeamId : ISharedComponentData
38{
39    public int Value;
40}
 1using Unity.Entities;
 2using Unity.Transforms;
 3using Unity.Mathematics;
 4using Unity.Burst;
 5
 6// ISystem: Unmanaged, Burst-compatible, highest performance
 7[BurstCompile]
 8public partial struct MovementSystem : ISystem
 9{
10    [BurstCompile]
11    public void OnCreate(ref SystemState state)
12    {
13        // Require components before system runs
14        state.RequireForUpdate<Speed>();
15    }
16
17    [BurstCompile]
18    public void OnUpdate(ref SystemState state)
19    {
20        float deltaTime = SystemAPI.Time.DeltaTime;
21
22        // Simple foreach - auto-generates job
23        foreach (var (transform, speed) in
24            SystemAPI.Query<RefRW<LocalTransform>, RefRO<Speed>>())
25        {
26            transform.ValueRW.Position +=
27                new float3(0, 0, speed.ValueRO.Value * deltaTime);
28        }
29    }
30
31    [BurstCompile]
32    public void OnDestroy(ref SystemState state) { }
33}
34
35// With explicit job for more control
36[BurstCompile]
37public partial struct MovementJobSystem : ISystem
38{
39    [BurstCompile]
40    public void OnUpdate(ref SystemState state)
41    {
42        var job = new MoveJob
43        {
44            DeltaTime = SystemAPI.Time.DeltaTime
45        };
46
47        state.Dependency = job.ScheduleParallel(state.Dependency);
48    }
49}
50
51[BurstCompile]
52public partial struct MoveJob : IJobEntity
53{
54    public float DeltaTime;
55
56    void Execute(ref LocalTransform transform, in Speed speed)
57    {
58        transform.Position += new float3(0, 0, speed.Value * DeltaTime);
59    }
60}

Pattern 3: Entity Queries

 1[BurstCompile]
 2public partial struct QueryExamplesSystem : ISystem
 3{
 4    private EntityQuery _enemyQuery;
 5
 6    public void OnCreate(ref SystemState state)
 7    {
 8        // Build query manually for complex cases
 9        _enemyQuery = new EntityQueryBuilder(Allocator.Temp)
10            .WithAll<EnemyTag, Health, LocalTransform>()
11            .WithNone<Dead>()
12            .WithOptions(EntityQueryOptions.FilterWriteGroup)
13            .Build(ref state);
14    }
15
16    [BurstCompile]
17    public void OnUpdate(ref SystemState state)
18    {
19        // SystemAPI.Query - simplest approach
20        foreach (var (health, entity) in
21            SystemAPI.Query<RefRW<Health>>()
22                .WithAll<EnemyTag>()
23                .WithEntityAccess())
24        {
25            if (health.ValueRO.Current <= 0)
26            {
27                // Mark for destruction
28                SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>()
29                    .CreateCommandBuffer(state.WorldUnmanaged)
30                    .DestroyEntity(entity);
31            }
32        }
33
34        // Get count
35        int enemyCount = _enemyQuery.CalculateEntityCount();
36
37        // Get all entities
38        var enemies = _enemyQuery.ToEntityArray(Allocator.Temp);
39
40        // Get component arrays
41        var healths = _enemyQuery.ToComponentDataArray<Health>(Allocator.Temp);
42    }
43}

Pattern 4: Entity Command Buffers (Structural Changes)

 1// Structural changes (create/destroy/add/remove) require command buffers
 2[BurstCompile]
 3[UpdateInGroup(typeof(SimulationSystemGroup))]
 4public partial struct SpawnSystem : ISystem
 5{
 6    [BurstCompile]
 7    public void OnUpdate(ref SystemState state)
 8    {
 9        var ecbSingleton = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
10        var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);
11
12        foreach (var (spawner, transform) in
13            SystemAPI.Query<RefRW<Spawner>, RefRO<LocalTransform>>())
14        {
15            spawner.ValueRW.Timer -= SystemAPI.Time.DeltaTime;
16
17            if (spawner.ValueRO.Timer <= 0)
18            {
19                spawner.ValueRW.Timer = spawner.ValueRO.Interval;
20
21                // Create entity (deferred until sync point)
22                Entity newEntity = ecb.Instantiate(spawner.ValueRO.Prefab);
23
24                // Set component values
25                ecb.SetComponent(newEntity, new LocalTransform
26                {
27                    Position = transform.ValueRO.Position,
28                    Rotation = quaternion.identity,
29                    Scale = 1f
30                });
31
32                // Add component
33                ecb.AddComponent(newEntity, new Speed { Value = 5f });
34            }
35        }
36    }
37}
38
39// Parallel ECB usage
40[BurstCompile]
41public partial struct ParallelSpawnJob : IJobEntity
42{
43    public EntityCommandBuffer.ParallelWriter ECB;
44
45    void Execute([EntityIndexInQuery] int index, in Spawner spawner)
46    {
47        Entity e = ECB.Instantiate(index, spawner.Prefab);
48        ECB.AddComponent(index, e, new Speed { Value = 5f });
49    }
50}

Pattern 5: Aspect (Grouping Components)

 1using Unity.Entities;
 2using Unity.Transforms;
 3using Unity.Mathematics;
 4
 5// Aspect: Groups related components for cleaner code
 6public readonly partial struct CharacterAspect : IAspect
 7{
 8    public readonly Entity Entity;
 9
10    private readonly RefRW<LocalTransform> _transform;
11    private readonly RefRO<Speed> _speed;
12    private readonly RefRW<Health> _health;
13
14    // Optional component
15    [Optional]
16    private readonly RefRO<Shield> _shield;
17
18    // Buffer
19    private readonly DynamicBuffer<InventoryItem> _inventory;
20
21    public float3 Position
22    {
23        get => _transform.ValueRO.Position;
24        set => _transform.ValueRW.Position = value;
25    }
26
27    public float CurrentHealth => _health.ValueRO.Current;
28    public float MaxHealth => _health.ValueRO.Max;
29    public float MoveSpeed => _speed.ValueRO.Value;
30
31    public bool HasShield => _shield.IsValid;
32    public float ShieldAmount => HasShield ? _shield.ValueRO.Amount : 0f;
33
34    public void TakeDamage(float amount)
35    {
36        float remaining = amount;
37
38        if (HasShield && _shield.ValueRO.Amount > 0)
39        {
40            // Shield absorbs damage first
41            remaining = math.max(0, amount - _shield.ValueRO.Amount);
42        }
43
44        _health.ValueRW.Current = math.max(0, _health.ValueRO.Current - remaining);
45    }
46
47    public void Move(float3 direction, float deltaTime)
48    {
49        _transform.ValueRW.Position += direction * _speed.ValueRO.Value * deltaTime;
50    }
51
52    public void AddItem(int itemId, int quantity)
53    {
54        _inventory.Add(new InventoryItem { ItemId = itemId, Quantity = quantity });
55    }
56}
57
58// Using aspect in system
59[BurstCompile]
60public partial struct CharacterSystem : ISystem
61{
62    [BurstCompile]
63    public void OnUpdate(ref SystemState state)
64    {
65        float dt = SystemAPI.Time.DeltaTime;
66
67        foreach (var character in SystemAPI.Query<CharacterAspect>())
68        {
69            character.Move(new float3(1, 0, 0), dt);
70
71            if (character.CurrentHealth < character.MaxHealth * 0.5f)
72            {
73                // Low health logic
74            }
75        }
76    }
77}

Pattern 6: Singleton Components

 1// Singleton: Exactly one entity with this component
 2public struct GameConfig : IComponentData
 3{
 4    public float DifficultyMultiplier;
 5    public int MaxEnemies;
 6    public float SpawnRate;
 7}
 8
 9public struct GameState : IComponentData
10{
11    public int Score;
12    public int Wave;
13    public float TimeRemaining;
14}
15
16// Create singleton on world creation
17public partial struct GameInitSystem : ISystem
18{
19    public void OnCreate(ref SystemState state)
20    {
21        var entity = state.EntityManager.CreateEntity();
22        state.EntityManager.AddComponentData(entity, new GameConfig
23        {
24            DifficultyMultiplier = 1.0f,
25            MaxEnemies = 100,
26            SpawnRate = 2.0f
27        });
28        state.EntityManager.AddComponentData(entity, new GameState
29        {
30            Score = 0,
31            Wave = 1,
32            TimeRemaining = 120f
33        });
34    }
35}
36
37// Access singleton in system
38[BurstCompile]
39public partial struct ScoreSystem : ISystem
40{
41    [BurstCompile]
42    public void OnUpdate(ref SystemState state)
43    {
44        // Read singleton
45        var config = SystemAPI.GetSingleton<GameConfig>();
46
47        // Write singleton
48        ref var gameState = ref SystemAPI.GetSingletonRW<GameState>().ValueRW;
49        gameState.TimeRemaining -= SystemAPI.Time.DeltaTime;
50
51        // Check exists
52        if (SystemAPI.HasSingleton<GameConfig>())
53        {
54            // ...
55        }
56    }
57}

Pattern 7: Baking (Converting GameObjects)

 1using Unity.Entities;
 2using UnityEngine;
 3
 4// Authoring component (MonoBehaviour in Editor)
 5public class EnemyAuthoring : MonoBehaviour
 6{
 7    public float Speed = 5f;
 8    public float Health = 100f;
 9    public GameObject ProjectilePrefab;
10
11    class Baker : Baker<EnemyAuthoring>
12    {
13        public override void Bake(EnemyAuthoring authoring)
14        {
15            var entity = GetEntity(TransformUsageFlags.Dynamic);
16
17            AddComponent(entity, new Speed { Value = authoring.Speed });
18            AddComponent(entity, new Health
19            {
20                Current = authoring.Health,
21                Max = authoring.Health
22            });
23            AddComponent(entity, new EnemyTag());
24
25            if (authoring.ProjectilePrefab != null)
26            {
27                AddComponent(entity, new ProjectilePrefab
28                {
29                    Value = GetEntity(authoring.ProjectilePrefab, TransformUsageFlags.Dynamic)
30                });
31            }
32        }
33    }
34}
35
36// Complex baking with dependencies
37public class SpawnerAuthoring : MonoBehaviour
38{
39    public GameObject[] Prefabs;
40    public float Interval = 1f;
41
42    class Baker : Baker<SpawnerAuthoring>
43    {
44        public override void Bake(SpawnerAuthoring authoring)
45        {
46            var entity = GetEntity(TransformUsageFlags.Dynamic);
47
48            AddComponent(entity, new Spawner
49            {
50                Interval = authoring.Interval,
51                Timer = 0f
52            });
53
54            // Bake buffer of prefabs
55            var buffer = AddBuffer<SpawnPrefabElement>(entity);
56            foreach (var prefab in authoring.Prefabs)
57            {
58                buffer.Add(new SpawnPrefabElement
59                {
60                    Prefab = GetEntity(prefab, TransformUsageFlags.Dynamic)
61                });
62            }
63
64            // Declare dependencies
65            DependsOn(authoring.Prefabs);
66        }
67    }
68}

Pattern 8: Jobs with Native Collections

 1using Unity.Jobs;
 2using Unity.Collections;
 3using Unity.Burst;
 4using Unity.Mathematics;
 5
 6[BurstCompile]
 7public struct SpatialHashJob : IJobParallelFor
 8{
 9    [ReadOnly]
10    public NativeArray<float3> Positions;
11
12    // Thread-safe write to hash map
13    public NativeParallelMultiHashMap<int, int>.ParallelWriter HashMap;
14
15    public float CellSize;
16
17    public void Execute(int index)
18    {
19        float3 pos = Positions[index];
20        int hash = GetHash(pos);
21        HashMap.Add(hash, index);
22    }
23
24    int GetHash(float3 pos)
25    {
26        int x = (int)math.floor(pos.x / CellSize);
27        int y = (int)math.floor(pos.y / CellSize);
28        int z = (int)math.floor(pos.z / CellSize);
29        return x * 73856093 ^ y * 19349663 ^ z * 83492791;
30    }
31}
32
33[BurstCompile]
34public partial struct SpatialHashSystem : ISystem
35{
36    private NativeParallelMultiHashMap<int, int> _hashMap;
37
38    public void OnCreate(ref SystemState state)
39    {
40        _hashMap = new NativeParallelMultiHashMap<int, int>(10000, Allocator.Persistent);
41    }
42
43    public void OnDestroy(ref SystemState state)
44    {
45        _hashMap.Dispose();
46    }
47
48    [BurstCompile]
49    public void OnUpdate(ref SystemState state)
50    {
51        var query = SystemAPI.QueryBuilder()
52            .WithAll<LocalTransform>()
53            .Build();
54
55        int count = query.CalculateEntityCount();
56
57        // Resize if needed
58        if (_hashMap.Capacity < count)
59        {
60            _hashMap.Capacity = count * 2;
61        }
62
63        _hashMap.Clear();
64
65        // Get positions
66        var positions = query.ToComponentDataArray<LocalTransform>(Allocator.TempJob);
67        var posFloat3 = new NativeArray<float3>(count, Allocator.TempJob);
68
69        for (int i = 0; i < count; i++)
70        {
71            posFloat3[i] = positions[i].Position;
72        }
73
74        // Build hash map
75        var hashJob = new SpatialHashJob
76        {
77            Positions = posFloat3,
78            HashMap = _hashMap.AsParallelWriter(),
79            CellSize = 10f
80        };
81
82        state.Dependency = hashJob.Schedule(count, 64, state.Dependency);
83
84        // Cleanup
85        positions.Dispose(state.Dependency);
86        posFloat3.Dispose(state.Dependency);
87    }
88}

Performance Tips

 1// 1. Use Burst everywhere
 2[BurstCompile]
 3public partial struct MySystem : ISystem { }
 4
 5// 2. Prefer IJobEntity over manual iteration
 6[BurstCompile]
 7partial struct OptimizedJob : IJobEntity
 8{
 9    void Execute(ref LocalTransform transform) { }
10}
11
12// 3. Schedule parallel when possible
13state.Dependency = job.ScheduleParallel(state.Dependency);
14
15// 4. Use ScheduleParallel with chunk iteration
16[BurstCompile]
17partial struct ChunkJob : IJobChunk
18{
19    public ComponentTypeHandle<Health> HealthHandle;
20
21    public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex,
22        bool useEnabledMask, in v128 chunkEnabledMask)
23    {
24        var healths = chunk.GetNativeArray(ref HealthHandle);
25        for (int i = 0; i < chunk.Count; i++)
26        {
27            // Process
28        }
29    }
30}
31
32// 5. Avoid structural changes in hot paths
33// Use enableable components instead of add/remove
34public 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

What Users Are Saying

Real feedback from the community

Environment Matrix

Dependencies

Unity 2022.3 LTS+
com.unity.entities 1.0+
com.unity.burst 1.8+
com.unity.collections 2.1+
com.unity.mathematics 1.2+

Framework Support

Unity DOTS ✓ (recommended) Unity Job System ✓ Burst Compiler ✓

Context Window

Token Usage ~5K-15K tokens for complex ECS architectures

Security & Privacy

Information

Author
wshobson
Updated
2026-01-30
Category
architecture-patterns