1. 项目概述为什么你需要掌握ECS的Authoring与Baking如果你正在使用Unity开发一个对性能有严苛要求的项目比如一个拥有成千上万单位同屏战斗的RTS游戏或者一个需要处理海量粒子与物理模拟的模拟器那么传统的GameObject-MonoBehaviour工作流很可能已经让你感到力不从心。帧率波动、GC垃圾回收卡顿、难以利用多核CPU这些都是GameObject模式在应对大规模数据时常见的瓶颈。Unity的ECSEntity Component System架构特别是其核心的Authoring创作与Baking烘焙工作流正是为了解决这些问题而生的。它不是对旧模式的简单修补而是一次从“面向对象”到“面向数据”的范式转变。简单来说Authoring Baking是连接我们熟悉的Unity编辑器世界GameObject、Prefab、MonoBehaviour与高性能的ECS运行时世界Entity、Component、System的桥梁。Authoring指的是我们在编辑器中使用传统的、对设计师友好的工具如Inspector、Prefab来创建和配置游戏内容。Baking则是一个自动化的转换过程它将我们创作的这些“设计师友好”的数据高效、不可逆地转换成ECS运行时所需的、为性能极致优化的Entity和Component数据。这个过程就像把生食材GameObject通过一个精密的厨房Baking系统加工成一份份标准化的、可以快速加热上菜的预制菜Entity数据从而在游戏运行时获得最高的“出餐”效率。本教程将带你从零开始深入理解Unity Entities 1.4中这套核心工作流。我们不会停留在概念层面而是会通过具体的代码示例和编辑器操作一步步拆解如何创建Authoring组件、编写Baker逻辑、管理依赖关系并最终生成高效的运行时数据。无论你是对ECS感到好奇的新手还是已经尝试过但被Baking过程困扰的开发者这篇文章都将为你提供一套清晰、可复现的实践指南。2. ECS核心工作流全貌从编辑器到运行时的数据之旅在深入细节之前我们必须建立一个宏观的认知。ECS工作流不是一个孤立的编程模式而是一个贯穿项目开发始终的完整数据管线。理解这个管线的每个环节是避免后续踩坑的关键。2.1 传统模式 vs. ECS模式的数据流对比在传统MonoBehaviour模式下你的工作流大致是这样的创建一个GameObject - 挂载MonoBehaviour脚本 - 在Start()或Awake()中初始化 - 在Update()中每帧执行业务逻辑。数据和逻辑强耦合在同一个脚本对象里。当你有10,000个这样的GameObject时你就有了10,000个Update调用CPU缓存不友好且难以并行。而引入了Authoring Baking的ECS模式数据流被清晰地分成了两个阶段创作阶段 (Authoring Phase):场所 Unity编辑器场景。对象 普通的GameObject我们称之为Authoring GameObject。组件 我们编写的继承自MonoBehaviour的脚本但它们的角色变了我们称之为Authoring Component。这些组件不再包含运行时逻辑它们只有一个职责声明这个GameObject在转换成Entity后应该拥有哪些数据即ECS Components。目标 提供友好的编辑体验。设计师可以在Inspector里调整数值策划可以配置Prefab所有工作都和以前一样直观。烘焙阶段 (Baking Phase):触发 当Authoring Scene被打开作为Subscene或发生修改时自动触发。过程 Unity会为场景中每一个带有Authoring Component的GameObject调用对应的Baker。Baker是一个特殊的类它的任务就是读取Authoring Component上的配置然后通过API向一个“构建中”的Entity添加ECS Component。输出 生成一个或多个Entity Scene文件二进制数据。这些文件包含了所有烘焙好的Entity和它们的Component数据格式对CPU缓存极其友好。运行时阶段 (Runtime Phase):加载 游戏运行时包括编辑器Play模式加载的是烘焙好的Entity Scene而不是原始的GameObject场景。执行 ECS的System在这些Entity数据上运行。System通过查询EntityQuery来筛选具有特定组件组合的Entity并以批处理、并行化的方式处理它们的数据从而实现极高的性能。这个流程的核心思想是“一次转换处处高效”。将昂贵的转换过程Baking放在编辑阶段完成运行时只处理最优化格式的数据。2.2 Subscene连接两个世界的容器Subscene是一个关键概念它是Authoring Scene和Entity Scene之间的纽带。在编辑器中你可以将一个普通的场景文件.unity标记为Subscene。当你打开这个Subscene进行编辑时你看到的是Authoring GameObject而当你关闭它或进入Play模式时它内部的内容会被自动烘焙并呈现为Entity。Subscene的两种烘焙模式实时烘焙 (Live Baking) 当Subscene处于打开状态时你对Authoring GameObject的任何修改都会触发增量烘焙。只有被修改部分相关的Entity会被重新烘焙速度极快让你能近乎实时地看到ECS数据的变化效果。这是迭代开发的主要方式。异步全量烘焙 (Async Full Bake) 当Subscene关闭时如果你修改了场景并保存Unity会在后台进行全量烘焙。这会重新处理整个场景生成全新的Entity Scene文件。当你首次加载一个包含关闭Subscene的主场景时也会触发这种烘焙可能会有一个短暂的等待。注意 增量烘焙和全量烘焙的结果在实体顺序和内存布局上可能存在细微差异这是由它们的处理机制决定的。你的游戏逻辑绝不能依赖实体的创建顺序或它们在内存块Chunk中的位置。所有逻辑都应基于组件数据和EntityQuery这才是ECS设计所倡导的。3. 实战入门创建你的第一个Authoring组件与Baker理论说得再多不如动手一试。让我们从一个最简单的例子开始将一个表示“移动物体”的GameObject通过Baking转换成ECS中具有位置和速度数据的Entity。3.1 定义运行时组件 (Runtime Components)首先我们需要定义ECS运行时需要的组件。这些是纯粹的数据结构不包含任何方法。using Unity.Entities; // 这是一个简单的速度数据组件实现了IComponentData接口。 public struct Velocity : IComponentData { public float Value; // 每秒移动的单位数 } // 这是一个标签组件用于标记需要被移动系统处理的实体。 public struct MovableTag : IComponentData, IEnableableComponent { // IEnableableComponent 允许我们在运行时动态启用/禁用这个组件非常有用。 }3.2 创建创作组件 (Authoring Component)接下来我们创建在编辑器中使用MonoBehaviour。它的作用是暴露配置参数给设计师。using UnityEngine; public class MovableAuthoring : MonoBehaviour { // 设计师可以在Inspector中设置初始速度 public float InitialSpeed 5.0f; // 这个字段决定烘焙后是否默认启用移动功能 public bool StartEnabled true; }将这个脚本拖到一个GameObject上你会在Inspector中看到InitialSpeed和StartEnabled两个字段可以随意调整。这就是Authoring的友好之处。3.3 编写烘焙器 (Baker)这是最关键的一步。Baker负责将MovableAuthoring的数据“翻译”成ECS组件。我们需要创建一个继承自BakerT的类其中T是我们的Authoring组件类型。using Unity.Entities; using UnityEngine; // 使用泛型指定这个Baker用于处理MovableAuthoring组件 public class MovableBaker : BakerMovableAuthoring { public override void Bake(MovableAuthoring authoring) { // 1. 获取或创建与这个GameObject关联的Entity // DependsOn(authoring.gameObject) 声明了烘焙依赖关系后面会详细讲。 var entity GetEntity(TransformUsageFlags.Dynamic); // 2. 添加运行时组件Velocity并从Authoring中获取初始值 AddComponent(entity, new Velocity { Value authoring.InitialSpeed }); // 3. 添加标签组件MovableTag AddComponentMovableTag(entity); // 4. 根据Authoring的配置设置标签组件的初始启用状态 // SetComponentEnabled 是用于IEnableableComponent的API。 SetComponentEnabledMovableTag(entity, authoring.StartEnabled); // 注意我们并没有直接添加Transform相关的组件如LocalTransform。 // 因为GetEntity(TransformUsageFlags.Dynamic)调用已经隐含地为我们处理了标准变换。 // 如果GameObject有Transform它会自动添加LocalTransform或LocalToWorld等组件。 } }代码解析与要点GetEntity(TransformUsageFlags.Dynamic): 这是获取Entity引用的标准方式。TransformUsageFlags告诉Unity这个实体在运行时是否需要变换组件如用于渲染或物理。Dynamic表示需要完整的变换支持。其他选项还有None不需要变换和Static静态物体有优化空间。AddComponent: 向目标Entity添加一个组件实例。SetComponentEnabled: 专门用于控制实现了IEnableableComponent的组件的激活状态。这是一个强大的特性允许你动态地“开关”某个系统对实体的处理而无需添加/移除组件性能更好。3.4 创建并观察Subscene在场景中创建一个空GameObject重命名为“MovableCube”。给它添加一个CubePrimitive和MovableAuthoring组件。在Hierarchy中右键选择New Subscene From Selection。Unity会创建一个Subscene容器并将“MovableCube”移入其中。选中这个Subscene在Inspector中点击Open。现在你进入了Subscene的编辑上下文。尝试修改“MovableCube”上MovableAuthoring的InitialSpeed值。如果一切正常你不会感觉到明显的卡顿这就是增量烘焙在默默工作。在编辑器菜单栏打开Window Entities Entity Inspector。在Entity Inspector中你应该能看到一个Entity它包含了Velocity、MovableTag以及由Unity自动添加的LocalTransform等组件。这证明你的Baking成功了4. 深入Baking依赖管理、多实体与预制件处理简单的单个实体转换只是开始。实际项目中一个Authoring GameObject可能对应多个Entity或者需要引用其他资产、生成复杂结构。Baker API提供了丰富的方法来处理这些情况。4.1 声明依赖关系 (Dependency)这是Baking中最重要也最容易出错的概念。Baking过程不是顺序执行的Unity可能会并行处理多个Baker。如果你的Baker依赖于其他Baker产生的数据比如另一个实体或组件你必须显式声明这种依赖否则会导致数据竞争或访问到未初始化的数据。public class SpawnerAuthoring : MonoBehaviour { public GameObject PrefabToSpawn; public int SpawnCount 10; } public class SpawnerBaker : BakerSpawnerAuthoring { public override void Bake(SpawnerAuthoring authoring) { // 错误示范直接使用authoring.PrefabToSpawn没有声明依赖。 // 如果PrefabToSpawn这个资源本身还在被其他Baker处理这里可能会出错。 // var prefabEntity GetEntity(authoring.PrefabToSpawn, TransformUsageFlags.Dynamic); // 正确做法声明对PrefabToSpawn这个资源的依赖 DependsOn(authoring.PrefabToSpawn); // 现在可以安全地获取这个预制件对应的Entity var prefabEntity GetEntity(authoring.PrefabToSpawn, TransformUsageFlags.Dynamic); var spawnerEntity GetEntity(TransformUsageFlags.None); AddComponent(spawnerEntity, new SpawnerData { Prefab prefabEntity, Count authoring.SpawnCount }); } } public struct SpawnerData : IComponentData { public Entity Prefab; public int Count; }DependsOn的常见使用场景DependsOn(authoring.gameObject): 依赖于此Authoring GameObject自身的变换、激活状态等。在GetEntity()内部通常已隐含此依赖。DependsOn(authoring.someComponent): 依赖于此GameObject上另一个MonoBehaviour组件的数据。DependsOn(authoring.someObjectReference): 依赖一个UnityEngine.Object引用如Prefab、Material、Texture等。这是必须的因为你需要等待这些资产被导入和烘焙。DependsOn(authoring.transform.parent): 依赖父节点当你需要基于层级关系创建实体时。实操心得养成一个习惯在Baker中任何使用GetEntity获取非当前Authoring GameObject的引用之前先写一句DependsOn。这能避免大量难以调试的“烘焙结果不一致”问题。4.2 烘焙多个实体与复杂结构一个Baker并不只限于创建一个Entity。你可以创建多个Entity并构建它们之间的关系。public class WeaponAuthoring : MonoBehaviour { public Transform FirePoint; public GameObject ProjectilePrefab; } public class WeaponBaker : BakerWeaponAuthoring { public override void Bake(WeaponAuthoring authoring) { var weaponEntity GetEntity(TransformUsageFlags.Dynamic); // 声明对所有依赖资源的依赖 DependsOn(authoring.FirePoint); DependsOn(authoring.ProjectilePrefab); // 获取FirePoint变换对应的Entity它会是一个子实体 var firePointEntity GetEntity(authoring.FirePoint, TransformUsageFlags.Dynamic); // 获取子弹预制件对应的Entity var projectilePrefabEntity GetEntity(authoring.ProjectilePrefab, TransformUsageFlags.Dynamic); // 为武器实体添加组件存储子弹预制件和开火点信息 AddComponent(weaponEntity, new WeaponComponent { ProjectilePrefab projectilePrefabEntity, FirePoint firePointEntity }); // 我们还可以为开火点实体单独添加一个标签组件方便系统查询 AddComponentFirePointTag(firePointEntity); } } public struct WeaponComponent : IComponentData { public Entity ProjectilePrefab; public Entity FirePoint; } public struct FirePointTag : IComponentData { }在这个例子中一个WeaponAuthoringGameObject烘焙后会产生至少两个Entity武器主体和开火点。它们通过WeaponComponent中的Entity引用关联起来。4.3 处理预制件 (Prefab) 的Baking预制件的Baking有其特殊性。当你在Authoring场景中放置一个Prefab时Unity实际上会实例化它。Baking过程会为这个Prefab实例创建一个独立的Entity。但是如果你需要获取这个Prefab的“原型”Entity用于运行时实例化你需要使用GetEntity并传入Prefab资产本身同时标记TransformUsageFlags.Dynamic。关键点用于实例化的Prefab Entity通常不应该包含那些每次实例化时都不同的数据。例如一个“怪物”Prefab可能包含Health组件但初始血量应该是运行时由生成系统赋予的而不是在Prefab的Baker中写死。Prefab的Baker应该只添加那些所有实例共有的组件如渲染相关的MaterialMeshInfo、碰撞体PhysicsCollider等。// 假设这是一个怪物预制件上的Authoring组件 public class MonsterAuthoring : MonoBehaviour { // 这个颜色是所有该类型怪物共有的 public Color MonsterColor Color.red; } public class MonsterBaker : BakerMonsterAuthoring { public override void Bake(MonsterAuthoring authoring) { var entity GetEntity(TransformUsageFlags.Dynamic); // 添加所有实例共有的组件 AddComponent(entity, new MonsterSharedData { Color (Vector4)authoring.MonsterColor }); AddComponentMonsterTag(entity); // 注意我们不在这里添加Health组件 // Health应该由生成怪物的SpawnerSystem在实例化时动态添加并设置初始值。 } } // 运行时生成系统会这样操作 public partial struct MonsterSpawnSystem : ISystem { public void OnUpdate(ref SystemState state) { var ecb new EntityCommandBuffer(Unity.Collections.Allocator.Temp); foreach (var (spawner, transform) in SystemAPI.QuerySpawnerData, LocalTransform()) { for (int i 0; i spawner.Count; i) { // 实例化Prefab var newMonster ecb.Instantiate(spawner.Prefab); // 动态添加并设置实例特有的数据 ecb.AddComponent(newMonster, new Health { Value 100 }); // 设置位置等 ecb.SetComponent(newMonster, LocalTransform.FromPosition(transform.Position ...)); } } ecb.Playback(state.EntityManager); } }5. 高级主题与性能优化策略当你掌握了基础Baking后以下高级主题和优化技巧能帮助你构建更健壮、更高效的项目。5.1 Baking版本控制与缓存Baking系统有完善的缓存机制来避免重复烘焙。它通过计算Authoring数据、Baker代码和项目设置的哈希值来判断是否需要重新烘焙。你可以通过[BakingVersion]属性来主动控制Baker代码的版本。// 在你的Baker类所在的程序集中添加一个BakingVersion属性。 // 当你修改了Baker的逻辑并希望强制所有使用该Baker的资源重新烘焙时改变这个版本号。 [assembly: BakingVersion(MonsterBaker, 2)] namespace MyGame.Authoring { public class MonsterBaker : BakerMonsterAuthoring { // ... Baker逻辑 } }何时需要更新BakingVersion修改了Baker内部的逻辑导致输出的Entity或组件结构发生变化。添加或移除了某个组件的添加操作。改变了组件数据的计算方式。如果你只修改了Authoring组件如增加了一个public字段但没有修改Baker读取它的逻辑通常不需要更新BakingVersion增量烘焙会自动处理字段值的变化。5.2 使用IBaker与Baker的取舍除了继承BakerT你还可以让Authoring组件实现IBakerT接口。这允许你将Baking逻辑直接写在Authoring组件类内部对于简单的转换可能更紧凑。但BakerT类更灵活可以一个Baker处理多种Authoring类型也更易于组织代码。对于复杂的Baking逻辑我强烈推荐使用独立的BakerT类。5.3 烘焙顺序与[BakeDependency]属性有时一个Baker需要等待另一个Baker完成才能执行。例如一个TeamManager的Baker需要收集所有Unit实体的引用。你可以通过[BakeDependency]属性来指定Baker之间的依赖顺序。// UnitBaker先执行 public class UnitBaker : BakerUnitAuthoring { ... } // TeamManagerBaker后执行因为它依赖UnitBaker的结果 [BakeDependency(typeof(UnitBaker))] public class TeamManagerBaker : BakerTeamManagerAuthoring { public override void Bake(TeamManagerAuthoring authoring) { // 这里可以安全地查询所有已经烘焙好的、带有UnitTag的实体 var query new EntityQueryBuilder(Allocator.Temp).WithAllUnitTag().Build(this); var units query.ToEntityArray(Allocator.Temp); // ... 使用units数组 } }注意过度使用[BakeDependency]会限制Baking的并行性可能影响烘焙速度。应优先考虑通过组件引用Entity在运行时建立关联而非在烘焙时进行复杂的查询和集合操作。5.4 性能优化减少烘焙开销精简Authoring组件 Authoring组件只在编辑器和烘焙时存在。避免在其中包含复杂的序列化对象如大数组、字典这会影响场景加载和烘焙速度。复杂的配置应考虑使用ScriptableObject。善用增量烘焙 在开发阶段尽量在打开的Subscene内工作享受增量烘焙的实时反馈。关闭不需要的Subscene以减少内存占用和后台烘焙负担。避免在Baker中进行昂贵计算 Baker会在每次修改时运行。如果某个计算量很大且结果稳定考虑将结果存储在ScriptableObject中或在运行时System的OnCreate中计算一次。清理调试组件 使用[TemporaryBakingType]属性标记那些仅用于调试烘焙过程的组件确保它们不会进入最终的运行时构建。6. 常见问题排查与调试技巧实录即使理解了原理在实际操作中依然会遇到各种问题。以下是我在项目中积累的一些常见问题及其解决方法。6.1 问题速查表问题现象可能原因解决方案实体在Entity Inspector中不显示或组件缺失1. Baker代码有编译错误。2. Authoring GameObject未放在打开的Subscene内。3. Baker中的GetEntity()使用了错误的TransformUsageFlags。4. 没有正确添加using Unity.Entities;。1. 检查Console窗口的编译错误。2. 确保GameObject在Subscene中且Subscene已打开。3. 对于需要变换的实体使用TransformUsageFlags.Dynamic。4. 检查Baker类文件的using语句。修改Authoring组件数值运行时无变化1. 增量烘焙未触发或失败。2. Baker代码没有读取你修改的那个字段。3. 该字段值被运行时System覆盖。1. 检查Subscene是否打开触发增量烘焙。尝试在Inspector中点击Subscene的“Reimport”按钮。2. 确认Baker中使用了authoring.YourField。3. 在System中打断点检查是否每帧都在修改该值。烘焙时报错InvalidOperationException1. 在Baker中访问了未声明依赖DependsOn的外部资源或实体。2. 尝试在Baker中通过EntityManager进行结构性更改这是不允许的。1. 在访问任何非自身GameObject的引用前添加DependsOn。2. 在Baker中只能使用this.AddComponent,this.GetEntity等Baker API不能使用World.EntityManager。预制件实例化后缺少某些组件1. 该组件是在Prefab实例的Baker中添加的而不是在Prefab资产的Baker中添加的。2. 组件是IEnableableComponent且初始状态为禁用在Entity Inspector中默认不显示。1. 确保所有实例共有的组件都在Prefab资产的Baker中添加。2. 在Entity Inspector中勾选“Show All Components”查看禁用组件。进入Play模式或构建后实体行为与编辑器Subscene中不一致1. 增量烘焙与全量烘焙的结果存在差异如实体顺序。2. 有些调试用的Authoring组件没有正确的Baker导致构建时被剥离。1. 确保游戏逻辑不依赖实体ID或顺序。使用SystemAPI.Query和组件数据来驱动逻辑。2. 为所有需要在运行时存在的Authoring组件编写Baker或使用[DisableInBaker]属性标记纯编辑器组件。烘焙过程非常缓慢1. Authoring场景过于复杂GameObject数量太多。2. Baker中包含了复杂的循环或计算。3. 有大量未优化的资源引用如高分辨率纹理被直接依赖。1. 考虑将静态或不常修改的部分拆分到独立的Subscene中并关闭它。2. 优化Baker逻辑将复杂计算移至运行时或预处理到ScriptableObject中。3. 检查依赖关系确保Baker只DependsOn真正需要的资源。6.2 实用调试技巧使用Debug.Login Baker: 你可以在Baker中使用Debug.Log输出信息这有助于理解烘焙的执行顺序和检查数据。这些日志只在编辑器的Baking过程中输出。Entity Inspector是你的最佳朋友: 始终打开Entity Inspector窗口与Game窗口并排。它可以实时显示当前选中Subscene或World中的所有实体及其组件数据是验证烘焙结果最直观的工具。检查Baking日志: 在Unity Console窗口将过滤模式切换到“Baking”可以看到详细的烘焙过程日志包括警告和错误。手动触发全量烘焙: 当怀疑增量烘焙有缓存问题时可以选中Subscene在Inspector底部点击“Reimport”按钮强制进行全量烘焙。清空Baking缓存: 如果遇到非常诡异的不一致问题可以尝试清空缓存。路径是Edit Preferences Entities Clear Baking Cache。注意这会导致所有Subscene重新烘焙可能需要一些时间。掌握Unity ECS的Authoring Baking工作流是一个从“如何写代码”到“如何设计数据管道”的思维升级。初期可能会觉得繁琐但一旦这套流程跑通你会发现它带来的性能提升、代码清晰度和团队协作效率的提升是巨大的。它强制你将数据Component与行为System分离将编辑时配置Authoring与运行时结构Baked Entity分离这种关注点分离正是构建大型、高性能项目的基石。从今天开始尝试在你的下一个功能模块中使用Subscene和Baker亲自体验这种数据驱动开发带来的流畅感吧。