unity-ecs-patterns

安装量: 3.3K
排名: #661

安装

npx skills add https://github.com/wshobson/agents --skill unity-ecs-patterns

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

返回排行榜