Back to Architecture & Design Patterns

unity-ecs-patterns

UnityECSDOTSGame DevelopmentPerformanceC#Job SystemBurst Compiler
36.8k📄 MIT🕒 2026-06-16Source ↗

Install this skill

npx skills add wshobson/agents

Works across Claude Code, Cursor, Codex, Copilot & Antigravity

Unity ECS Patterns provides an architectural framework for implementing the Data-Oriented Technology Stack. By decoupling data from logic, this approach moves away from traditional monobehaviour-based object hierarchy toward linear memory arrays. The primary goal involves cache-friendly execution where systems iterate over packed components using the Job System and Burst Compiler for hardware-level parallelism. This skill focuses on translating entity management, structural component changes, and query filtering into performant C# code suitable for high-frequency simulations. It emphasizes the use of ISystem for modern unmanaged logic, buffer components for collection data, and command buffers to defer structural updates that would otherwise invalidate component iteration. Adopting these patterns allows for scaling simulations into thousands of active entities while maintaining deterministic performance across diverse CPU architectures.

When to Use This Skill

  • Simulating large crowds or particle-like entity behaviors
  • Refactoring CPU-heavy logic from standard MonoBehaviours
  • Building systems requiring deterministic, frame-stable updates
  • Processing complex sensor or AI data across many agents

How to Invoke This Skill

Example prompts that trigger this skill in Claude Code, Cursor, or Antigravity:

  • How do I structure my movement logic in Unity ECS?
  • Show me how to filter entities using a query in DOTS
  • Explain the difference between SystemAPI and IJobEntity
  • How to manage structural changes with Entity Command Buffers
  • Create a clean ISystem pattern for my project

Pro Tips

  • 💡Always profile your ECS systems to identify bottlenecks; often, optimizing data access patterns yields the biggest gains.
  • 💡Start with a 'pure ECS' mindset. Try to avoid mixing OOP and ECS excessively in critical performance paths to maximize DOTS benefits.
  • 💡Leverage IJobChunk or IJobEntityBatch for processing large datasets in parallel, ensuring your systems scale effectively across multiple cores.

What this skill does

  • Implementation of ISystem and IJobEntity for parallel processing
  • Optimization of memory layout using Archetypes and Chunks
  • Management of structural changes via Entity Command Buffers
  • Efficient querying of entity sets using EntityQueryBuilder
  • Creation of high-performance custom data components and tags

When not to use it

  • Simple UI-driven scenes with minimal entity counts
  • Prototypes where rapid iteration is prioritized over raw performance
  • Projects requiring high integration with existing third-party OOP middleware

Example workflow

  1. Define pure data structs inheriting from IComponentData
  2. Create an ISystem that queries the required component types
  3. Write the logic within the OnUpdate method using SystemAPI
  4. Batch process data using IJobEntity to enable Burst compilation
  5. Queue structural changes like entity spawning via CommandBuffers

Prerequisites

  • Unity 2022.3+ or 6000+
  • Entities package installed via Package Manager
  • Fundamental knowledge of C# memory management

Pitfalls & limitations

  • !Modifying entity structures directly inside a foreach loop causes exceptions
  • !Overusing shared components leads to fragmented memory archetypes
  • !Forgetting to mark jobs with [BurstCompile] negates performance gains

FAQ

Why should I use ISystem instead of SystemBase?
ISystem is unmanaged and offers better performance by avoiding heap allocations, making it more compatible with Burst compilation and low-level optimization.
Can I use MonoBehaviours alongside ECS?
Yes, but they cannot directly access ECS data stores; you must use Baker scripts to convert GameObject data into Entities.
What is an Entity Command Buffer?
It is a temporary storage for structural changes, like adding or removing components, that are applied during safe synchronization points in the frame.
How does memory layout affect performance?
Contiguous memory layout allows the CPU cache to pre-fetch component data, drastically reducing latency compared to scattered object-based heap access.

How it compares

While manual implementation of DOTS code is prone to runtime errors and race conditions, this skill provides standardized templates that enforce memory safety and cache-efficient iteration.

Source & trust

37k stars📄 MIT🕒 Updated 2026-06-16
📄 Full skill instructions — original source: wshobson/agents
# 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

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

How to Use This Skill Unit

Option A: Project-Specific (Recommended)

  1. Click "Download" above
  2. In your project, create the directory: .agent/skills/unity-ecs-patterns/
  3. Save the file as SKILL.md
  4. The agent will automatically discover the skill based on its description.

Option B: Global Installation (All Agents)

Save the file to these locations to make it available across all projects:

  • Claude Code: ~/.claude/skills/wshobson/agents/unity-ecs-patterns/SKILL.md
  • Cursor: ~/.cursor/skills/wshobson/agents/unity-ecs-patterns/SKILL.md
  • Antigravity: ~/.gemini/antigravity/skills/wshobson/agents/unity-ecs-patterns/SKILL.md

🚀 Install with CLI:
npx skills add wshobson/agents

Read the Master Guide: Mastering Agent Skills

Recommended Rules

View more rules

Recommended Workflows

View more workflows

Recommended MCP Servers

View more MCP servers

Take It Further

Maximize your productivity with these powerful resources

📋

Define Your Standards

Set up coding standards to ensure this workflow produces consistent, high-quality results.

Browse Rules Library
📖

Master Workflows

Learn how to create custom workflows, use Turbo Mode, and build your automation library.

Complete Guide

How to use this Skill in Claude Code & Cursor

For Claude Code (CLI)

To use this skill in Claude Code, copy the rule content into your project's custom instructions or follow our Add-Skill CLI guide. This ensures Claude follows your standards during every code generation.

For Cursor & Windsurf

For Cursor or Windsurf, individual skills are best used in the "Rules for AI" section. This specific unit helps the agent avoid architecture & design patterns issues, leading to cleaner, more efficient code.

Why the skill format matters: the standardized Agent Skills format lets your AI agent load detailed instructions only when they are relevant, keeping your prompt clean while improving results.

Source & attribution

This skill is categorized under Architecture & Design Patterns and is published by W. Shobson, maintained in wshobson/agents.

← Browse All Agent Skills
Sponsored AI assistant. Recommendations may be paid.