ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Unity DOTS核心:彻底理解World架构与多World协作

2026/9/9 15:23:54 拓冰建站 浏览量
Unity DOTS核心:彻底理解World架构与多World协作 如果你刚接触Unity DOTS八成会因为World这个词陷入沉思。我初学时就卡在这上面很久明明Entity才是实体System才是逻辑为什么所有文档都在强调World当时照着示例代码折腾了一个星期项目里莫名其妙出现了好几个WorldSystem执行了两遍数据怎么查都是空的最后才反应过来——我对DOTS整套架构的理解顺序就错了。其实World才是整个ECS架构的真正根节点Entity、Component、System都只是它的“住户”。这篇博文我就把World这件事彻底讲透。会从它的本质定义出发拆解它内部怎么管理Entity、Component和System再讲默认World的行为规则、怎么创建自定义World、多World之间怎么协作最后分享一些调试和排查的实用经验。适合两类人一是刚接触DOTS、被World这个概念卡住的初学者二是已经在项目里用上了ECS、但对World的底层机制还停留在“能用就行”阶段的进阶开发者。我尽量把每个结论背后“为什么是这样”也一起讲清楚因为现实中你踩的坑多半不是API不会调而是不理解设计者的意图。1. World到底是什么——把它当成“独立小宇宙”而不是“管理器”先给结论World是ECS中所有Entity、ComponentData和System的容器是它们唯一的生存空间。一个Entity离开了World就毫无意义一个System如果不挂在某个World里就不会被任何机制驱动。我通常用一个类比来理解它每个World就像一整套独立运行的“建筑”——Entity是房间里的人ComponentData是每个人手里的物品和属性卡System则是物业管理处和住户的行动规则。不同的World就是不同的建筑里面的人、物品、规则各自独立互不干涉。这个设计和传统的GameObject/Component架构差别非常大。你写传统Unity时场景里的GameObject天然属于当前Scene大部分情况下只有一个场景在跑游戏对象之间的交互逻辑靠“引用”彼此连接。而DOTS的设计思路是“面向数据”——它想让所有实体数据连续排列在内存里让系统批量遍历时能利用CPU缓存。要实现这一点就必须有一个清晰的边界来划定“哪些数据归谁管”这个边界就是World。所以World不是一个“管理器”不是一个服务提供者它不是用来“帮你看管”实体列表的。它是一个执行容器Entity数据存放在World的EntityManager里System实例属于WorldSystem的代码里访问到的EntityQuery也是在World层面注册的。你可以把World理解为ECS世界里唯一合法的“上下文”。只要你在某个World里创建了一个Entity这个Entity的所有组件数据都记录在这个World的EntityManager中其他World根本无法感知它的存在。还有一个容易忽略的点同一个World内部Entity和Component才有意义跨World之后Entity ID完全随机即使两个World里的Entity ID碰巧相同它们之间也没有任何关系。这个特性既是优势也是陷阱。优势在于多个World可以完全隔离互不污染比如网络同步的计算世界和表现渲染的世界分开各自独立更新陷阱在于很多新手会下意识地把其他World的Entity引用拿过来用结果发现数据根本不存在。从架构层级来看World处于ECS的最顶端World内部拥有一个EntityManager负责所有实体和组件数据的创建、销毁、增删该组件World内部挂着一堆System它们按固定的Update Order被驱动执行World内部有EntityQuery的缓存和状态管理World自身有一个名字和一组Flags用来标识它的用途。用一张不严谨但好记的图来概括World独立宇宙 ├── EntityManager实体与数据的管理者 │ ├── Entity实体ID仅是一个索引 │ └── ComponentData挂在实体上的数据 ├── System列表 │ ├── System A读取 ComponentData 做变换 │ ├── System B根据输入生成新的实体 │ └── System Group把多个系统组织成一组统一更新 └── WorldFlags / 生命周期状态明白这个层级之后很多问题就豁然开朗了。比如你创建了Entity但看不到它第一反应应该是它被创建在哪个World里你查询的System又挂在哪个World里两个World不同当然查不到。又比如你写了一个System怎么都不跑那就要去确认这个System所属的World是否被加入了PlayerLoop更新循环。这些问题后面我会逐一展开。2. 解密World内部的三位居民Entity、ComponentData、System如何协同运转2.1 Entity不是物体是“门牌号”在World里Entity本身不是一个对象不带任何数据。它本质上是EntityManager内部Archetype和Chunk索引的一个包装。你可以把Entity理解成一张门牌号它只有一个ID告诉你“这户人家住在哪一片区域”。真正“住在里面”的是ComponentData。刚接触ECS的人最容易在这里栽跟头创建一个Entity、AddComponent、SetComponentData然后把这个Entity当对象传来传去。但Entity其实只是一个纯值类型它不携带任何业务逻辑也不保存任何状态。你说“我有一个Entity”本质上只是说“我拿到了一个能定位到某些数据块的索引”。World里的EntityManager维护着从Entity ID到实际数据存储位置的映射。这种设计的好处是数据可以紧凑地连续排布在内存里System批量处理时能享受CPU缓存的优势。坏处是对Entity的直接引用非常脆弱它在不同World之间几乎不可传递。2.2 ComponentData真正的数据本体ComponentData是挂在Entity上的数据在ECS里分为好几种组件类型说明典型用途IComponentData最基本的组件类型值类型数据位置、速度、血量ISharedComponentData多个Entity共享的引用类型数据材质、碰撞体引用IBufferElementData动态缓冲元素类似数组元素子弹列表、路径点IEnableableComponent可启停的组件标记“是否激活”说一个让很多新手困惑的点在传统Unity里Component通常携带行为Update、OnCollision之类的生命周期方法但在ECS中ComponentData只是纯数据一点逻辑都不带。这其实是刻意设计的。传统面向对象的思维是“数据方法内聚成一个类”ECS的思路是“方法独立出来放进System数据单独存放”。所以在World内部组件数据并不是按Entity一个一个堆在一起的而是按Archetype实体结构模板分成不同块Chunk。同样一组组件类型的Entity被放在同一块内存区域里System遍历时只需连续扫过这块区域效率极高。2.3 SystemWorld里的“行动规则”System是逻辑的载体。它不保存实体状态只负责读取World里的ComponentData、执行计算、写入结果。一个World可以包含任意数量的System这些System由SystemGroup组织起来按顺序执行。在Unity Entities 1.x版本里主要有两种System编写方式SystemBase类模式常用using Unity.Entities; using Unity.Burst; public partial struct RotationSystem : ISystem { [BurstCompile] public void OnUpdate(ref SystemState state) { foreach (var (transform, speed) in SystemAPI.QueryRefRWLocalTransform, RefRORotationSpeed()) { transform.ValueRW transform.ValueRO.RotateY(speed.ValueRO.Value * SystemAPI.Time.DeltaTime); } } }ISystem结构体模式新推荐using Unity.Entities; using Unity.Burst; [BurstCompile] public partial struct RotationSystem : ISystem { [BurstCompile] public void OnCreate(ref SystemState state) { } [BurstCompile] public void OnUpdate(ref SystemState state) { float dt SystemAPI.Time.DeltaTime; foreach (var (transform, speed) in SystemAPI.QueryRefRWLocalTransform, RefRORotationSpeed()) { transform.ValueRW transform.ValueRO.RotateY(speed.ValueRO.Value * dt); } } }注意上面的System并没有显式声明“我在哪个World”。这个问题稍后我会专门讲System所属World不是由代码决定的而是由创建它的地方决定的。这一点非常关键。2.4 EntityManagerWorld的“大管家”前面提到World的EntityManager管理着所有实体和组件数据但它还有一个角色——是System获取数据和执行操作的主要入口。在System里你会频繁用到EntityManager、EntityQuery、Archetype、ComponentLookupT这些对象。它们都是从SystemState或World中获取的。换句话说System的每次数据访问都隐含着一个“World上下文”。如果你在System里拿EntityManager去查询数据查的就是自己所属World的数据。这也是DOTS能实现高度并行化和安全性的基础——System知道自己在哪个World里数据访问边界是明确的。3. 默认World的幕后规则——那些不配置就会踩的坑在Unity中你还没有写任何代码项目里其实就已经自动创建了一个默认World。这是DOTS通过DefaultWorldInitialization在进入Play模式时干的事。它有几个关键规则不搞清楚的话你会遇到很多莫名其妙的问题。3.1 默认World是怎么来的Unity在进入Play模式时会扫描程序集中所有继承自ComponentSystemBase或实现ISystem的类型然后自动把它们创建进默认World里。它还自动创建了三个内置SystemGroupInitializationSystemGroup初始化阶段适合做数据准备比如生成实体 ├── BeginInitializationEntityCommandBufferSystem ├── ... └── EndInitializationEntityCommandBufferSystem SimulationSystemGroup模拟阶段适合做游戏逻辑比如移动、战斗 ├── BeginSimulationEntityCommandBufferSystem ├── ... └── EndSimulationEntityCommandBufferSystem PresentationSystemGroup表现阶段适合做渲染相关同步 ├── BeginPresentationEntityCommandBufferSystem └── EndPresentationEntityCommandBufferSystem这就是为什么你写了一个System不用手动注册进入Play模式它就会自动跑。因为默认规则会把当前程序集里所有System都创建到默认World里并且挂在合适的SystemGroup下面。说到SystemGroup科普一下SystemGroup是一个特殊的System它本身不执行逻辑它的职责是按顺序更新一组子System。比如SimulationSystemGroup内部会依次调用它的子System的OnUpdate。这种层次结构给了你极大的控制力——你可以编写一个自定义SystemGroup把一系列System按依赖顺序塞进去再决定整个组什么时候跑。3.2 常见坑一一键创建的System为什么会执行两遍这是新手最常踩的坑之一。你可能写了一个System放在了默认World里然后又在代码里手动创建了一个同样的System还加到了另一个自定义World里。结果就是同一个System逻辑被同时执行了两遍。你自己还一脸懵“我没写两遍啊”其实System只对创建它的World负责。默认World会自动扫一遍程序集如果你在别处又手动创建了一份那自然会在多个World里都存活。排查方法很简单打印World.All列表看看里面有几个World、每个World里有哪些System。后面我会写怎么用代码做这件事。3.3 常见坑二修改脚本后System跑了两份DOTS在代码热重载Domain Reload时如果World的生命周期没有处理干净经常会出现World残留。表现就是Play模式进去后World数量比预期多System也重复创建。这个问题在Unity 2021之后有了改进但如果你手动创建过World且没有在退出时销毁依然会踩坑。解决方式是确保在退出Play模式时销毁你手动创建的World。通常放在OnDisable或[RuntimeInitializeOnLoadMethod]配套逻辑里处理。也可以在创建World时存下引用监听Application.quitting或播放模式退出事件及时调用World.Dispose()。3.4 常见坑三EntityManager不是全局单例很多教程代码里会看到World.DefaultGameObjectInjectionWorld.EntityManager这种写法。请一定记住这只是默认World的EntityManager它不是全局的。如果你的逻辑在自定义World里执行却通过默认World的EntityManager去创建Entity结果会是你创建出来的实体和你的System根本不在同一个World里System当然不会处理它。正确的做法是在System内部永远使用SystemAPI.GetSingletonEntityT()、state.EntityManager这类带上下文的方式拿到EntityManager在System外部明确指定你要操作哪个World。实在需要全局操作的才用World.DefaultGameObjectInjectionWorld而且要清楚它的边界。4. 从零创建一个自定义World完整实操与更新链路的断开前面讲了很多默认行为。现在我来演示一下如果默认World满足不了需求怎么自己造一个“独立小宇宙”。我有一次做服务端战斗模拟需要把底层战斗计算和Unity表现层彻底隔离默认World自动创建的System根本没法满足需求于是手动创建了一个专门的ScholarshipWorld。那次经历让我彻底理解了World生命周期管理的细节。4.1 创建World与添加System首先创建World并添加System的代码很简单using Unity.Entities; using UnityEngine; public class CustomWorldExample : MonoBehaviour { private World simulationWorld; void Start() { // 1. 创建一个空的World simulationWorld new World(SimulationWorld); // 2. 在World里创建一个SystemGroup用来管理我们的模拟系统 var simulationGroup simulationWorld.CreateSystemManagedSimulationSystemGroup(); // 3. 创建具体的System假设CombatSystem、MovementSystem都是SystemBase子类 simulationWorld.CreateSystemManagedCombatSystem(); simulationWorld.CreateSystemManagedMovementSystem(); // 4. 把SystemGroup挂到当前PlayerLoop中 ScriptBehaviourUpdateOrder.AppendWorldToCurrentPlayerLoop(simulationWorld); } void OnDestroy() { if (simulationWorld ! null simulationWorld.IsCreated) { // 销毁World时它内部所有Entity、System都会被回收 simulationWorld.Dispose(); simulationWorld null; } } }这里的几个动作值得逐一说清楚new World(SimulationWorld)创建了一个带名字的空World。它自带EntityManager但没有任何System。CreateSystemManagedSimulationSystemGroup()在World内部创建System。这一步非常关键System的“所属World”是由这行代码决定的不是你写在类上的任何字段。ScriptBehaviourUpdateOrder.AppendWorldToCurrentPlayerLoop(simulationWorld)把整个World的PlayerLoop更新接入Unity主循环。如果不加这行World里的System即便创建了也永远不会被驱动更新——它们只存在于内存里一动不动。这个知识点很有用你想手动控制某个World的更新时机就故意不把它加到PlayerLoop而是自己调用simulationWorld.Update()下面细说。4.2 手动控制更新时机脱离PlayerLoop的World在某些场景下你可能不想让World跟着Unity的PlayerLoop走。比如网络同步的固定步长模拟比如回合制游戏的“只有需要时才更新”。这时候就要绕开PlayerLoop手动驱动World。public class ManualFixedStepSimulation : MonoBehaviour { private World simulationWorld; private SimulationSystemGroup simulationGroup; private float timer; void Awake() { simulationWorld new World(FixedStepWorld); simulationGroup simulationWorld.CreateSystemManagedSimulationSystemGroup(); simulationWorld.CreateSystemManagedBattleSystem(); // 注意不调用 ScriptBehaviourUpdateOrder.AppendWorldToCurrentPlayerLoop } void Update() { timer Time.deltaTime; // 固定每0.1秒模拟一次和渲染帧率完全脱钩 if (timer 0.1f) { timer 0f; simulationWorld.SetTime(new TimeData(0.1f, Time.time)); simulationGroup.Update(); simulationWorld.SetTime(default); } } void OnDestroy() { if (simulationWorld ! null simulationWorld.IsCreated) { simulationWorld.Dispose(); } } }这里有一个非常重要的细节如果不调用simulationWorld.SetTime(...)World内部的时间SystemAPI.Time默认是零你所有基于DeltaTime的逻辑都会原地不动。手动驱动World时一定要在每次Update前设置时间。我第一次手动驱动的时候就忘了这茬结果所有单位都不动排查了半天才发现是DeltaTime恒为0。4.3 销毁World的正确姿势World的销毁比创建更容易踩坑。执行World.Dispose()时它内部所有EntityManager中的数据、所有System实例、所有EntityQuery的缓存都会被释放。如果你还引用了这个World里的Entity或组件数据之后再去访问就会拿到无效数据。我说几个我自己踩过的坑World.Dispose之后不能再访问world.EntityManager。如果你想在销毁前清理某些东西请先保存EntityManager引用在Dispose之前完成操作或者干脆别在销毁后持有这个World的任何成员。如果World里的System持有非托管资源请先手动释放。比如System里申请过NativeArray、NativeList要在System的OnDestroy或OnStopRunning中释放否则World.Dispose时可能不会帮你清理干净容易引发内存泄漏。退出Play模式时要统一销毁。如果你在多个脚本里创建了多个World建议用一个静态列表统一管理在播放模式退出时遍历并Dispose。别依赖Unity自动清理尤其在开启Domain Reload关闭的情况下残留World会越来越多。public static class CustomWorldRegistry { public static ListWorld AllCustomWorlds new ListWorld(); public static World Create(string name) { var world new World(name); AllCustomWorlds.Add(world); return world; } [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void OnSubsystemRegistration() { // 进入Play模式时清理上一次残留 foreach (var world in AllCustomWorlds) { if (world ! null world.IsCreated) world.Dispose(); } AllCustomWorlds.Clear(); } }这个模式虽然简单但能极大降低在编辑器中反复进出Play模式导致World残留的概率。4.4 自定义World的典型应用场景手动创建World不是炫技它解决的是真实问题。我见过的几种典型用法服务端模拟与客户端表现分离用无渲染的Pure ECS World跑战斗模拟表现层World读取模拟结果并通过正常ECS渲染。多局对战隔离每张地图、每个房间一个World切换房间时只需要Dispose旧World、创建新World不需要手动清空一堆Entity。编辑器工具世界在编辑器里创建一个临时World来跑批量数据转换不污染正在编辑的场景。独立AI计算世界让AI逻辑的World以更低帧率更新与主渲染World分开方便控制性能。5. 多World共存与跨World协作Entity传不过去怎么办多World并行是DOTS的高级玩法但很多人在真正需要多World协作时会被一个基础问题卡住World A里的Entity怎么让World B来处理先给出一个你应该深深刻在脑子里的结论Entity不能跨World直接引用。World A创建的Entity ID在World B里毫无意义即使某个Entity ID碰巧一样它指向的数据也完全不同。数据要跨World传递必须走拷贝或迁移这条路。5.1 数据拷贝CopyEntitiesFrom和MoveEntitiesFrom如果你只是想把数据复制到另一个World可以用EntityManager.CopyEntitiesFromvar sourceWorld World.DefaultGameObjectInjectionWorld; var targetWorld customWorld; using (var srcQuery sourceWorld.EntityManager.CreateEntityQuery(typeof(PlayerTag))) { targetWorld.EntityManager.CopyEntitiesFrom(sourceWorld.EntityManager, srcQuery); }这个API会把源World中符合条件的Entity连同所有组件数据复制到目标World。拷贝出来的Entity是全新的ID和源World没有任何关联。如果你想要真正的“搬家”——源World里删除、目标World里新增就得用MoveEntitiesFrom。这个API会把Entity从源World迁移到目标World包括它们的Archetype和ComponentData并且保证迁移后的Entity在源World不再存在。// 将源World里的所有实体都迁移到目标World targetWorld.EntityManager.MoveEntitiesFrom(sourceWorld.EntityManager);注意MoveEntitiesFrom有诸多限制。比如源World如果正在被某些System使用迁移时会遇到写入冲突。实际项目中最好在固定状态下例如源World停止更新时执行迁移否则容易触发异常。5.2 跨World通信系统之间的桥梁除了数据拷贝两个World里的System也需要通信。常用的几种方式共享静态对象定义一个静态类作为消息总线World A的System把结果写进去World B的System去读取。这是最简单的方案但要注意线程安全别在Job里乱写静态字段。单例组件如果你用默认World做表现自定义World做逻辑可以把“表现层要显示的数据”打包成一个组件放到“表现层World”里。但本质上这依然是数据拷贝。ExternalEntity让一个Entity同时存在于多个World用外部Entity ID关联。这个方案偏hack适合老项目迁移新人不要轻易用。一种我在项目中验证可行的做法是用一个“事件World”只存放跨世界通信的请求实体每个World的System在Update时检查这个事件World里的待处理数据。虽然多了一次数据拷贝的代价但边界很清晰不需要在System里做复杂的静态状态管理。5.3 World选择和并发更新的注意事项多World并行更新时要记住每个World的System更新是独立的PlayerLoop节点。Unity会尝试并行执行不同的World更新吗实际上不会——默认情况下所有World的更新都跑在主线程上除非你在System内部用Job或EcsJobSystem并行化。我见过有人为了让两个World并行自己在多线程里调用world.Update()结果在Unity的PlayerLoop之外执行更新会引发大量竞争条件。这不是Unity推荐的用法。如果你确实想要并行计算正确的方式是在System内部用Job System让单个System在内部利用多线程并行处理大批量实体而不是把多个World的更新放到不同线程。6. World查询与调试用代码找到你想要的System和Entity调试World相关的问题最重要的是有办法“看见”World内部状态。Unity提供了Entities Tools窗口Windows Entities Hierarchy 和 Component System但有时候在代码里打印信息更快更精准。6.1 遍历所有Worldusing Unity.Entities; using UnityEditor; public static class WorldDebugUtil { public static void LogAllWorlds() { foreach (var world in World.All) { UnityEngine.Debug.Log($World: {world.Name}, IsCreated: {world.IsCreated}, Flags: {world.Flags}); } } public static void LogSystemsInWorld(World world) { if (world null || !world.IsCreated) return; foreach (var system in world.Systems) { UnityEngine.Debug.Log($ System: {system.GetType().Name}, UpdateBefore/After: {system.UpdateBefore?.GetType().Name ?? None}); } } }在System里的调试我会用下面这种方式protected override void OnUpdate() { // 查看当前System所在World的信息 Debug.Log($World: {World.Name}); }6.2 查找System并手动触发更新有一个非常常见的问题我想在某个外部脚本里临时让一个System运行怎么找到它var world World.DefaultGameObjectInjectionWorld; var query world.GetExistingSystemMySystem(); if (query ! null) { // SystemBase可以直接调用UpdateISystem需要用world.GetExistingSystemMySystem()拿到句柄再通过SystemHandle调用 query.Update(); }注意GetExistingSystemT()只会返回当前World里存在的System不会跨World搜索。如果你在多个World里都创建了同一个System你需要分别去每个World里查找。6.3 定位“Entity去了哪里”的排查链路当你在DOTS项目里遇到“实体找不到了”的情况按下面的链路排查基本能定位先确认World数量用World.All打印所有World的名字和数量。确认Entity属于哪个World在创建Entity时打印创建它的World名。最方便的办法是创建一个临时组件比如CreateSourceWorldTag把World名写进去。确认System是否属于同一个World在System的OnUpdate里打印World.Name和上面实体的World名对照。确认System的更新是否被驱动如果System一直不执行检查它的World是否被加入了PlayerLoop。确认查询条件是否匹配System里的查询和实际组件的Archetype是否匹配。新手最常见的错误是查询里写了几个组件但实际上实体缺少其中一个组件导致查询结果为空。6.4 利用World状态排查重复System问题我遇到“同一个System执行两遍”时最常用的排查代码public static int CountSystemInstancesT() where T : ComponentSystemBase { int count 0; foreach (var world in World.All) { if (world.IsCreated world.GetExistingSystemT() ! null) { count; Debug.Log($找到System: {typeof(T).Name} 位于: {world.Name}); } } return count; }如果输出数量大于1那肯定是在多个World里各自创建了一份。这时候要么修改创建逻辑让它只在目标World里创建要么通过WorldFlags或者自定义Attribute过滤默认World的自动扫描逻辑。7. World状态管理和Flag什么时候该用WorldFlags关于World还有一个容易被忽略的东西——WorldFlags。它标记了World的类型和生命周期特征比如WorldFlags.GameObjectProxy表示这个World会挂上GameObject代理的组件同步逻辑。WorldFlags.Editor编辑器专用World主要用于场景预览和编辑器序列化。WorldFlags.Conversion在GameObject转Entity转换时使用。大部分情况下你不需要手动设置WorldFlags但如果你在做编辑器工具或者特殊运行时了解它的存在可以帮你免掉一些奇怪的坑。比如我在做编辑器批量转换工具时手动创建的World没有设置WorldFlags.Conversion导致某些转换系统判断World类型时走错了分支数据转换结果不正确。后来显式加了对应Flag才解决。创建带Flag的World的方式var world new World(EditorConversionWorld, WorldFlags.Conversion);这个细节很冷门但如果你真的走到编辑器工具这一层会感谢自己看过这段。8. 版本差异和迁移Entities 1.0前后的World API变化如果你在网上搜World相关教程会发现很多老代码和你项目里的API对不上。这是因为DOTS在2022年到2023年间经历了大版本变化尤其是Entities 1.0正式版发布之后API做了很多调整。我整理了一下最常见的差异项目Entities 0.x旧Entities 1.x新System基类ComponentSystemBase/JobComponentSystemSystemBase/ISystem创建Systemworld.CreateSystemT()world.CreateSystemManagedT()SystemBase或world.CreateSystemT()ISystem销毁Worldworld.Dispose()相同但ISystem的Unmanaged World释放要更小心实体位置组件Translation/RotationLocalTransformQuery创建GetComponentGroupCreateEntityQuery或SystemAPI.Query主世界访问World.ActiveWorld.DefaultGameObjectInjectionWorld对于迁移中的项目我最大的建议是不要混用新旧API在同一个System里要么全用SystemBase要么全用ISystem否则差异会让你崩溃。ISystem因为是结构体可以Burst编译到极致但它没有OnDestroy这种生命周期方法它用的是OnDestroy(ref SystemState state)而且它不能保存引用类型字段很多新手一上来就直接踩坑。从版本角度再补充一点在Entities 1.0中World.Active已经废弃统一使用World.DefaultGameObjectInjectionWorld。但请注意这个“默认注入World”不一定等于你手动创建的World很多人把两者混淆结果在不同World之间操作数据搞出一堆灵异现象。如果项目只有默认World那确实可以直接用但凡涉及自定义World一定要明确引用哪个World。9. 关于World我最后想说的几句话这一路写下来覆盖了World从诞生到销毁的各个方面。最后分享一点个人经验DOTS之所以难上手参考文档少只是表面原因更深的坑在于它改变的不是某几个API而是整套思维方式。World这个概念本身并不复杂它就是一个“数据逻辑的专有空间”复杂的是你之前习惯了全局变量、习惯了GameObject.Find、习惯了单例管理器来访问跨模块的东西而World硬要你给所有数据一个明确的归属边界。我踩过最大的一次坑是在一个项目中同时用了默认World、自定义模拟World和编辑器转换World结果三个World里各自有一套非常相似的System。当时为了调试“某个敌人为什么不移动”整整查了两天最后定位到是模拟World里的移动System根本没被创建成功而表现World里的System一直试图读取模拟World的实体数据自然什么都读不到。后来我给自己定了一条规矩每个System在OnCreate时打一行日志记录自己所在的World名和System名这些日志在项目前期帮了大忙。如果你也准备在项目里认真用DOTS第一周不急着写业务逻辑先把World的创建、查询、销毁、跨World数据同步这些“地基”跑透。地基稳了后面写ECS代码会顺畅很多。真要到了排查问题时请记住第一件事永远是问自己当前这个Entity、这个System、这个Query到底属于哪个World答案明确了问题通常就解了一半。