
1. 项目概述为什么我们要告别 MonoBehaviour如果你在 Unity 里做过稍微复杂点的项目尤其是那种有成百上千个需要每帧更新逻辑的物体时大概率遇到过这个问题游戏跑得好好的突然就卡一下帧率FPS像坐过山车一样往下掉。你打开 Profiler 一看CPU 主线程上堆满了密密麻麻的MonoBehaviour.Update调用它们像一堵墙把渲染、物理这些后续工作都堵在后面。这就是经典的 MonoBehaviour 卡顿问题也是我们今天要聊的 Unity DOTS 技术栈最直接的“入场券”。MonoBehaviour 是 Unity 传统工作流的核心它简单、直观让开发者能快速上手。但它的设计哲学是面向对象的每个 GameObject 都是一个独立的“小王国”挂载着各种组件Component。这种设计在项目规模扩大后会暴露出几个致命弱点直接导致性能瓶颈。首先就是内存访问模式低效。想象一下CPU 要去处理一千个敌人的 AI 逻辑这些敌人的数据位置、血量、状态分散在内存的各个角落。CPU 读取数据就像去一个杂乱无章的仓库取货每次都要跑很远效率极低。这就是所谓的“缓存命中率”低下CPU 大量时间花在了等待数据从内存加载上而不是实际计算。其次是单线程的枷锁。默认情况下所有 MonoBehaviour 的Update都在主线程上顺序执行。一个复杂的 AI 计算卡住了后面所有的物体更新、输入处理、UI 响应都得等着。在现代多核 CPU 成为标配的今天这种工作方式无异于“一核有难多核围观”是对硬件资源的巨大浪费。最后是GC垃圾回收的压力。频繁地实例化、销毁 GameObject或者在Update里大量使用new关键字创建临时对象比如new Vector3()都会产生大量的托管内存垃圾。Unity 的 Mono/.NET 运行时需要定期暂停所有线程来进行垃圾回收这个“世界暂停”的时刻就是玩家感受到卡顿的罪魁祸首之一。而 DOTSData-Oriented Technology Stack数据导向技术栈就是为了从根本上解决这些问题而生的。它不是一个单一的功能而是一套完整的技术栈核心思想是“数据与行为分离”和“面向数据的设计”。简单说它不再关心“敌人”这个对象而是关心“所有需要移动的数据”在哪里然后一次性用最高效的方式处理它们。这带来了几个立竿见影的好处极高的缓存利用率、天然的多线程并行能力以及通过值类型和 Burst 编译器几乎消除 GC 压力。网上有测试显示在相同逻辑下遍历处理 2000 个实体DOTS 的 ECS 架构能比传统的 MonoBehaviour 方式快上 8 到 12 倍。这个差距在移动端、WebGL 或者追求高帧率的竞技游戏中就是“能玩”和“不能玩”的区别。所以“告别 MonoBehaviour 卡顿”不是一个口号而是项目性能触及天花板后一个必须认真考虑的技术升级路径。2. DOTS 核心概念快速入门与避坑要点刚接触 DOTS你可能会被一堆新名词搞晕ECS、JobSystem、Burst Compiler、Entity、ComponentData、System…… 别慌我们先把它们的关系和最容易踩的坑理清楚。2.1 ECS 架构Entity, Component, System 三位一体你可以把 ECS 看作一种全新的组织代码和数据的方式它和我们熟悉的面向对象OOB有本质区别。Entity实体它不是一个“对象”而是一个极其轻量级的 ID可以理解为数据库里的一行唯一索引。它本身不包含任何数据或逻辑只用于标识和关联。在 Unity 编辑器中你看到的那个小圆点就是 Entity 的视觉化表示。ComponentData组件数据这才是数据的真正载体。它是一个纯粹的数据结构在 C# 里是struct只包含字段没有方法。例如Translation位置、Rotation旋转、Health血量。多个 ComponentData 可以附加到同一个 Entity 上共同描述这个实体的状态。关键点ComponentData 是值类型struct存储在连续的内存块Chunk中这是高性能的基石。System系统系统是行为的执行者。它不“拥有”任何实体而是通过查询Query来寻找所有拥有特定组合 ComponentData 的实体然后对它们的数据进行批量处理。例如一个MovementSystem会查询所有拥有Translation和MoveSpeed组件的实体然后在一个 Job 里并行地更新它们的位置。第一个大坑试图用面向对象的思想写 ECS。这是新手最容易犯的错误。比如总想给一个“敌人实体”单独发消息或者在一个 System 里处理非常复杂的、独属于某个实体的逻辑。在 ECS 里你要习惯“群体思维”。你的代码应该是“对所有有 A 和 B 数据的实体执行 C 操作”。如果你发现某个逻辑很难用这种模式描述可能需要重新思考数据的设计。2.2 JobSystem 与 Burst Compiler并行的魔法JobSystem 是 Unity 提供的用于编写安全、高效多线程代码的框架。你可以创建 Job一个结构体在里面定义要并行执行的工作然后调度它。它的核心安全机制是通过[ReadOnly]和[WriteOnly]等属性声明数据的访问权限防止多线程竞争。Burst Compiler 则是一个 LLVM 后端的编译器它能把你的 Job 代码编译成高度优化的本地机器码。经过 Burst 编译的代码运行速度通常会有数量级的提升并且由于它严格限制托管对象的使用能有效避免 GC 分配。第二个大坑在 Job 中访问托管对象。Job 里只能操作 NativeContainer如NativeArray或 Blittable 类型的值类型。如果你在 Job 里尝试读取一个ListGameObject或者调用一个 UnityEngine.Object 的方法编辑器会报错或者运行时崩溃。所有需要的数据必须在调度 Job 前预先转换并复制到 NativeArray 这样的原生容器中。第三个大坑忽视 Job 的依赖关系。如果 Job A 需要读取 Job B 写入的数据你必须显式地管理它们的依赖关系通常通过JobHandle.CombineDependencies和将前一个 Job 的JobHandle传递给后一个 Job 的Schedule方法来实现。依赖管理混乱会导致数据竞争或难以调试的随机错误。2.3 混合模式如何与现有的 GameObject 世界共处完全重写现有项目为纯 DOTS 是不现实的。Unity 提供了强大的混合模式支持让你可以渐进式地迁移。通过GameObjectEntity或ConvertToEntity这是最简单的桥梁。给你一个现有的 GameObject 挂上ConvertToEntity组件或旧版的GameObjectEntity在运行时或烘焙Baking时它会自动将 GameObject 上的特定组件如Transform转换为 ECS 的LocalTransform等并生成一个对应的 Entity。这样这个实体既可以被传统的MonoBehaviour系统影响虽然效率不高也可以被 ECS 的 System 处理。EntityManager与World你可以通过World.DefaultGameObjectInjectionWorld.EntityManager来获取默认世界的实体管理器从而在 MonoBehaviour 中创建、查询或销毁实体。反过来在 System 中虽然不能直接操作 GameObject但可以通过EntityManager添加或设置共享组件如RenderMesh来影响渲染。第四个大坑滥用混合模式导致性能反而下降。混合模式是桥梁不是终点。如果你在 MonoBehaviour 的Update里每帧都通过EntityManager去获取或修改大量实体的数据那么你不仅没享受到 ECS 的好处反而增加了 MonoBehaviour 与 ECS 之间数据同步的开销。正确的做法是在 MonoBehaviour 中只进行低频次的操作如玩家输入、游戏状态切换或者将数据“推送”到 ECS 可以读取的 NativeContainer 中然后由 ECS 的 Job 进行高频次的批量处理。3. Entity Debugger 实战你的 DOTS 显微镜当你开始编写 DCS 代码后很快就会遇到一个挑战我怎么知道我创建的实体长什么样我的 System 到底查询到了哪些实体数据对不对这时候Entity Debugger 就是你不可或缺的“显微镜”和“调试器”。它不是一个预设的窗口而是需要你通过Window Analysis Entity Debugger手动打开。3.1 界面详解与信息解读打开 Entity Debugger你会看到几个主要面板World 列表显示当前所有的 World。通常你会看到DefaultWorld和ConversionWorld用于 SubScene 烘焙。选中一个 World 进行查看。System 列表显示该 World 中所有已注册的 System以及它们上一帧的执行时间。这是性能分析的神器你可以一眼看出哪个 System 最耗性能点击还能看到它内部每个 Job 的耗时。Entity 列表这是核心区域。它展示了当前选中的 Chunk 或查询结果中的所有实体。默认视图是All Entities但更强大的是上方的查询输入框。查询输入框你可以在这里输入 ECS 查询语句例如*ComponentType所有拥有该组件的实体、ComponentTypeA, ComponentTypeB同时拥有 A 和 B 的实体、-ComponentTypeC不拥有 C 的实体。这是你验证 System 查询逻辑是否正确的最直接方式。实体详情面板选中一个实体后这里会列出该实体上附加的所有 ComponentData以及它们的当前值。你可以直接修改这些值并立即在游戏中看到效果这对于调试来说无比方便。实战技巧一验证查询逻辑。你的MovementSystem应该只移动有Translation和MoveSpeed的实体。在 System 的OnUpdate里你可以通过Entities.ForEach或IJobEntity来写逻辑。但在写之前先到 Entity Debugger 的查询框输入Translation, MoveSpeed看看列出的实体是不是你期望的那些。如果不是可能是你的组件添加逻辑有问题或者实体还未创建。实战技巧二动态监测数据变化。有时候 bug 表现为实体行为异常。你可以先找到这个实体然后在 Entity Debugger 中选中它让详情面板保持打开。运行游戏观察它的组件数据是如何随着每一帧变化的。如果某个值的变化不符合预期你就能快速定位到是哪个 System 在错误地修改它。3.2 利用 Entity Debugger 诊断典型问题“我的实体怎么不见了”首先在查询框输入*查看所有实体。如果找不到确认实体是否被正确创建检查EntityManager.CreateEntity的调用是否执行。如果创建了但瞬间消失可能是某个 System 的查询包含了Entity组件这是一个特殊组件表示实体本身并在逻辑中错误地销毁了它们。“System 没执行”去 System 列表查看。如果找不到你的 System说明它可能没有正确注册到 World 中。检查你的 System 类是否继承了SystemBase或ISystem并且是否在一个继承了WorldSystemBase的 Bootstrap 类中被创建。如果 System 存在但执行时间为 0可能是它的查询条件匹配不到任何实体或者它被放在了不运行的 SystemGroup 里。“性能瓶颈在哪里”直接看 System 列表的耗时排序。耗时最高的 System 就是优化重点。点击它展开看是哪个 Job 最慢。然后结合代码分析这个 Job 处理了多少实体循环内部逻辑是否复杂是否可以用 Burst 编译检查是否有导致 Burst 编译失败的代码如调用非 Burst 兼容的静态方法是否可以通过改变数据结构如使用NativeParallelHashMap代替线性查找来优化“内存布局是否高效”在 Entity 列表你可以看到实体是按照 Archetype原型分组存储在 Chunk 中的。一个 Archetype 由一组唯一的组件类型组合定义。频繁地添加或删除组件会导致实体在 Chunk 间移动开销很大。通过 Debugger你可以观察你的实体是否因为组件变化过于频繁而产生大量不同的 Archetype这可能是性能隐患。4. 从 MonoBehaviour 到 DOTS 的迁移实战案例理论说再多不如动手做一遍。我们以一个最简单的场景为例在屏幕上生成 10000 个不断做正弦运动的小方块。我们将分别用 MonoBehaviour 和 DOTS 实现并对比性能。4.1 MonoBehaviour 传统实现// SineWaveMono.cs using UnityEngine; public class SineWaveMono : MonoBehaviour { public float amplitude 1f; public float frequency 1f; private Vector3 startPos; private float timer 0f; void Start() { startPos transform.position; } void Update() { timer Time.deltaTime; float y Mathf.Sin(timer * frequency) * amplitude; // 注意这里每帧都 new 了一个新的 Vector3会产生 GC Alloc transform.position startPos new Vector3(0, y, 0); } }然后我们写一个生成器在 Awake 时 Instantiate 10000 个带有上述脚本的预制体。性能问题分析CPU主线程需要顺序调用 10000 次Update每次包含数学计算和transform.position的赋值这背后是引擎内部的一次矩阵运算。内存10000 个独立的 GameObject 和 Transform 组件内存分散。GC每帧 10000 个new Vector3()产生巨量的临时对象GC 压力爆炸。Profiler 里你会看到GC Alloc那一栏数字飞涨。4.2 DOTS 实现步骤首先我们需要定义组件数据。在 DOTS 中我们通常把数据和行为分开。步骤一定义 ComponentData// SineWaveData.cs using Unity.Entities; public struct SineWaveData : IComponentData { public float Amplitude; public float Frequency; public float PhaseOffset; // 初始相位让每个方块运动不同步看起来更自然 public float StartY; }注意这里我们不需要timer因为时间可以由 System 统一管理。StartY用来记录初始高度。步骤二创建 Authoring 组件用于在 GameObject 上配置为了能在编辑器里像配置 MonoBehaviour 一样配置我们的 DOTS 实体我们需要一个 Authoring 组件和对应的 Baker。// SineWaveAuthoring.cs using Unity.Entities; using UnityEngine; public class SineWaveAuthoring : MonoBehaviour { public float Amplitude 1f; public float Frequency 1f; class Baker : BakerSineWaveAuthoring { public override void Bake(SineWaveAuthoring authoring) { var entity GetEntity(TransformUsageFlags.Dynamic); AddComponent(entity, new SineWaveData { Amplitude authoring.Amplitude, Frequency authoring.Frequency, PhaseOffset Random.Range(0f, 6.283185f), // 0-2PI 的随机相位 StartY authoring.transform.position.y }); } } }这个 Baker 会在场景烘焙Baking时运行将 GameObject 转换Convert为 Entity并把我们配置的数据写入SineWaveData组件。步骤三编写处理运动的 System这是核心逻辑我们将使用IJobEntity来并行处理所有实体。// SineWaveMovementSystem.cs using Unity.Burst; using Unity.Entities; using Unity.Transforms; using UnityEngine; public partial struct SineWaveMovementSystem : ISystem { [BurstCompile] public void OnUpdate(ref SystemState state) { float deltaTime SystemAPI.Time.DeltaTime; float elapsedTime (float)SystemAPI.Time.ElapsedTime; // 使用 IJobEntity 进行并行处理 var job new SineWaveMovementJob { DeltaTime deltaTime, ElapsedTime elapsedTime }; // ScheduleParallel 会尝试在多核上并行执行这个Job job.ScheduleParallel(); } [BurstCompile] // 使用 Burst 编译极大提升性能 public partial struct SineWaveMovementJob : IJobEntity { public float DeltaTime; public float ElapsedTime; // 这个 Execute 方法会对每一个拥有 LocalTransform 和 SineWaveData 的实体执行 void Execute(ref LocalTransform transform, in SineWaveData data) { // 计算当前时间点该实体的 Y 轴偏移 float yOffset Mathf.Sin(ElapsedTime * data.Frequency data.PhaseOffset) * data.Amplitude; // 直接修改 transform 的 Position 属性。注意这里没有 new使用的是值类型的计算。 transform.Position.y data.StartY yOffset; } } }关键点解析IJobEntity这是一个代码生成Source Generator特性它会根据Execute方法的参数自动生成查询和调度代码非常简洁高效。ref LocalTransform表示我们要修改位置in SineWaveData表示我们只读取正弦波数据。ScheduleParallel()这是关键它会将工作分解成多个批次在多核 CPU 上并行执行。对于 10000 个实体这个计算会被分摊到所有可用的 CPU 核心上。[BurstCompile]这个属性让 Burst 编译器介入将 C# Job 代码编译成高度优化的机器码性能提升可达数十倍。无 GC 分配整个Execute方法中只进行浮点数运算没有创建任何托管堆对象完全避免了 GC 压力。步骤四生成 10000 个实体我们不再用Instantiate而是用EntityManager。可以写一个简单的 MonoBehaviour 在 Start 时批量创建。// SpawnerAuthoring.cs using Unity.Entities; using Unity.Mathematics; using UnityEngine; public class SpawnerAuthoring : MonoBehaviour { public GameObject Prefab; // 一个带有 SineWaveAuthoring 和 Renderer 的预制体 public int Count 10000; public float SpawnRadius 50f; class Baker : BakerSpawnerAuthoring { public override void Bake(SpawnerAuthoring authoring) { // 获取对预制体 Entity 的引用 var prefabEntity GetEntity(authoring.Prefab, TransformUsageFlags.Dynamic); // 添加一个 Spawner 组件用于在运行时生成实体 AddComponent(new MassSpawner { Prefab prefabEntity, Count authoring.Count, SpawnRadius authoring.SpawnRadius }); } } } public struct MassSpawner : IComponentData { public Entity Prefab; public int Count; public float SpawnRadius; } // 运行时生成实体的 System public partial class MassSpawnerSystem : SystemBase { protected override void OnUpdate() { // 遍历所有拥有 MassSpawner 组件的实体通常只有一个 foreach (var (spawner, entity) in SystemAPI.QueryMassSpawner().WithEntityAccess()) { var ecb new EntityCommandBuffer(WorldUpdateAllocator); // 使用 ECB 进行批量操作 for (int i 0; i spawner.Count; i) { // 实例化预制体 Entity var newEntity ecb.Instantiate(spawner.Prefab); // 随机位置 float3 randomPos new float3( UnityEngine.Random.Range(-spawner.SpawnRadius, spawner.SpawnRadius), 0, UnityEngine.Random.Range(-spawner.SpawnRadius, spawner.SpawnRadius) ); // 设置初始位置 ecb.SetComponent(newEntity, LocalTransform.FromPosition(randomPos)); } // 执行命令并销毁 Spawner 自身防止重复生成 ecb.Playback(EntityManager); ecb.DestroyEntity(entity); // 只需要执行一次所以跳出循环 break; } } }4.3 性能对比与实战心得完成上述步骤后分别运行两个场景并打开 Unity Profiler (Window Analysis Profiler) 进行对比。Profiler 数据对比预期结果指标MonoBehaviour 方案DOTS 方案说明CPU 主线程耗时极高可能 20ms极低1msDOTS 将计算转移到多线程 Job。GC Alloc/帧极高~100KB0 BDOTS 使用值类型和 Burst无托管分配。帧率 (FPS)很低剧烈波动稳定在高帧率如 60DOTS 消除了主线程瓶颈和 GC 卡顿。内存访问模式随机访问缓存不友好顺序访问缓存高效ECS 数据连续存储CPU 预取效果好。实战心得与避坑从 Authoring 开始不要一上来就纯代码创建 Entity。先用 GameObject 和 Authoring 组件在编辑器里摆好、调好参数、做好预制体。利用 SubScene 和 Baking 流程这是连接现有工作流和 DOTS 最顺畅的方式。善用EntityCommandBuffer(ECB)在 System 中尤其是 Job 里不要直接调用EntityManager来创建/销毁实体或修改组件因为EntityManager不是线程安全的。应该使用EntityCommandBuffer来记录命令然后在主线程上一次性回放Playback。WorldUpdateAllocator是一个每帧重置的分配器适合这种临时命令。LocalTransform是新宠在 DOTS 中尤其是面向未来的 Entities Graphics 渲染管线优先使用LocalTransform和LocalToWorld组件而不是旧的Translation,Rotation,Scale组合。LocalTransform更高效且是Unity.Transforms包的核心。Burst 编译失败排查如果 Job 用了[BurstCompile]但没生效在 Console 窗口可能会看到 Burst 警告。常见原因包括Job 中调用了非静态方法、使用了接口、访问了托管对象或静态变量。确保 Job 结构体内部只包含值类型字段和 Blittable 类型。调试是常态初期你的 DOTS 代码可能不工作。别慌立刻打开 Entity Debugger。检查实体是否生成组件是否添加System 是否执行查询是否正确。这是定位问题最快的方法。5. 深入进阶性能优化与架构设计当你掌握了基础开始构建更复杂的 DOTS 项目时以下几个进阶话题将决定你项目的最终性能天花板。5.1 内存布局与 Archetype 优化ECS 的性能核心在于内存布局。实体根据其拥有的组件类型组合Archetype被分组存储在固定大小的内存块Chunk通常为 16KB中。一个 Chunk 内只存储同一种 Archetype 的实体。优化原则一减少 Archetype 变化。当实体添加或删除组件时它需要从一个 Chunk 移动到另一个 Chunk这是一个相对昂贵的操作。设计时应尽量让实体的组件组合在运行时保持稳定。例如不要把临时状态如IsSelected做成一个独立的IComponentData而是可以考虑用一个EnableableComponent可启用组件或者在一个标签组件上使用Enabled属性。优化原则二关注 Chunk 利用率。一个 Chunk 没存满就浪费了内存。如果某个 Archetype 的实体数量很少但种类很多会导致大量半满的 Chunk。可以考虑使用SharedComponentData进行分组但要注意SharedComponentData会导致根据共享值进行额外的 Chunk 划分滥用也会造成碎片化。实战工具使用Unity.Entities.Debugging命名空间下的EntityManager.GetAllUniqueArchetypes等方法或在 Entity Debugger 中观察不同 Archetype 的数量和实体分布来诊断内存布局问题。5.2 System 调度与依赖管理复杂的游戏会有几十上百个 System。如何安排它们的执行顺序和依赖关系至关重要。SystemGroupUnity 提供了预定义的 SystemGroup如InitializationSystemGroup、SimulationSystemGroup、PresentationSystemGroup你也可以创建自己的。通过[UpdateInGroup(typeof(YourSystemGroup))]特性将 System 放入组中并通过[UpdateBefore(typeof(OtherSystem))]和[UpdateAfter(...)]来精确控制顺序。手动依赖管理对于 Job 之间的依赖你需要手动管理JobHandle。IJobEntity和SystemAPI.Query().ScheduleParallel()等方法都会返回一个JobHandle。使用JobHandle.CombineDependencies来合并多个依赖并将合并后的 handle 传递给后续依赖它们的 Job 或SystemAPI.Query().ScheduleParallel()的dependsOn参数。Complete()的时机调用JobHandle.Complete()会强制主线程等待该 Job 完成。要避免在OnUpdate中过早调用Complete()否则会阻塞主线程失去并行优势。通常依赖关系会在 System 链中自动传递由最后一个需要该结果的 System 或World的更新末尾来隐式完成。5.3 与 Physics、Graphics、UI 的交互DOTS 是一个完整的生态Unity 正在将越来越多的子系统 DOTS 化。Unity Physics (DOTS)这是全新的、基于 DOTS 的高性能物理系统。你需要使用PhysicsBody、PhysicsVelocity、PhysicsCollider等组件。物理模拟在独立的PhysicsSystem中运行你的游戏逻辑 System 可以在其前后更新物理相关数据。Entities Graphics未来的渲染方向。实体通过RenderMesh、MaterialMeshInfo等组件定义其渲染属性。RenderBounds和WorldTransform用于裁剪和变换。你需要使用 Hybrid Renderer V2 或更新的渲染管线。避坑确保渲染相关的组件在PresentationSystemGroup中更新并且LocalToWorld矩阵是最新的。UI目前 Unity 的主流 UIuGUI/UI Toolkit仍基于 GameObject。与 DOTS 交互通常通过“桥梁”模式创建一个 MonoBehaviour 的 UI 控制器它监听 ECS 世界中的事件通过ComponentSystemGroup的OnUpdate末尾手动触发或利用EntityCommandBuffer添加特定标签组件作为信号然后更新 UI。反之UI 输入事件也可以通过写入一个 NativeArray 或触发一个 ECS 事件来影响 DOTS 系统。5.4 常见陷阱与排查清单即使你理解了所有概念实际开发中仍会踩坑。这里有一个快速排查清单实体没有显示/运动Entity Debugger 里能看到实体和组件吗 - 检查生成逻辑和 Baker。对应的 System 在 System 列表里吗执行时间大于 0 吗 - 检查 System 是否被正确创建和启用。System 的查询条件对吗在 Entity Debugger 里用查询语句验证。游戏运行巨卡甚至崩溃Profiler 里看哪个 System 或 Job 耗时异常高。检查 Job 中是否有无限循环或极其耗时的单个操作。检查是否在 Job 中错误访问了EntityManager或托管对象。检查 Burst 编译是否成功查看 Console 的 Burst 日志。数据不同步或表现怪异是否有多线程数据竞争确保只在一个 Job 中写入某个组件或用NativeDisableParallelForRestriction小心处理。检查 System 执行顺序。A System 依赖 B System 的数据但 A 在 B 之前执行了调整 SystemGroup 或[UpdateBefore/After]。使用Unity.Entities.Debugging的EntityLogging在控制台打印实体和组件变化进行追踪。内存泄漏是否创建了NativeArray或NativeContainer但没有Dispose使用Allocator.Temp的必须在同一帧内 DisposeAllocator.Persistent的必须在不用时手动 Dispose。实体是否被正确销毁使用EntityCommandBuffer.DestroyEntity或EntityManager.DestroyEntity。从 MonoBehaviour 到 DOTS 的转变不仅仅是学习一套新的 API更是一种思维模式的转换——从“对象”思维转向“数据”和“系统”思维。初期会有阵痛但一旦你习惯了这种高效、并行的数据处理方式就很难再回去了。尤其是在面对大规模单位、复杂模拟或严苛的性能要求时DOTS 提供的性能提升和稳定性是传统方式难以企及的。