从个体户到流水线:Unity DOTS的效率革命
一句话传统 Unity 像一群各干各的个体户每个物体自己管自己DOTS 像一座现代化工厂同类数据放进同一条流水线多个工人同时开工每个工人还都受过专业训练。东西越多这座工厂的优势就越明显。一、先看一个画面学校收作业传统方式每个学生背一个大书包全校 1 万名学生每人一个书包里面装着语文、数学、英语作业还有饭盒、水杯、课外书。数学老师要批改所有人的数学作业走到 1 号同学面前打开书包翻出数学作业批改走到 2 号同学面前他坐在另一栋楼打开书包翻找……重复 1 万次老师大部分时间都在走路和翻书包真正批改的时间很少。这就是传统的GameObject MonoBehaviour敌人 AGameObject ├─ Transform 位置 ├─ Enemy 脚本 血量、速度、AI 状态 ├─ Animator 动画 └─ AudioSource 声音每个物体都是一个大书包数据散落在内存各处。CPU 想更新所有敌人的位置就得在内存里东跑西跑。DOTS 方式按科目分柜子学校改了规矩每个学生只有一个学号所有数学作业放进数学柜按学号顺序排好所有语文作业放进语文柜数学老师只管数学柜从头翻到尾作业太多叫上 8 个助教每人负责一段老师们还都练过一目十行同样 1 万份作业原来要改一整天现在一节课就改完了。学校DOTS学号Entity实体只是一个编号数学作业、语文作业Component组件纯数据数学老师、语文老师System系统处理某类数据的逻辑按科目分好的柜子Chunk同类数据连续存放多个助教同时批改Job System多线程老师一目十行Burst 编译器生成高效机器码一次处理多个数据二、DOTS 的三位主角DOTS 全称Data-Oriented Technology Stack面向数据的技术栈。可以把它想成一支由三个角色组成的团队️ 主角一ECS —— 极度爱整洁的仓库管理员他的口头禅是“东西要分类放常用的放一起。”他会把拥有相同组件组合的实体放在一个区域这叫Archetype原型原型 1[位置, 速度, 血量] ← 普通小兵都在这里 原型 2[位置, 速度, 血量, 飞行] ← 会飞的单位在这里 原型 3[位置, 静态模型] ← 石头、树木在这里每个区域再分成一个个16KB 的小抽屉Chunk抽屉里同类数据挨个排好抽屉Chunk ┌────────────────────────────────────────┐ │ 位置 [P1][P2][P3][P4][P5] … │ │ 速度 [V1][V2][V3][V4][V5] … │ │ 血量 [H1][H2][H3][H4][H5] … │ └────────────────────────────────────────┘移动系统只需要位置和速度血量那一排连碰都不碰。还记得 DRAM 那一篇吗CPU 每次从内存搬64 字节。数据排得整齐搬来的 64 字节全是有用的数据乱放搬来的 64 字节里可能只有 4 字节有用。 主角二Job System —— 会安排活的包工头他的口头禅是“一个人干不完大家分着干。”现在的手机都有 8 个左右的 CPU 核心可传统Update()基本只在主线程上跑传统方式 核心 1████████████████████ 累死了 核心 2 闲着 核心 3 闲着 …… Job System 核心 1█████ 核心 2█████ 核心 3█████ 核心 4█████ 大家一起干很快干完更厉害的是他还是个安全员如果两个工人同时去改同一份数据他会在编辑器里直接大喊停而不是让 bug 悄悄溜进游戏。而且每个工人负责不同的抽屉互不干扰自然避开了上一篇讲的伪共享。⚡ 主角三Burst —— 一目十行的特种兵他的口头禅是“一次只算一个数太慢了”Burst 是一个编译器它把你写的 C# 代码翻译成高度优化的机器码并尽量使用SIMD单指令多数据普通计算 11223344 做 4 次 SIMD [1 2 3 4] [1 2 3 4] 做 1 次就像普通人一次端一盘菜Burst 训练出来的服务员一次能端四盘。不过他也有脾气只接受干净的代码。不能用class、不能用string、不能产生垃圾回收。规矩多但守规矩就跑得飞快。三人合体角色解决的问题比喻ECSCPU 总在等内存东西排整齐不用到处找Job System多个核心在闲着大家分工一起干Burst单个核心干活不够快每个人都受过专业训练三个加速叠在一起才是 DOTS 真正的威力。三、思维方式的转变从名词到动词这是学习 DOTS 最大的难点也是最关键的一点。传统思维先问它是什么这是一个僵尸 → 它有血量、速度 → 它会走、会咬人 → 写一个 Zombie 类 这是一个骷髅 → 写一个 Skeleton 类 这是一个会飞的僵尸 → 继承组合头开始疼……DOTS 思维先问这批数据要做什么所有有位置 速度的东西 → 都要移动 → 移动系统 所有有血量的东西 → 都可能死 → 死亡系统 所有有位置 飞行的东西 → 都要受升力 → 飞行系统一个会飞的僵尸不需要新类只要给它多贴一张飞行标签实体位置速度血量飞行会被哪些系统处理普通僵尸✅✅✅移动、死亡飞行僵尸✅✅✅✅移动、死亡、飞行石头✅都不处理传统方式像给每个人写一份完整的人生剧本DOTS 像发一张张技能卡谁拿了卡谁就被对应的老师管理。四、看一帧画面万人僵尸潮的 16 毫秒假设场景里有2 万只僵尸一帧只有约 16 毫秒60 帧。传统方式的一帧主线程 僵尸1.Update() → 僵尸2.Update() → 僵尸3.Update() → … → 僵尸20000.Update() 每次都要去内存里找这只僵尸的数据 ……16 毫秒早就用完了画面开始卡成幻灯片DOTS 方式的一帧0 ms 移动老师上场 所有有位置和速度的排好队 包工头 8 个人每人管几个抽屉开工 Burst 特种兵 一次算 4 个唰唰唰…… 2 ms 移动完成 2 ms 攻击老师上场 所有离玩家近的准备攻击 3 ms 攻击完成 3 ms 死亡老师上场 血量归零的先登记在小本子上帧末统一处理。 3.5 ms 完成 剩下的时间 → 留给渲染、物理、UI游戏流畅运行那个小本子就是EntityCommandBuffer命令缓冲区。为什么不能当场删除想象工人们正在沿着抽屉批改作业你突然把其中一个抽屉搬走了工人们会一头雾水。所以要先记下来等大家都忙完了再统一搬。五、代码长什么样比想象中简单只看三段就能明白 DOTS 的基本写法。① 组件一张数据卡片usingUnity.Entities;// 只有数据没有任何方法publicstructMoveSpeed:IComponentData{publicfloatValue;}② 编辑器配置照样拖拖拽拽usingUnity.Entities;usingUnityEngine;// 挂在预制体上在 Inspector 里照常填数值publicclassZombieAuthoring:MonoBehaviour{publicfloatspeed2f;classBaker:BakerZombieAuthoring{publicoverridevoidBake(ZombieAuthoringauthoring){varentityGetEntity(TransformUsageFlags.Dynamic);AddComponent(entity,newMoveSpeed{Valueauthoring.speed});}}}这个过程叫Baking烘焙在编辑器里GameObject 被烤成紧凑的实体数据。就像把新鲜食材做成真空包装的预制菜运行时拆开就能用。③ 系统一位只管一件事的老师usingUnity.Burst;usingUnity.Entities;usingUnity.Mathematics;usingUnity.Transforms;[BurstCompile]publicpartialstructMoveSystem:ISystem{[BurstCompile]publicvoidOnUpdate(refSystemStatestate){floatdtSystemAPI.Time.DeltaTime;// 所有有位置和速度的实体往前走foreach(var(transform,speed)inSystemAPI.QueryRefRWLocalTransform,RefROMoveSpeed()){transform.ValueRW.Positionmath.forward(transform.ValueRO.Rotation)*speed.ValueRO.Value*dt;}}}读起来就像一句话“找出所有有位置和速度的实体让它们往前走。”不关心它是僵尸、骷髅还是小车。这里为了好懂写成了主线程上的循环。想让多个核心一起干可以改成上一篇里的IJobEntityScheduleParallel()。六、DOTS 家族全家福成员形象比喻作用Entities仓库管理员ECS 框架本体Jobs包工头多线程调度Burst特种兵高性能编译Collections不会产生垃圾的收纳箱NativeArray等容器没有 GCMathematics特种兵专用计算器float3、math.*对 Burst 友好Entities Graphics批量化妆师一口气渲染海量实体Unity Physics / Havok物理裁判面向 ECS 的物理引擎Netcode for Entities联机调度员多人同步、预测、回滚好消息是Jobs Burst Collections 可以单独使用。不想重写整个项目只把最慢的那段计算交给它们就行。七、它在游戏里能干什么游戏类型场景用 DOTS 的感觉割草 / 幸存者类满屏怪物、满屏子弹从几百个就卡到几万个照样跑RTS / 塔防千军万马同时行军一次寻路结果全军共用城市模拟车流、人流、水电网络每个市民都在独立生活开放世界满山的树、遍地的石头分区快速加载走到哪加载到哪多人竞技服务器要算所有人的状态预测回滚需要反复重算速度至关重要生态 / 群体模拟鱼群、鸟群、虫群每个个体遵守简单规则整体形成壮观效果公开资料中《V Rising》使用了 ECS 和 Netcode for Entities《都市天际线 2》的城市模拟也大量使用了 DOTS。它们的共同特点是东西特别多规则相对统一。八、DOTS 的脾气踩坑图鉴DOTS 跑得快但它有几个小脾气惹到了也会变慢。 脾气一讨厌搬家结构性变化给实体增加或删除组件它就得从一个区域搬到另一个区域把所有家当都拷贝一遍。僵尸被眩晕 → 加眩晕组件 → 搬家 眩晕结束 → 删眩晕组件 → 又搬家 一秒钟眩晕十次 → 搬家十次 → 累垮了解决使用可开关组件IEnableableComponent像墙上的电灯开关按一下就行不用搬家。 脾气二讨厌等红灯同步点工人们正在流水线上干活主线程突然要直接改结构就得喊全体停工等所有人停下来才能动手。解决结构性修改尽量用小本子ECB记下来在固定时间统一处理。️ 脾气三讨厌空房间共享组件滥用共享组件会按值分房间。如果每个实体的值都不同就会出现一大堆只住一两个人的房间抽屉利用率极低。解决共享组件只用于种类很少的分组比如所在区域、材质。 脾气四讨厌东奔西跑随机访问子弹要查目标在哪目标的数据可能在仓库另一头。偶尔跑一趟没关系几万颗子弹都这样跑就慢了。解决用空间网格把找附近的人变成按格子顺序查找。 脾气五不会演戏缺少官方骨骼动画目前 Entities 没有官方的骨骼动画系统。大量小兵可以用顶点动画贴图VAT重要角色可以用 GameObject 配合 Animator或者使用第三方动画库。开工之前先想清楚动画方案这是很多团队后期才发现的大坑。九、该不该用一张对照表你的游戏建议同屏单位成千上万✅ 非常适合大规模模拟、计算密集✅ 非常适合只有少数几个复杂角色剧情为主❌ 传统方式更省事UI 为主的休闲游戏❌ 瓶颈通常不在这里卡顿来自渲染或 GPU❌ DOTS 解决的主要是 CPU 问题团队工期紧、没人学过⚠️ 先只用 Jobs Burst 优化热点已有大型 GameObject 项目⚠️ 混合开发逐步迁移引入的推荐顺序第一步用 Profiler 确认 CPU 是瓶颈 第二步只用 Jobs Burst 优化最慢的那段计算 第三步把一个独立子系统子弹、小兵迁到 ECS 第四步看收益再决定要不要扩大范围和研发效能那一篇的道理一样小步试验用数据说话收益不明显就停下来。十、常见的误会误会真相“用了 DOTS 游戏就自动飞起来”只有 CPU 是瓶颈、东西够多时收益才明显“DOTS 就是 ECS”ECS 只是其中一部分Jobs 和 Burst 可以单独使用“用 DOTS 就得推倒重来”混合开发是主流做法“DOTS 里不能用编辑器拖拽”通过 Baking照样用 Inspector 编辑“网上教程都能照着抄”Entities 1.0 前后 API 变化很大很多旧写法已弃用一句话总结传统 Unity每个物体背着自己的大书包一个一个慢慢处理。DOTS数据按种类分柜摆好多个工人一起顺序处理每个工人都能一目十行。记住一句口诀“数据排整齐ECS大家一起干Jobs每人都麻利Burst。”它最擅长人海战术不擅长单挑剧情。东西越多规则越统一DOTS 越能发挥威力。