ECS实战~生成十万个预制体
一、环境与准备(3 分钟上手)
-
Unity 版本:2022.3 LTS 或以上(推荐 LTS,省心)
-
必装包(Package Manager)
Entities1.3.14BurstCollectionsMathematics- (可选)
Entities Graphics(需要 GPU 实例化渲染海量实体时)
开启 Burst:Jobs > Burst > Enable Compilation(Play 前点一下)
二、要做什么(Demo 目标)
我们做一个小行星场:
- 启动时批量生成 N 个实体(方块也行)
- 每帧按朝向位移
- 支持多线程 + Burst
- 带寿命自动回收
- 结构清晰:Authoring → Baker → Components → Aspects → Systems

流程解读
- SpawnerAuthoring
- 场景里你看到的只是一个空物体挂了 Authoring 脚本
- 不直接生成东西,只存生成参数
- Baker 转换
- 运行前,Baker 把 Authoring 转成 ECS 组件(SpawnSettings)+ 把 Prefab 转成 ECS 模板实体
- ECS 世界运行
- Spawner 实体只占一个 Entity ID + 一个 SpawnSettings 组件
- Prefab 实体包含渲染和 Transform 等基础组件,作为模板存在
- SpawnSystem 一次性批量生成
- 用 EntityCommandBuffer 循环
Instantiate(Prefab)N 次 - 实体按 Archetype(组件组合)被连续存储在 Chunk 里()
- 同类数据连续放内存 → CPU 缓存命中率极高
- 用 EntityCommandBuffer 循环
- 其他 Systems 并行处理
- MoveSystem 批量处理所有有
MoveSpeed+Heading的实体 - LifetimeSystem 批量减少寿命并回收过期实体
- MoveSystem 批量处理所有有
三、数据建模(ECS 组件与 Aspect)
1)纯数据组件(IComponentData)
using Unity.Entities;
using Unity.Mathematics;
public struct MoveSpeed : IComponentData
{
public float Value;
}
public struct Heading : IComponentData
{
public float3 Value; // 归一化方向
}
public struct Lifetime : IComponentData
{
public float Seconds;
}
// 单例配置(场景内 1 份)
public struct SpawnSettings : IComponentData
{
public Entity Prefab;
public int Count;
public float3 AreaSize; // 生成范围盒
}
2)把常用组合做成 Aspect(写逻辑更顺手)
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
//Aspect is a struct that contains the data required to perform a specific action on an entity.
public readonly partial struct MovementAspect : IAspect
{
//RefRW is a reference type that allows us to read and write to the entity's components.
public readonly RefRW<LocalTransform> Transform;
//RefRO is a reference type that allows us to read the entity's components.
public readonly RefRO<MoveSpeed> Speed;
public readonly RefRO<Heading> Dir;
public void Move(float dt)
{ //We use the ValueRW property to read and write to the entity's components.
var t = Transform.ValueRW;
t.Position += Dir.ValueRO.Value * Speed.ValueRO.Value * dt;
Transform.ValueRW = t;
}}
四、从 Authoring 到 Baker(把编辑器设置“烘焙”成 ECS 数据)
Authoring 脚本(挂场景中)
using UnityEngine;
public class SpawnerAuthoring : MonoBehaviour
{
public GameObject Prefab;
public int Count = 10000;
public Vector3 AreaSize = new(200, 0, 200);
}
Baker(转换为 SpawnSettings + 预制体实体)
using Unity.Entities;
//Baker is a generic class that takes a component data authoring
//class as a parameter and provides a Bake method to create an entity with the component data.
public class SpawnerBaker : Baker<SpawnerAuthoring>
{
public override void Bake(SpawnerAuthoring authoring)
{
//TransformUsageFlags.None means that the entity will not be parented to any other entity.
var entity = GetEntity(TransformUsageFlags.None);
//TransformUsageFlags.Dynamic means that the entity will be parented to another entity.
var prefabEntity = GetEntity(authoring.Prefab, TransformUsageFlags.Dynamic);
AddComponent(entity, new SpawnSettings {
Prefab = prefabEntity,
Count = authoring.Count,
AreaSize = authoring.AreaSize
});
}}
五、生成系统(一次性跑,批量造实体)
做法:ISystem + EntityCommandBuffer(ECB)安全并行创建、设初值
说明:Entities 1.3 里用
SystemAPI写法最顺手
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
[BurstCompile]
public partial struct SpawnSystem : ISystem
{
// 系统初始化
[BurstCompile]
public void OnCreate(ref SystemState state)
{ //只有存在SpawnSettings组件的实体才会被激活
state.RequireForUpdate<SpawnSettings>();
}
// 系统更新
[BurstCompile]
public void OnUpdate(ref SystemState state)
{ var settings = SystemAPI.GetSingleton<SpawnSettings>();
var ecb = new EntityCommandBuffer(Allocator.Temp);
var rnd = new Unity.Mathematics.Random(
(uint)SystemAPI.Time.ElapsedTime * 1664525u + 1013904223u);// 随机种子
for (int i = 0; i < settings.Count; i++)
{
// 实例化ECS实体预制体,发生在内存连续的ArchetypeChunk中
var e = ecb.Instantiate(settings.Prefab);
float3 pos = new float3(
rnd.NextFloat(-settings.AreaSize.x * .5f, settings.AreaSize.x * .5f),
rnd.NextFloat(-settings.AreaSize.y * .5f, settings.AreaSize.y * .5f),
rnd.NextFloat(-settings.AreaSize.z * .5f, settings.AreaSize.z * .5f));
float3 dir = math.normalize(rnd.NextFloat3Direction());
dir.y = 0;
ecb.SetComponent(e, LocalTransform.FromPositionRotationScale(
pos, quaternion.LookRotationSafe(dir, math.up()), 1f));
ecb.AddComponent(e, new Heading { Value = dir });
ecb.AddComponent(e, new MoveSpeed { Value = rnd.NextFloat(2f, 8f) });
ecb.AddComponent(e, new Lifetime { Seconds = rnd.NextFloat(10f, 25f) });
}
ecb.Playback(state.EntityManager);
ecb.Dispose();
// 只生成一次
state.Enabled = false;
}}
流程解释
- 记录阶段(Record Phase)
- 在
OnUpdate或 Job 中,用ecb.Instantiate()、ecb.SetComponent()等方法把改动写进 ECB 缓冲区。 - 这个阶段不改动 ECS 世界,所以遍历时不会冲突。
- 在
- 回放阶段(Playback Phase)
- 遍历结束后,在安全时机调用
ecb.Playback(state.EntityManager)。 - ECB 会按记录顺序执行所有命令(创建实体、改组件、加/删组件等)。
- 这时 ECS 世界(EntityManager)才真正被修改。
- 遍历结束后,在安全时机调用
- 销毁阶段(Dispose)
- 用完 ECB 必须
Dispose()释放分配的内存(除非用的是系统自带的 Persistent ECB)。
- 用完 ECB 必须
六、移动系统(IJobEntity 并行 + Burst)
写法 1:IJobEntity(更易并行化)
using Unity.Burst;
using Unity.Entities;
using Unity.Jobs;
using Unity.Transforms;
[BurstCompile]
public partial struct MoveJob : IJobEntity
{
public float DeltaTime;
void Execute(MovementAspect aspect)
{
aspect.Move(DeltaTime);
}
}
[BurstCompile]
public partial struct MoveSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
new MoveJob { DeltaTime = SystemAPI.Time.DeltaTime }
.ScheduleParallel();
}
}
写法 2:SystemAPI.Query(最直接,易读)
[BurstCompile]
public partial struct MoveSystem_Alt : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
float dt = SystemAPI.Time.DeltaTime;
foreach (var aspect in SystemAPI.Query<MovementAspect>())
{
aspect.Move(dt);
}
}
}
提示:想要最大化吞吐,倾向 IJobEntity + ScheduleParallel;想快速验证逻辑用
foreach也行。
七、寿命与回收(无 GC 的“对象池”味道)
using Unity.Burst;
using Unity.Entities;
[BurstCompile]
public partial struct LifetimeSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var ecb = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>()
.CreateCommandBuffer(state.WorldUnmanaged);
foreach (var (life, entity) in SystemAPI.Query<RefRW<Lifetime>>().WithEntityAccess())
{
life.ValueRW.Seconds -= SystemAPI.Time.DeltaTime;
if (life.ValueRO.Seconds <= 0f)
ecb.DestroyEntity(entity);
}
}
}
想做“复用而非销毁”的对象池:把
Destroy换成移出可见区 + 重置组件即可。
🚨EndSimulationEntityCommandBufferSystem 就是 Unity 内置的“一帧收单 → 一次性执行 → 清空收银台”机制,保证结构性更改安全、批量、高效,同时不让你手动管理生命周期。
八、渲染与可视化(Entities Graphics 可选)
- 如果装了
Entities Graphics:准备一个带MeshRenderer的 Prefab,在 Baker 里GetEntity(prefab, TransformUsageFlags.Dynamic)即可批量实例化。 - 没装也没事:可用
Hybrid Renderer或者简单 Gizmos 验证。
九、性能与调试(一分钟自检)
- Burst:确保菜单已启用;看 Console 首帧会打印启用信息
- Profiler:添加 “Entities” 模块,观察 Archetype Chunk、System 时间
- 查询筛选:
WithAll/WithNone/WithDisabled控制查询集合 - Chunk 顺序:把常访问的组件放一起(减少跨 Chunk)
- 只读标注:
RefRO<>/in参数,帮助调度并行 - ECB:结构变化(增删组件、创建销毁)放到 CommandBuffer,避免结构性竞争
十、从 MonoBehaviour 迁移的 4 步法
- 拆数据:把脚本里的字段抽成
IComponentData(只留数据) - 提算法:把
Update()逻辑搬进ISystem/IJobEntity - 做 Baker:把 Prefab/初始参数从 Authoring 转成组件
- 小步并行:先
foreach跑通 → 换IJobEntity.ScheduleParallel→ 最后 Burst 化
十二、完整文件清单(拷就能跑)
Scripts/
Components/
MoveSpeed.cs
Heading.cs
Lifetime.cs
SpawnSettings.cs
MovementAspect.cs
Authoring/
SpawnerAuthoring.cs
SpawnerBaker.cs
Systems/
SpawnSystem.cs
MoveSystem.cs
LifetimeSystem.cs
十三、常见坑位清单
- 没装 Burst 或 未启用 → 性能上不去
- 在 Job 里用 UnityEngine API → 会报错/不兼容
- 结构改动太频繁(增删组件、创建销毁)→ 用 ECB + 批量合并
- 随机数:别用
UnityEngine.Random;用Unity.Mathematics.Random,Job 内要用ref储存与回写 - 多场景:用 SubScene 存 ECS 物体,避免转化期抖动
十四、加餐:以 IJobEntity 改写随机游走(并行刷新朝向)
using Unity.Burst;
using Unity.Entities;
using Unity.Mathematics;
[BurstCompile]
public partial struct WanderJob : IJobEntity
{
public float DeltaTime;
public uint Seed;
void Execute(ref Heading heading)
{
var rnd = new Unity.Mathematics.Random(Seed ^ (uint)heading.GetHashCode());
// 轻微抖动朝向
var dir = heading.Value + rnd.NextFloat3Direction() * 0.1f * DeltaTime;
dir.y = 0;
heading.Value = math.normalize(dir);
}
}
[BurstCompile]
public partial struct WanderSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
new WanderJob {
DeltaTime = SystemAPI.Time.DeltaTime,
Seed = (uint)(SystemAPI.Time.ElapsedTime * 1000003)
}.ScheduleParallel();
}
}
十五、5 道 Unity / DOTS 面试题(含解析)
-
问:为什么 ECS 比 MonoBehaviour 快?
答:数据局部性 + 批量处理 + 并行 + Burst。相同组件连续存储,缓存命中率高;系统一次性遍历同类组件,减少函数与对象跳转;Job System 并行调度;Burst 生成向量化本地码。 -
问:
IJobEntity和SystemAPI.Query的取舍?
答:IJobEntity更适合并行和 Burst,写明依赖可自动调度;Query更直接易读,验证逻辑快。性能优先用IJobEntity,原型优先用Query。 -
问:为何结构性更改要用
EntityCommandBuffer?
答:创建/销毁/增删组件会改 Archetype,直接改可能与其他系统并行冲突。ECB 记录更改,统一安全回放,避免结构竞争与同步成本。 -
问:如何在 Job 中使用随机数?
答:使用Unity.Mathematics.Random,非静态、可复制的结构体。为每个 Job/实体准备独立种子,避免竞态;完成后写回状态。 -
问:Burst 有哪些限制?
答:不能用UnityEngine.*、反射、GC 分配、异常等托管高级特性;数据结构需 blittable;函数尽量static/可内联;readonly有助于优化。
作者:世纪末的魔术师
出处:https://www.cnblogs.com/Firepad-magic/
Unity最受欢迎插件推荐:点击查看
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

浙公网安备 33010602011771号