名称:unity-ecs-patterns 描述:掌握Unity ECS(实体组件系统)与DOTS、作业系统和Burst编译器,用于高性能游戏开发。适用于构建数据导向游戏、优化性能或处理大量实体。
Unity ECS 模式
生产级模式,用于Unity的数据导向技术栈(DOTS),包括实体组件系统、作业系统和Burst编译器。
何时使用此技能
- 构建高性能Unity游戏
- 高效管理数千个实体
- 实现数据导向游戏系统
- 优化CPU密集型游戏逻辑
- 将面向对象游戏代码转换为ECS
- 使用作业和Burst进行并行化
核心概念
1. ECS 与 OOP
| 方面 | 传统OOP | ECS/DOTS |
|---|---|---|
| 数据布局 | 面向对象 | 数据导向 |
| 内存 | 分散 | 连续 |
| 处理 | 每个对象 | 批处理 |
| 扩展性 | 数量增加时较差 | 线性扩展 |
| 最适用于 | 复杂行为 | 大规模模拟 |
2. DOTS 组件
实体:轻量级ID(无数据)
组件:纯数据(无行为)
系统:处理组件的逻辑
世界:实体的容器
原型:组件的唯一组合
块:相同原型实体的内存块
模式
模式 1: 基本ECS设置
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using Unity.Burst;
using Unity.Collections;
// 组件:纯数据,无方法
public struct Speed : IComponentData
{
public float Value;
}
public struct Health : IComponentData
{
public float Current;
public float Max;
}
public struct Target : IComponentData
{
public Entity Value;
}
// 标签组件(零大小标记)
public struct EnemyTag : IComponentData { }
public struct PlayerTag : IComponentData { }
// 缓冲区组件(可变大小数组)
[InternalBufferCapacity(8)]
public struct InventoryItem : IBufferElementData
{
public int ItemId;
public int Quantity;
}
// 共享组件(分组实体)
public struct TeamId : ISharedComponentData
{
public int Value;
}
模式 2: 使用 ISystem 的系统(推荐)
using Unity.Entities;
using Unity.Transforms;
using Unity.Mathematics;
using Unity.Burst;
// ISystem:非托管、Burst兼容、最高性能
[BurstCompile]
public partial struct MovementSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
// 在系统运行前要求组件
state.RequireForUpdate<Speed>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
float deltaTime = SystemAPI.Time.DeltaTime;
// 简单的foreach - 自动生成作业
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) { }
}
// 用于更多控制的显式作业
[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);
}
}
模式 3: 实体查询
[BurstCompile]
public partial struct QueryExamplesSystem : ISystem
{
private EntityQuery _enemyQuery;
public void OnCreate(ref SystemState state)
{
// 为复杂情况手动构建查询
_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 - 最简单的方法
foreach (var (health, entity) in
SystemAPI.Query<RefRW<Health>>()
.WithAll<EnemyTag>()
.WithEntityAccess())
{
if (health.ValueRO.Current <= 0)
{
// 标记销毁
SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>()
.CreateCommandBuffer(state.WorldUnmanaged)
.DestroyEntity(entity);
}
}
// 获取计数
int enemyCount = _enemyQuery.CalculateEntityCount();
// 获取所有实体
var enemies = _enemyQuery.ToEntityArray(Allocator.Temp);
// 获取组件数组
var healths = _enemyQuery.ToComponentDataArray<Health>(Allocator.Temp);
}
}
模式 4: 实体命令缓冲区(结构变化)
// 结构变化(创建/销毁/添加/移除)需要命令缓冲区
[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;
// 创建实体(延迟到同步点)
Entity newEntity = ecb.Instantiate(spawner.ValueRO.Prefab);
// 设置组件值
ecb.SetComponent(newEntity, new LocalTransform
{
Position = transform.ValueRO.Position,
Rotation = quaternion.identity,
Scale = 1f
});
// 添加组件
ecb.AddComponent(newEntity, new Speed { Value = 5f });
}
}
}
}
// 并行ECB使用
[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 });
}
}
模式 5: Aspect(分组组件)
using Unity.Entities;
using Unity.Transforms;
using Unity.Mathematics;
// Aspect:分组相关组件以简化代码
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]
private readonly RefRO<Shield> _shield;
// 缓冲区
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)
{
// 护盾先吸收伤害
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 });
}
}
// 在系统中使用aspect
[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)
{
// 低血量逻辑
}
}
}
}
模式 6: 单例组件
// 单例:仅有一个实体有此组件
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;
}
// 在世界创建时创建单例
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
});
}
}
// 在系统中访问单例
[BurstCompile]
public partial struct ScoreSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
// 读取单例
var config = SystemAPI.GetSingleton<GameConfig>();
// 写入单例
ref var gameState = ref SystemAPI.GetSingletonRW<GameState>().ValueRW;
gameState.TimeRemaining -= SystemAPI.Time.DeltaTime;
// 检查存在
if (SystemAPI.HasSingleton<GameConfig>())
{
// ...
}
}
}
模式 7: 烘焙(转换GameObjects)
using Unity.Entities;
using UnityEngine;
// 创作组件(编辑器中的MonoBehaviour)
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)
});
}
}
}
}
// 带依赖的复杂烘焙
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
});
// 烘焙预制体缓冲区
var buffer = AddBuffer<SpawnPrefabElement>(entity);
foreach (var prefab in authoring.Prefabs)
{
buffer.Add(new SpawnPrefabElement
{
Prefab = GetEntity(prefab, TransformUsageFlags.Dynamic)
});
}
// 声明依赖
DependsOn(authoring.Prefabs);
}
}
}
模式 8: 作业与本地集合
using Unity.Jobs;
using Unity.Collections;
using Unity.Burst;
using Unity.Mathematics;
[BurstCompile]
public struct SpatialHashJob : IJobParallelFor
{
[ReadOnly]
public NativeArray<float3> Positions;
// 线程安全写入哈希映射
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();
// 如果需要,调整大小
if (_hashMap.Capacity < count)
{
_hashMap.Capacity = count * 2;
}
_hashMap.Clear();
// 获取位置
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;
}
// 构建哈希映射
var hashJob = new SpatialHashJob
{
Positions = posFloat3,
HashMap = _hashMap.AsParallelWriter(),
CellSize = 10f
};
state.Dependency = hashJob.Schedule(count, 64, state.Dependency);
// 清理
positions.Dispose(state.Dependency);
posFloat3.Dispose(state.Dependency);
}
}
性能技巧
// 1. 处处使用Burst
[BurstCompile]
public partial struct MySystem : ISystem { }
// 2. 优先使用IJobEntity而非手动迭代
[BurstCompile]
partial struct OptimizedJob : IJobEntity
{
void Execute(ref LocalTransform transform) { }
}
// 3. 尽可能调度并行
state.Dependency = job.ScheduleParallel(state.Dependency);
// 4. 使用块迭代进行并行调度
[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++)
{
// 处理
}
}
}
// 5. 避免在热路径进行结构变化
// 使用可启用组件替代添加/移除
public struct Disabled : IComponentData, IEnableableComponent { }
最佳实践
应该做的
- 使用ISystem而非SystemBase - 更好的性能
- Burst编译所有内容 - 大幅加速
- 批处理结构变化 - 使用ECB
- 使用Profiler分析 - 识别瓶颈
- 使用Aspect - 清晰组件分组
不应该做的
- 不要使用托管类型 - 破坏Burst
- 不要在作业中结构变化 - 使用ECB
- 不要过度设计 - 从简单开始
- 不要忽略块利用率 - 分组相似实体
- 不要忘记处置 - 本地集合泄漏