ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Unity ECS 烘焙完全拆解:子场景到实体场景的完整流水线

Unity ECS 烘焙完全拆解:子场景到实体场景的完整流水线 Unity ECS 烘焙完全拆解子场景到实体场景的完整流水线【免费下载链接】EntityComponentSystemSamples项目地址: https://gitcode.com/GitHub_Trending/en/EntityComponentSystemSamplesUnity ECS 的烘焙Baking决定子场景SubScene如何变成运行时可加载的实体场景Entity Scene。本文基于 EntitiesSamples 官方示例拆解 Baker、Baking System 与 BakingType 三者的分工、增量烘焙的依赖追踪以及 BlobAsset 去重技巧适合正在搭 DOTS 内容管线、想弄清编辑器数据去哪了的工程师。一、为什么子场景需要烘焙背景与核心概念 先把类比放前面。GameObject 层级像是家具厂的图纸——给设计师看的摆起来直观但没法直接上流水线实体加组件Entity Component则是零件清单——给机器用的按数据布局存放、缓存友好。烘焙就是那道从图纸到零件清单的工序它在构建期编辑器里修改子场景时也会实时重跑把场景数据转换成一种新的序列化格式运行时通过场景系统Scene System异步加载。四个术语严格定义对应官方文档 EntitiesSamples/Docs/baking.md术语定义子场景SubScene由SubSceneMonoBehaviour 挂进主场景的普通 Unity 场景资产实体场景Entity Scene序列化的实体与组件集合可在运行时加载Baker继承BakerT的类T 是 MonoBehaviour拥有 Baker 的 MonoBehaviour 叫 authoring 组件Baking System标注[WorldSystemFilter(WorldSystemFilterFlags.BakingSystem)]的普通系统运行在烘焙世界整条流水线的主干只有三步关键点在 C 到 D 的交接Baker 是每实体视角一次只处理一个 authoring 组件Baking System 是整个世界视角可以在所有 Baker 跑完之后跨实体做结构变更——比如把一堆顶点实体合并成一份 BlobAsset。另外注意第三步是全量重跑的即使只有某个 Baker 被增量触发Baking Systems 每次都会完整执行一遍这直接决定了 Baking System 的代码必须写成幂等、按变化量工作的形式。要点提示子场景是给人编辑的中间态实体场景才是运行时消费的产物理解谁在哪一步跑是后面所有机制的地基。二、关键机制拆解2.1 Baker 的最小形态把字段从 authoring 拷进组件结论一个标准 Baker 就做两件事——拿实体、加组件示例代码里一行逻辑都不多。仓库里 EntitiesSamples/Assets/ExampleCode/Baking.cs 的EnergyShieldAuthoring是最短路径public class Baker : BakerEnergyShieldAuthoring { public override void Bake(EnergyShieldAuthoring authoring) { // TransformUsageFlags 决定实体带哪些 Transform 组件 // None 表示一个都不要纯数据实体 var entity GetEntity(TransformUsageFlags.None); AddComponent(entity, new EnergyShield { HitPoints authoring.MaxHitPoints, // 编辑器不暴露初始值烘焙时直接取满 MaxHitPoints authoring.MaxHitPoints, RechargeDelay authoring.RechargeDelay, RechargeRate authoring.RechargeRate, }); } }注意GetEntity的TransformUsageFlags参数它控制这个 GameObject 对应的实体是否携带 Transform 组件这是烘焙里最常碰的配置项取值效果适用场景None不加任何 Transform 组件纯数据实体如配置、标签Dynamic加LocalTransformLocalToWorld位置会被运行时修改的实体Static只烘焙LocalToWorld世界矩阵摆放后不再移动的场景物体ManualOverride跳过 Transform 烘焙系统由自己加baker 里手工写入 Transform避免重复冲突BakingDependencies示例里每个像素实体就是用ManualOverride然后手动SetComponent(entity, LocalTransform.FromPosition(...))——因为 baker 自己已经加了 Transform如果不用ManualOverride系统又会补一份两边打架。2.2 增量烘焙怎么知道谁该重跑依赖注册结论Baker 读过的数据必须显式注册成依赖否则数据变了 Baker 不会重跑实体场景里的数据就悄悄过期了。这是烘焙系统里最容易埋雷的机制。ImageGeneratorAuthoring的 bakerBakingDependencies/ImageGeneratorAuthoring.cs把四路依赖一条条注册出来public override void Bake(ImageGeneratorAuthoring authoring) { // 以下 4 处任一变更都会触发本 baker 重跑 DependsOn(authoring.Image); // 1. 贴图本身 DependsOn(authoring.Info); // 2. ScriptableObject 资产 DependsOn(authoring.Info.Mesh); // 3. ScriptableObject 引用的 Mesh DependsOn(authoring.Info.Material); // 4. ScriptableObject 引用的 Material // 空引用检查刻意放在依赖注册*之后* // 引用丢失时注册依赖资产恢复后 baker 会自动重跑 if (authoring.Info null) return; if (authoring.Info.Mesh null) return; if (authoring.Image null) return; // 用 GetComponent 而不是 authoring.transform // 前者会注册依赖移动 GameObject 时 baker 才能重跑 var transform GetComponentTransform(); ... }依赖注册的入口有四类漏掉任何一条都会造成改了没反应注册方式覆盖的数据典型漏网场景authoring 字段自动追踪组件自身暴露的字段嵌套对象里深层引用的资产DependsOn(asset)任意资产忘记调用资产被引用链间接使用GetComponentT(go)其他 GameObject 的组件直接用authoring.transform绕过注册GetEntity(prefab, flags)被烘焙的预制体把GameObject字段直接读数据而不转实体还有一个反直觉的细节对引用丢失的资产注册依赖是期望行为。null 引用有两种来源——真没赋值和资产被删。第二种情况下DependsOn让 baker 在资产恢复后自动重跑所以空检查要放在注册之后。增量重跑的传播范围是这样的BakingDependencies示例的 README 描述了一个很能说明问题的对照改ImageGeneratorInfo里的 float 字段会重跑两个 GameObject因为两个都引用这个资产而只改 hello.png 只重跑引用它的那一个。依赖粒度是baker 实例 × 数据级别的不是整个子场景级别的。2.3 Baker 够不着的地方Baking System结论Baker 只能以单个 authoring 组件为单位工作需要跨实体、结构变更或批量处理的逻辑只能放进 Baking System。ImageGeneratorBakingSystemBakingDependencies/ImageGeneratorBakingSystem.cs展示了 Baker 与 System 之间的标准配合baker 负责登记依赖 产出原料system 负责组装成品。[WorldSystemFilter(WorldSystemFilterFlags.BakingSystem)] public partial struct ImageGeneratorBakingSystem : ISystem { EntityQuery m_ImageGeneratorEntitiesQuery; public void OnCreate(ref SystemState state) { m_ImageGeneratorEntitiesQuery SystemAPI.QueryBuilder().WithAllImageGeneratorEntity, MeshArrayBakingType().Build(); // 只处理本轮被 baker 更新过的实体响应式过滤 m_ImageGeneratorEntitiesQuery.SetChangedVersionFilter( ComponentType.ReadOnlyImageGeneratorEntity()); state.RequireForUpdate(m_ImageGeneratorEntitiesQuery); } public void OnUpdate(ref SystemState state) { // 后续循环做结构变更不能用 SystemAPI.Query() 迭代 // 改为随机访问组件值实体数量预期很少可接受 var entities m_ImageGeneratorEntitiesQuery.ToEntityArray(Allocator.Temp); foreach (var entity in entities) { var bakingEntities SystemAPI.GetBufferImageGeneratorEntity(entity) .ReinterpretEntity().ToNativeArray(Allocator.Temp); foreach (var bakingEntity in bakingEntities) RenderMeshUtility.AddComponents(bakingEntity, state.EntityManager, ...); } } }Baker 与 Baking System 的能力边界对照维度BakerBaking System运行时机烘焙过程中按 authoring 组件逐个执行所有 baker 跑完之后每次全量执行数据视野单个 GameObject 及其依赖烘焙世界里的全部实体依赖表达可声明DependsOn等不能声明自己决定跑不跑结构变更可以CreateAdditionalEntity等可以且是主要用途典型用途字段拷贝、单实体组装跨实体聚合、批量建 BlobAsset、渲染组件补齐文档里有一句值得原文引用的告诫Baking System 不应再去访问子场景的原始 GameObject。它的输入应该全部来自 baker 产出的实体数据——这也是上面例子里 baker 把生成的实体 id 塞进ImageGeneratorEntity缓冲、system 再从缓冲里取回的原因。2.4 标记组件三件套BakingType、TemporaryBakingType、Cleanup结论烘焙世界和目标世界是隔离的一份数据活在哪完全由标记决定标错就是数据丢失或泄漏。BakingTypes示例BakingTypes/BoundingBoxAuthoring.cs把三种标记用在了同一个功能里——给一组立方体算合并包围盒// BakingType活在烘焙过程里不进入目标世界 // 用途把 baker 算出的包围盒传给 Baking System [BakingType] public struct BoundingBox : IComponentData { public Entity Parent; public float3 MinBBVertex; public float3 MaxBBVertex; } // TemporaryBakingTypeBaking Systems 跑完后即被移除 // 用途标记这个实体的包围盒变了本轮用完就丢 [TemporaryBakingType] public struct Changes : IComponentData { } // ICleanupComponentData实体销毁时组件残留一拍 // 用途记住上一个父实体父实体变更/子实体销毁时 // 都能找到该重算的父包围盒 [BakingType] public struct BoundingBoxCleanup : ICleanupComponentData { public Entity PreviousParent; }CompoundBBSystem的工作方式值得细看它先给所有带BoundingBox的实体补BoundingBoxCleanup再找出带Changes标记的实体把它们的父级以及换过父级的旧父级收进changedCBBs集合只对这些父级重置并累加包围盒——典型的按变化量工作否则每次全量重跑都重算所有父盒256 个实体规模下编辑器拖一次物体就要白算一遍。标记生命周期用途反例后果[BakingType]烘焙期间存在不进目标世界baker → baking system 的数据总线忘了标中间数据泄漏进运行时实体场景[TemporaryBakingType]Baking System 跑完即删本轮变更通知误用为正式数据下一轮取不到ICleanupComponentData实体销毁后多存活一拍反查谁依赖了我不做清理清理组件无限累积2.5 BlobAsset 烘焙自动管理与手动去重结论在 Baker 里用AddBlobAsset创建的 BlobAsset 由 BlobAssetStore 自动托管和去重在 Baking System 里创建的则完全要自己管——这也是BlobAssetBakingSystem示例存在的全部理由。该示例的子场景有 256 个 GameObject按 Capsule/Cube/Cylinder/Sphere 四类共享 mesh。如果每个实体各建一份 BlobAsset同样网格的包围盒数据会被重复存几十份。示例的做法BlobAssetBakingSystem.csbaker 把顶点写进MeshVertex缓冲、把 mesh 的Hash128哈希存进RawMesh组件baking system 按哈希去重只为每个唯一哈希建一份// 收集需要新建的 BlobAsset去重 foreach (var (rawMesh, entity) in SystemAPI.QueryRefRORawMesh() .WithAllMeshBB().WithEntityAccess()) { // 哈希已登记过就直接跳过 if (m_BlobAssetReferences.TryAdd(rawMesh.ValueRO.Hash, BlobAssetReferenceMeshBBBlobAsset.Null)) { // 先查 BlobAssetStore 里是否已有上一轮建好的 if (blobAssetStore.TryGetMeshBBBlobAsset(rawMesh.ValueRO.Hash, out BlobAssetReferenceMeshBBBlobAsset blobAssetReference)) { m_BlobAssetReferences[rawMesh.ValueRO.Hash] blobAssetReference; } else { m_EntitiesToProcess.Add(entity); // 只留一个代表去计算 } } } // 用 IJobParallelFor 并行计算结果写回哈希表 new ComputeBlobDataJob { ... }.Schedule(m_EntitiesToProcess.Length, 1).Complete(); // 再把引用分发给所有实体 foreach (var (rawMesh, meshBB) in SystemAPI.QueryRefRORawMesh, RefRWMeshBB()) { var blobAssetReference m_BlobAssetReferences[rawMesh.ValueRO.Hash]; blobAssetStore.TryAdd(rawMesh.ValueRO.Hash, ref blobAssetReference); meshBB.ValueRW.BlobData blobAssetReference; }哈希来源也做了分支处理见MeshBBAuthoring.cs内置资源如默认 Cube拿不到资产依赖哈希退回GetHashCode()持久资产用AssetDatabase.GetAssetDependencyHash(assetPath)——因为mesh.GetHashCode()不会随 mesh 在编辑器外的变更而变化。维度Baker AddBlobAssetBaking System 手动管理去重BlobAssetStore 自动自己按 Hash128 查表生命周期自动引用计数实体没了就回收自己TryAdd/清理删实体要手动回收并行baker 逐个执行可上IJobParallelFor适合单实体数据一条动画曲线、一张材质参数表跨实体共享数据多实体共用网格的包围盒对比BlobAssetBaker示例里只有一行AddBlobAssetAnimationBlobData(ref blobReference, out _)就能把AnimationCurve烘成 Blob——简单场景永远优先走这条路手动管理只在规模逼你上时才值得。要点提示Baker 管单点转换Baking System 管全局收尾BakingType 管数据活在哪三者边界清晰写错一层就是运行时数据缺失或泄漏。三、实战一个最小烘焙管线的配置与调优把上面机制收拢成一份落地清单。以给场景物体烘焙碰撞包围盒为目标推荐配置顺序主场景建SubScene引用子场景在子场景里摆 GameObject 并挂 authoring 组件写 BakerGetEntity选对TransformUsageFlags读到的资产全部走DependsOn/GetComponent需要跨实体聚合时加 Baking System输入只取 baker 产出的数据用SetChangedVersionFilter做响应式过滤中间数据打[BakingType]纯烘焙期的宿主实体打BakingOnlyEntity合并进目标世界时会被丢弃共享数据用 BlobAsset单实体走AddBlobAsset多实体共享走哈希去重。调优参数逐项说明项在哪里设什么情况该调调到什么程度TransformUsageFlagsGetEntity/CreateAdditionalEntity编辑器里实体莫名多了LocalToWorld却从不移动静态物体降为Static纯数据实体用None减少序列化体积依赖注册覆盖面baker 内改了资产/预制体后实体不更新逐条审计 baker 的每个数据来源全走注册通道响应式过滤Baking System 的OnCreate烘焙全量耗时高、实体上万SetChangedVersionFilter只处理本轮变化的实体配合RequireForUpdate让无变化时整个系统跳过EntityPrefabReferenceRequestEntityPrefabLoaded同一预制体被多个子场景直接烘焙产物重复多场景复用改走预制体引用一份烘焙产物多场景实例化见 PrefabReference/LoadPrefabSystem.csSceneSection烘焙期设置场景太大、加载峰值高拆 section 分批加载但注意 section N 的实体只能引用同 section 或 section 0 的实体BlobAsset 去重键baking system 哈希表大量实体共享同一份数据用资产依赖哈希而非GetHashCode()保证资产变更后引用可刷新运行时侧Boids 演示就是这套流水线产出的实体数据在跑——实体布局、系统调度全部来自烘焙结果编辑器里改完子场景运行时的行为随之变化加载实体场景永远走异步路径SceneSystem.LoadSceneAsync不要在加载完成状态上写死时序——官方文档明确建议检查数据存在与否而不是检查场景加载状态这样数据换来源换场景、走网络下发代码不用改。要点提示默认值Dynamic、无过滤、不去重在小场景下都能跑规模上去后优先动依赖覆盖面和响应式过滤这两项收益最大。四、高频问题与排查清单按踩坑频率排序改了贴图/预制体实体没更新。九成是依赖没注册baker 里读了authoring.transform而不是GetComponentTransform()或者资产是嵌套对象里间接引用的。排查顺序baker 里每个数据来源 → 是否走了注册通道 → 用DependsOn补上。baker 手动加的 Transform 和系统补的 Transform 冲突。给实体用TransformUsageFlags.ManualOverride让 Transform 烘焙系统闭嘴LocalTransform/LocalToWorld全部由你自己SetComponent。Baking System 里建的 BlobAsset 越积越多。Baker 路径的 BlobAsset 有引用计数自动回收system 路径没有。实体删除时要手动从 BlobAssetStore 清理旧引用BlobAssetBakingSystem示例的 README 专门写了这条否则编辑期反复重烘焙会持续泄漏。中间数据出现在运行时实体里。检查是否漏标[BakingType]——没标的组件会跟着实体一起进目标世界反之内敛的数据该留的反而被丢了要确认标记和期望一致。同一预制体在 N 个子场景里烘出 N 份实体。直接把 prefab 摆进每个子场景烘焙产物天然重复改用EntityPrefabReferenceRequestEntityPrefabLoaded让多个子场景指向同一份烘焙产物运行时再实例化。Baking System 里迭代查询同时做结构变更数据错乱。结构变更期间不能用SystemAPI.Query()迭代当前查询集要么ToEntityArray随机访问要么换 CommandBufferImageGeneratorBakingSystem用了前者并注释了原因。五、小结烘焙 每 GameObject 建实体 → Baker 逐个转换 → Baking System 全量收尾产物是运行时异步加载的实体场景Baker 的数据来源必须全部走注册通道DependsOn/GetComponent/GetEntity漏一条就是一次改了没反应[BakingType]、[TemporaryBakingType]、ICleanupComponentData决定数据活在烘焙期还是运行时标错即泄漏或缺失BlobAsset 去重单实体用AddBlobAsset自动托管跨实体共享用 Hash128 手动查表 并行 Job性能杠杆TransformUsageFlags降配、SetChangedVersionFilter响应式过滤、EntityPrefabReference跨场景复用延伸阅读官方文档EntitiesSamples/Docs/baking.md示例总览EntitiesSamples/Assets/Baking/README.md基础 baker 写法EntitiesSamples/Assets/ExampleCode/Baking.cs依赖追踪完整实现EntitiesSamples/Assets/Baking/BakingDependencies/BlobAsset 并行烘焙实现EntitiesSamples/Assets/Baking/BlobAssetBakingSystem/【免费下载链接】EntityComponentSystemSamples项目地址: https://gitcode.com/GitHub_Trending/en/EntityComponentSystemSamples创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表