Agent Skills
Discover and share powerful Agent Skills for AI assistants
unity-ecs-patterns - Agent Skill - Agent Skills
Home/ Skills / unity-ecs-patterns 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.
Use the skills CLI to install this skill with one command. Auto-detects all installed AI assistants.
Method 1 - skills CLI
npx skills i wshobson/agents/plugins/game-development/skills/unity-ecs-patterns CopyMethod 2 - openskills (supports sync & update)
npx openskills install wshobson/agents CopyAuto-detects Claude Code, Cursor, Codex CLI, Gemini CLI, and more. One install, works everywhere.
Installation Path
Download and extract to one of the following locations:
Claude Code Cursor OpenCode Gemini CLI Codex CLI
~/.claude/skills/unity-ecs-patterns/ Back No setup needed. Let our cloud agents run this skill for you.
Select Model
Claude Haiku 4.5 $0.10 Claude Sonnet 4.5 $0.20 Claude Opus 4.5 $0.50 Claude Sonnet 4.5 $0.20 /task
Best for coding tasks
Try NowNo setup required
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
Pattern 2: Systems with ISystem (Recommended)
using Unity . Entities ;
using Unity . Transforms ;
using Unity . Mathematics ;
using Unity . Burst
Pattern 3: Entity Queries
[ BurstCompile ]
public partial struct QueryExamplesSystem : ISystem
{
private EntityQuery _enemyQuery ;
public void OnCreate ( ref
Pattern 4: Entity Command Buffers (Structural Changes)
// Structural changes (create/destroy/add/remove) require command buffers
[ BurstCompile ]
[ UpdateInGroup ( typeof ( SimulationSystemGroup ))]
public partial struct SpawnSystem
Pattern 5: Aspect (Grouping Components)
using Unity . Entities ;
using Unity . Transforms ;
Pattern 6: Singleton Components
// Singleton: Exactly one entity with this component
public struct GameConfig : IComponentData
{
public float DifficultyMultiplier ;
public int MaxEnemies ;
public float
Pattern 7: Baking (Converting GameObjects)
using Unity . Entities ;
using UnityEngine ;
// Authoring component (MonoBehaviour in Editor)
public class
Pattern 8: Jobs with Native Collections
using Unity . Jobs ;
using Unity .
// 1. Use Burst everywhere
[ BurstCompile ]
public partial struct MySystem : ISystem { }
// 2. Prefer IJobEntity over manual iteration
[ BurstCompile ]
partial struct OptimizedJob : IJobEntity
{
void Execute
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
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 ;
}
;
// 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);
}
}
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);
}
}
:
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 });
}
}
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
}
}
}
}
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 >())
{
// ...
}
}
}
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);
}
}
}
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);
}
}
(
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 { }