关于 unity-ecs-dots
面向 Unity 6.3 LTS 开发者的 ECS/DOTS 技能资料,涵盖实体、组件、系统、作业与 Burst 编译。重点提供烘焙、并行处理和常见架构问题的实践指导。
主要功能
- 01ECS 核心概念与组件类型
- 02ISystem 与 SystemAPI 用法
- 03烘焙和 SubScene 工作流
- 04作业调度与并行处理
- 05Burst 编译及限制
- 06实体命令缓冲区模式
适用场景
- 01将 GameObject 逻辑改写为 ECS 系统
- 02在 SubScene 中配置实体烘焙
- 03通过 Jobs 并行处理实体
- 04排查结构变更与 Burst 限制
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ nice-wolf-studio/ unity-claude-skills --skill unity-ecs-dots安装后在 Claude Code 中输入/unity-ecs-dots确认已加载。
附带文件references/ecs-api.mdreferences/jobs-burst.md
详细介绍
Unity ECS / DOTS(面向数据的技术栈)
基于 Unity 6.3 LTS(6000.3)——Entities 1.3、Burst 1.8、Jobs 包
核心概念
ECS(实体组件系统)是 Unity 的面向数据框架。它用一种缓存友好、高性能的架构取代了 GameObject/MonoBehaviour 模型。
ECS 与 MonoBehaviour 对比
| 方面 | MonoBehaviour | ECS |
|---|---|---|
| 标识 | GameObject(重量级、受托管) | Entity(轻量级 int ID) |
| 数据 | 组件上的类字段 | 非托管结构体(IComponentData) |
| 逻辑 | MonoBehaviour 上的方法 | System(ISystem / SystemBase) |
| 内存布局 | 分散在堆上 | 按 Archetype 存储在连续的块中 |
| 线程处理 | 仅限主线程 | 使用 Jobs + Burst 并行处理 |
实体
实体是轻量级标识符(一个索引 + 版本整数)。它们没有行为,也没有数据——只是用于关联组件的句柄。
组件(IComponentData)
组件是没有方法的非托管结构体,只保存数据。
using Unity.Entities;
public struct Speed : IComponentData
{
public float Value;
}
public struct Health : IComponentData
{
public float Current;
public float Max;
}
Archetype 和 Chunk
Archetype 是组件类型的一种唯一组合。拥有相同组件集合的所有实体共享一个 Archetype。实体按 Archetype 分组,存储在 Chunk(16 KB 块)中,从而实现高效利用缓存的迭代。
World 和 EntityManager
World 包含一个 EntityManager 和一组系统。默认 World 会自动创建。EntityManager 是用于创建/销毁实体以及添加/移除组件的主要 API。
Baking 和 SubScene 工作流
Baking 会将创作用 GameObject 转换为运行时实体。这是一种单向转换,会在构建时或在 Editor 中加载 SubScene 时执行。
Baker
using Unity.Entities;
// Authoring component (MonoBehaviour on GameObject)
public class SpeedAuthoring : MonoBehaviour
{
public float speed;
}
// Baker converts authoring data to ECS components
public class SpeedBaker : Baker<SpeedAuthoring>
{
public override void Bake(SpeedAuthoring authoring)
{
var entity = GetEntity(TransformUsageFlags.Dynamic);
AddComponent(entity, new Speed { Value = authoring.speed });
}
}
SubScene
- SubScene 包含会在构建时烘焙为实体的 GameObject
- 在运行时,实体数据会高效地流入(没有 GameObject 的开销)
- 在 Editor 中,可以打开 SubScene 进行编辑(显示 GameObject),也可以关闭(显示烘焙后的实体)
- 始终将由 ECS 管理的对象放在 SubScene 中
TransformUsageFlags
| 标志 | 用途 |
|---|---|
Dynamic | 实体会在运行时移动(获得 LocalTransform、LocalToWorld) |
Renderable | 实体会被渲染,但不会由代码移动 |
WorldSpace | 实体仅使用世界空间变换 |
None | 不添加变换组件 |
系统
系统包含所有逻辑。它们会遍历与组件查询匹配的实体。
ISystem(推荐)
ISystem 是现代的非托管系统类型。它兼容 Burst,相较于 SystemBase 应优先使用。
using Unity.Burst;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
[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 (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) { }
}
要点:
- 必须是
partial struct - 在结构体和每个方法上使用
[BurstCompile] OnCreate、OnUpdate、OnDestroy接收ref SystemStatestate.RequireForUpdate<T>()—— 仅当 T 存在时系统才会运行SystemAPI.Query<T>()—— 类型安全的 foreach 迭代
SystemBase(受托管,旧版)
SystemBase 是基于类的受托管系统。它支持受托管代码,但无法在系统级别进行 Burst 编译。所有新代码都应使用 ISystem。SystemBase 使用 Entities.ForEach() lambda 语法,而不是 SystemAPI.Query。
SystemGroup 和更新顺序
系统会按定义的顺序组织到组中并进行更新:
InitializationSystemGroup
-> BeginInitializationEntityCommandBufferSystem
SimulationSystemGroup (default group)
-> BeginSimulationEntityCommandBufferSystem
-> [Your systems go here by default]
-> EndSimulationEntityCommandBufferSystem
PresentationSystemGroup
-> BeginPresentationEntityCommandBufferSystem
使用属性控制顺序:
[UpdateInGroup(typeof(SimulationSystemGroup))]
[UpdateBefore(typeof(OtherSystem))]
[UpdateAfter(typeof(AnotherSystem))]
public partial struct MySystem : ISystem { }
SystemAPI
SystemAPI 提供用于安全、兼容 Burst 的访问方式的静态方法:
| 方法 | 用途 |
|---|---|
SystemAPI.Query<T1, T2>() | 遍历匹配的实体 |
SystemAPI.Time | 访问 TimeData(DeltaTime、ElapsedTime) |
SystemAPI.GetSingleton<T>() | 获取单例组件值 |
SystemAPI.SetSingleton<T>(value) | 设置单例组件值 |
SystemAPI.GetComponent<T>(entity) | 读取实体上的组件 |
SystemAPI.SetComponent<T>(entity, value) | 写入实体上的组件 |
SystemAPI.HasComponent<T>(entity) | 检查实体是否具有组件 |
SystemAPI.GetComponentLookup<T>() | 获取随机访问查找器 |
SystemAPI.GetBuffer<T>(entity) | 获取 DynamicBuffer |
SystemAPI.GetAspect<T>(entity) | 获取实体的 aspect |
深入了解组件
IComponentData(非托管结构体)
基础组件类型。必须是非托管结构体(不含引用类型或托管数组)。
ISharedComponentData
在实体之间共享——具有相同共享组件值的实体会被归入同一个 chunk。更改共享组件值属于结构性更改。
public struct TeamId : ISharedComponentData
{
public int Value;
}
IBufferElementData(DynamicBuffer)
附加到实体的可变长度数组。
[InternalBufferCapacity(8)]
public struct DamageEvent : IBufferElementData
{
public float Value;
public Entity Source;
}
// Usage in a system
foreach (var (buffer, entity) in SystemAPI.Query<DynamicBuffer<DamageEvent>>()
.WithEntityAccess())
{
for (int i = 0; i < buffer.Length; i++)
totalDamage += buffer[i].Value;
buffer.Clear();
}
ICleanupComponentData
在实体销毁后仍会保留。用于清理逻辑(例如,释放原生资源)。移除所有清理组件后,实体才会被完全销毁。
IEnableableComponent
可在不进行结构性更改(不移动 chunk)的情况下启用或禁用的组件。
public struct Stunned : IComponentData, IEnableableComponent { }
// Toggle in system
SystemAPI.SetComponentEnabled<Stunned>(entity, true);
bool isStunned = SystemAPI.IsComponentEnabled<Stunned>(entity);
标记组件
纯粹用于筛选查询的零大小结构体。不含字段——每个实体的内存开销为零。
public struct IsPlayer : IComponentData { }
常见模式
创建和销毁实体
// Direct creation (structural change -- main thread only)
Entity e = entityManager.CreateEntity(typeof(Speed), typeof(Health));
// Preferred: use EntityCommandBuffer for deferred structural changes
var ecb = new EntityCommandBuffer(Allocator.TempJob);
Entity e2 = ecb.CreateEntity();
ecb.AddComponent(e2, new Speed { Value = 5f });
ecb.Playback(entityManager);
ecb.Dispose();
EntityCommandBuffer(ECB)
结构性更改(创建/销毁实体、添加/移除组件)不能在迭代过程中进行。使用 ECB 延迟执行这些操作。有关完整的 ECB API,请参阅 references/ecs-api.md。
系统管理的 ECB(推荐)
[BurstCompile]
public partial struct SpawnSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var ecbSingleton = SystemAPI.GetSingleton<
EndSimulationEntityCommandBufferSystem.Singleton>();
var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);
// ecb is played back automatically -- no Playback/Dispose needed
foreach (var spawner in SystemAPI.Query<RefRW<Spawner>>())
{
if (spawner.ValueRO.Timer <= 0)
{
ecb.Instantiate(spawner.ValueRO.Prefab);
spawner.ValueRW.Timer = spawner.ValueRO.Interval;
}
}
}
}
单例组件
当某个组件恰好由一个实体持有时使用。通过 SystemAPI.GetSingleton<T>() / SystemAPI.SetSingleton<T>(value) / SystemAPI.GetSingletonRW<T>() 访问。适用于全局配置、游戏状态及类似的唯一数据。
Aspects(IAspect)
Aspects 将相关组件组合到一个访问包装器中,并可包含可选方法。
public readonly partial struct CharacterAspect : IAspect
{
public readonly RefRW<LocalTransform> Transform;
public readonly RefRO<Speed> Speed;
public readonly RefRW<Health> Health;
public void Move(float3 direction, float deltaTime)
{
Transform.ValueRW.Position +=
direction * Speed.ValueRO.Value * deltaTime;
}
}
// Use in system
foreach (var character in SystemAPI.Query<CharacterAspect>())
{
character.Move(new float3(1, 0, 0), deltaTime);
}
作业系统
IJobEntity(推荐)
根据 Execute 参数类型自动生成 EntityQuery。
[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);
}
}
// Schedule from system
[BurstCompile]
public partial struct MoveSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
new MoveJob
{
DeltaTime = SystemAPI.Time.DeltaTime
}.ScheduleParallel();
}
}
IJobChunk(底层)
手动迭代 chunk,以获得最大控制力。完整示例请参阅 references/jobs-burst.md。
作业调度
| 方法 | 行为 |
|---|---|
Run() | 在主线程上执行(不调度作业) |
Schedule() | 在工作线程上执行单线程作业 |
ScheduleParallel() | 跨 chunk 执行多线程作业 |
并行作业中的 ECB
[BurstCompile]
public partial struct DestroyDeadJob : IJobEntity
{
public EntityCommandBuffer.ParallelWriter ECB;
void Execute([ChunkIndexInQuery] int sortKey, in Health health, Entity entity)
{
if (health.Current <= 0)
ECB.DestroyEntity(sortKey, entity);
}
}
Burst 编译器
Burst 编译器使用 LLVM 将 IL/.NET 字节码转换为高度优化的原生代码。
用法
在 ISystem 结构体(及其每个方法)和 IJobEntity 结构体上添加 [BurstCompile]。Burst 会通过 LLVM 将它们编译为优化的原生代码。
限制
- 不支持托管类型(不支持
class、string、List<T>、托管数组) - 不支持分配内存(不支持对引用类型使用
new) - 不支持 try/catch/finally
- 不支持虚方法或接口(作业接口除外)
- 不支持静态可变字段(请改用
SharedStatic<T>) - 集合必须使用
NativeContainer类型
Unity.Mathematics
经过 Burst 优化的数学库,可替代 UnityEngine.Mathf 和 Vector3:
using Unity.Mathematics;
float3 position = new float3(1, 2, 3);
quaternion rot = quaternion.Euler(0, math.PI, 0);
float dist = math.distance(a, b);
float3 dir = math.normalize(b - a);
float val = math.lerp(0f, 1f, 0.5f);
| UnityEngine | Unity.Mathematics |
|---|---|
Vector3 | float3 |
Vector2 | float2 |
Quaternion | quaternion |
Mathf.Lerp | math.lerp |
Mathf.Sin | math.sin |
Matrix4x4 | float4x4 |
反模式
| 内容 | 错误原因 | 修复方法 |
|---|---|---|
在 ISystem 可用时使用 SystemBase | 系统无法通过 Burst 编译,会产生堆分配 | 使用带有 [BurstCompile] 的 ISystem(partial struct) |
| 在迭代期间进行结构性更改 | 会使迭代器失效并导致异常 | 使用 EntityCommandBuffer 延迟更改 |
| 在没有 ECB 的并行作业中进行结构性更改 | 竞争条件、崩溃 | 使用带有排序键的 EntityCommandBuffer.ParallelWriter |
| 在 Burst 编译的代码中使用托管类型 | Burst 无法编译托管类型 | 使用非托管结构体、FixedString、NativeContainer |
忘记在 ISystem 上添加 [BurstCompile] | 系统以托管代码运行,损失性能 | 为结构体和所有方法添加 [BurstCompile] |
每帧分配 NativeArray 却不释放 | 内存泄漏 | 使用 Allocator.Temp 分配,或在 OnDestroy 中释放 |
在 Burst 代码中使用 UnityEngine.Mathf | 未针对 Burst 优化 | 使用 Unity.Mathematics.math |
忘记调用 state.RequireForUpdate<T>() | 即使不存在匹配实体,系统也会运行 | 在 OnCreate 中调用,以便在不需要时跳过更新 |
频繁修改 SharedComponentData | 每次更改都是结构性更改(块移动) | 对频繁变化的数据使用常规 IComponentData |
在内层循环中使用 GetComponent | 随机访问会破坏缓存一致性 | 使用 SystemAPI.Query 进行线性迭代 |
关键 API 速查
| 类 / 结构体 | 关键成员 | 说明 |
|---|---|---|
EntityManager | CreateEntity、DestroyEntity、AddComponent<T>、RemoveComponent<T>、GetComponentData<T>、SetComponentData<T> | 仅限主线程;结构性更改 |
EntityCommandBuffer | CreateEntity、DestroyEntity、AddComponent、RemoveComponent、Instantiate、Playback | 延迟执行结构性更改 |
ECB.ParallelWriter | 与 ECB 相同,但带有 sortKey 参数 | 可在线程安全地用于并行作业 |
SystemAPI | Query<T>、Time、GetSingleton<T>、GetComponent<T>、GetComponentLookup<T> | 从系统中进行静态访问 |
EntityQuery | ToEntityArray、ToComponentDataArray<T>、CalculateEntityCount | 批量操作 |
IComponentData | (标记接口) | 非托管结构体组件 |
IBufferElementData | (标记接口) | 动态缓冲区元素 |
DynamicBuffer<T> | Add、RemoveAt、Length、Clear、AsNativeArray | 每个实体的可变长度数组 |
IJobEntity | Execute(ref T1, in T2, ...) | 根据参数自动生成查询 |
IJobChunk | Execute(in ArchetypeChunk, ...) | 手动迭代块 |
World | DefaultGameObjectInjectionWorld、EntityManager | 系统和实体的容器 |
RefRW<T> / RefRO<T> | ValueRW / ValueRO | 可读写 / 只读组件引用 |
相关技能
- unity-scripting -- MonoBehaviour 脚本、协程、async/await
- unity-physics -- 物理引擎、Rigidbody、碰撞体(适用于 Unity Physics ECS 包)
- unity-foundations -- GameObject、Transform、Scene、Prefab
其他资源
- 详细的 Entity/Component API 参见 references/ecs-api.md
- Jobs 和 Burst 深入介绍参见 references/jobs-burst.md
- Unity Entities 手册
- Unity Burst 手册
- Unity Jobs 手册
- Unity Entities API 参考