一句话:传统 Unity 像一群各干各的个体户,每个物体自己管自己;DOTS 像一座现代化工厂,同类数据放进同一条流水线,多个工人同时开工,每个工人还都受过专业训练。东西越多,这座工厂的优势就越明显。
一、先看一个画面:学校收作业
传统方式:每个学生背一个大书包
全校 1 万名学生,每人一个书包,里面装着语文、数学、英语作业,还有饭盒、水杯、课外书。
数学老师要批改所有人的数学作业:
- 走到 1 号同学面前,打开书包,翻出数学作业,批改
- 走到 2 号同学面前(他坐在另一栋楼),打开书包,翻找……
- 重复 1 万次
老师大部分时间都在走路和翻书包,真正批改的时间很少。
这就是传统的GameObject + MonoBehaviour:
敌人 A(GameObject) ├─ 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(单指令多数据):
普通计算: 1+1,2+2,3+3,4+4 做 4 次 SIMD: [1 2 3 4] + [1 2 3 4] 做 1 次就像普通人一次端一盘菜,Burst 训练出来的服务员一次能端四盘。
不过他也有脾气:只接受"干净"的代码。不能用class、不能用string、不能产生垃圾回收。规矩多,但守规矩就跑得飞快。
三人合体
| 角色 | 解决的问题 | 比喻 |
|---|---|---|
| ECS | CPU 总在等内存 | 东西排整齐,不用到处找 |
| 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{publicfloatspeed=2f;classBaker:Baker<ZombieAuthoring>{publicoverridevoidBake(ZombieAuthoringauthoring){varentity=GetEntity(TransformUsageFlags.Dynamic);AddComponent(entity,newMoveSpeed{Value=authoring.speed});}}}这个过程叫Baking(烘焙):在编辑器里,GameObject 被"烤"成紧凑的实体数据。就像把新鲜食材做成真空包装的预制菜,运行时拆开就能用。
③ 系统:一位只管一件事的老师
usingUnity.Burst;usingUnity.Entities;usingUnity.Mathematics;usingUnity.Transforms;[BurstCompile]publicpartialstructMoveSystem:ISystem{[BurstCompile]publicvoidOnUpdate(refSystemStatestate){floatdt=SystemAPI.Time.DeltaTime;// "所有有位置和速度的实体,往前走"foreach(var(transform,speed)inSystemAPI.Query<RefRW<LocalTransform>,RefRO<MoveSpeed>>()){transform.ValueRW.Position+=math.forward(transform.ValueRO.Rotation)*speed.ValueRO.Value*dt;}}}读起来就像一句话:“找出所有有位置和速度的实体,让它们往前走。”不关心它是僵尸、骷髅还是小车。
这里为了好懂,写成了主线程上的循环。想让多个核心一起干,可以改成上一篇里的
IJobEntity+ScheduleParallel()。
六、DOTS 家族全家福
| 成员 | 形象比喻 | 作用 |
|---|---|---|
| Entities | 仓库管理员 | ECS 框架本体 |
| Jobs | 包工头 | 多线程调度 |
| Burst | 特种兵 | 高性能编译 |
| Collections | 不会产生垃圾的收纳箱 | NativeArray等容器,没有 GC |
| Mathematics | 特种兵专用计算器 | 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 越能发挥威力。