如何制作一款开放世界游戏:技术全面解析

这篇文章基于对一款成熟商业开放世界游戏的引擎与客户端代码的长期源码级逆向阅读(约 700+ 篇源码级笔记)整理而成——它覆盖的不只是底层引擎,也包括玩法、联机、存档、UI、遥测这些更偏游戏端的系统。文章不讲某一个炫技点,而是回答一个更朴素也更难的问题:当你真的要把一座”装不下、停不下、还要联网运营”的世界做出来,到底有哪些工程问题在等着你,它们各自是怎么被解掉的。

写法上每一块都遵循同一条线索:先讲要解决什么问题,再讲它怎么解,最后落到守了什么边界、对今天做开放世界有什么启发

诚实边界:我读透的是”运行时怎么被组织起来”,没读透的是”底层算法怎么实现”。凡是碰撞求解器、shader 数学、寻路内核、音频 DSP 这类算法盒子,以及时间天气、内饰、拾取这几个本就没读到的空白,我都会明确标注”盲区”——读到哪、讲到哪,这条边界划得清清楚楚。


引言:开放世界最重的瓶颈,不是画面

先做个思想实验。假设有人塞给你一个能跑 demo 的引擎——渲染、物理、动画都现成——然后说:用它做一款开放世界游戏吧。你觉得最重的瓶颈会出现在哪?

不是画面。画面再差,至少能跑。

真正最重的瓶颈,出现在你把几百个 NPC、上百辆车、成片的可交互物件一并放进世界、让它们”同时活着”的那一刻。这一刻,所有看似无害的设计假设会同时暴露:没有人知道这一帧该先算谁、后算谁;没有人说得清一个状态到底归谁拥有、谁只是临时借用;更没有人能保证当你把镜头摇到一公里外,那几百个对象会自动”算得少一点”而不是把帧率拖进泥潭。

这就是开放世界真正的难点。它的难,不在”东西多”,而在怎么让海量异构对象不各自为政。具体拆成三件事:

  • 第一,一帧的固定预算里,怎么有序地推进所有对象。 你不能让每个对象自己决定什么时候更新自己——那样 AI 拿不到完整的事件上下文,物理拿不到贴近模拟的时机,渲染拿不到最终的骨骼矩阵。
  • 第二,怎么分清谁拥有状态、谁只是桥接。 一辆 NPC 驾驶的车,驾驶循环到底归谁?一个被复制到其他玩家机器上的角色,它的”真身”在哪台机器上?把这个问题想清楚,是规模化和联网的前提。
  • 第三,怎么把”造世界、能联网、能上线运营”这一整圈外围工程都接住。 内容是怎么生产出来的、多人怎么同步、上线后怎么防作弊、怎么知道哪里卡了——这些不是”锦上添花”,是商业开放世界的地基。

整篇文章会围绕三条副线展开,它们贯穿始终:

  1. 世界级调度,而非每对象 Tick。 一帧的推进是被一个全局编排器按相位驱动的,不是几百个对象各自 Tick 出来的。
  2. 分层所有权。 owner(拥有状态的)、requester(请求生成的)、observer(只是观察的)、recorder(只是录制的)——把这几个角色分清楚,很多看似无解的耦合就化开了。
  3. 把成本编码进对象状态。 dummy⇄real 的双向切换、行为级 LOD——海量对象之所以不爆,是因为”算多少”被编码进了对象自己的状态里。

下图是这套引擎的七大类全景。它既是这篇文章的目录,也是结尾我们要回扣的地图。

七大类全景地图

七大类从底座到外围依次是:引擎底座(怎么跑起来、一帧怎么推进)、世界与内容(装不下的世界怎么动态加载)、模拟核心(对象怎么组织、怎么动、怎么活——全文证据最厚的主菜)、玩法系统(把模拟变成有规则的游戏)、表现层(玩家看到听到操作的)、联机与在线服务(多人同步加商业运营)、工程与运营支撑(防作弊、遥测、以及那条被严重低估的离线内容管线)。

下面逐章展开。需要提醒的是:这是一张广度地图——每个深点我都会讲到”为什么这么设计”,但每一个都值得单独深挖成篇,后续会陆续补上。


第一章 · 引擎底座:一帧是怎么有序推进的

这一章要解决的问题:海量对象,一帧里谁先算、谁后算?怎么保证这个顺序是确定的、可复现的?

如果说整篇文章只有一个”读完能改变你架构直觉”的点,那就是这一章的帧相位调度。我们先从一个更基础的解耦讲起。

1.1 引擎与游戏的解耦:边界划在哪

任何一个想长期演进的引擎,都要先回答一个问题:引擎层和游戏层的边界划在哪?

这套引擎的做法很克制:引擎层只暴露三个入口回调——初始化、每帧更新、关闭——然后让游戏层把自己的逻辑”挂”上去。引擎本身完全不知道游戏里有 NPC、有车、有通缉系统;它只知道”该初始化了””该更新一帧了””该关闭了”,剩下的全交给挂上来的那三个回调。

引擎与游戏解耦

这种”三回调入口”的设计看起来朴素,但它划清了一条极重要的界线:引擎提供的是能力和节奏,游戏提供的是内容和规则。 引擎负责”什么时候该推进一帧”,游戏负责”这一帧具体推进什么”。两边通过这三个挂载点通信,谁也不必知道对方的内部细节。

在游戏层内部,则有一个自己的状态机来管理”现在处于哪个大阶段”——是在加载、在主菜单、在游戏中、还是在过场。这个状态机决定了每一帧的更新走哪条路径。

启发:今天用 UE 或自研引擎做开放世界,最该问自己的就是这条界线划得对不对。引擎能力(渲染、物理、资源)和游戏逻辑(你的玩法、你的世界规则)一旦糊在一起,后面想替换引擎、想做工具链、想让美术和程序并行,都会处处掣肘。把”节奏”留给引擎,把”内容”留给游戏,中间用尽量窄的挂载点连接——这是所有后续可维护性的源头。

1.2 帧相位调度(frame phase scheduling):为什么不能”每个对象自己 Tick”

现在来到这套引擎最反直觉、也最值得学的设计。

新手做游戏,第一直觉几乎都是:每个对象有一个 Tick() 方法,引擎每帧遍历所有对象,挨个调用。这在小游戏里完全够用。但在开放世界里,它会从根上崩掉。原因是:对象之间的更新存在大量”时序约束”,而这些约束,单个对象的 Tick 根本表达不了。

举几个具体的约束:

  • AI 要在拿到完整事件上下文之后才能决策。 如果一个 NPC 在自己的 Tick 里决策,而它依赖的”周围发生了什么”事件可能在同一帧里还没收集齐——它就会基于过时信息行动。
  • 物理要贴近模拟步推进。 物理求解必须在一个集中的、靠近模拟时钟的阶段做,不能散落在各个对象的 Tick 里被随意打断。
  • 某些”物理后标志位”必须在模拟结束后统一重置。 比如”这一帧是否发生了碰撞”这类标志,要在物理跑完后、下一帧开始前,由一个统一的阶段来清。散在各对象里清,必然有先后错乱。
  • IK(反向动力学)要拿到最终骨骼矩阵才能做。 脚贴地、手扶墙这类 IK,必须在动画和物理都定稿之后做,否则贴的是过时的姿态。

这些约束的共同点是:它们关心的是”在整帧的哪个阶段做”,而不是”在哪个对象里做”。 一个对象的 Tick 是”按对象切分”的,而这些约束是”按阶段切分”的——两套切分方式正交,所以用前者表达后者,必然别扭。

这套引擎的解法是:不让对象自己 Tick,而是由一个世界级的编排器,把一帧切成多个有序相位,每个相位扫一遍相关对象、只做该相位该做的事。

一帧的相位时序图

如上图,一帧被切成若干相位,典型的顺序是:

  1. 处理相位(Process):收集事件、跑 AI 决策、推进任务状态机。此时所有对象的”意图”被确定下来。
  2. 移动后相位(After All Movement):所有移动都施加完之后,做需要”最终位置”的工作——比如 Verlet 约束、浮力这类依赖位置已定稿的计算。
  3. 相机后相位(After Camera):相机已经更新到最终位置,做依赖相机的工作。
  4. 预渲染后相位(After Pre-Render):渲染前的最后准备都做完了,做依赖最终骨骼矩阵的 IK 之类。

每个相位都有明确的”为什么必须在这里”,图里都标了出来。再配合一个”场景更新标志”,引擎能精确控制哪些对象在哪些相位参与更新。

这套设计的代价,是你要放弃”对象自治”这个直觉——对象不再是一个能自己决定何时更新的主体,而更像一条被多个相位反复扫过的数据记录。但换来的是确定性:同样的输入,每一帧的推进顺序完全一致,这对联网(要求各端可复现)和回放(要求逐帧重建)是命根子。

启发:这是“世界级相位调度”和“每个对象各自更新”两种思路最根本的分界。当世界规模上去、当你要联网要回放,对象更新的先后顺序就不再是实现细节,而是正确性的前提。那几个有强时序约束的系统(AI、物理后处理、IK),值得从各自为政的更新方式里抽出来,交给一套统一的相位顺序来驱动; 不必把所有东西都改造,但越是依赖确定顺序的系统,越早纳入集中调度越省心。

1.3 三档 update 与平台底座

帧调度之外,引擎底座还有两块值得点到。

第一块是分档更新。这套引擎的每帧更新不是只有一种,而是分三档:完整(FULL)、共通(COMMON)、精简(SIMPLE)

三档 update 对比

简单说:完整档是正常游戏中的全量更新;精简档用于某些不需要全量模拟的场景(比如某些加载或过渡态);共通档是两者都要做的那部分公共逻辑。把更新分档的意义在于——不是每一帧、每个场景都需要付出全量模拟的代价,引擎可以根据当前所处的大状态,选择走哪一档,省下不必要的开销。

第二块是平台底座,这部分我只到结构层:系统核心的初始化/退出/重启、图形设备丢失后的恢复(比如 d3d11 设备重置、present-flip 的处理)、画质的自动配置(探测硬件→生成配置→应用)、内存分配器、文件与打包文件(packfile)的挂载、以及流式安装(一边下载一边能玩的 PlayGo 式机制)。

这些东西玩家永远不会注意到,但它们是”能跨平台、能上线”的隐形地基。设备一丢失就要能恢复、配置一变就要能落地、内容没下完就要能先玩起来——每一条都是商业产品的硬要求。

启发:底座这一层的教训是,“能 demo”与”能上线”之间,隔着一整层平台工程。自动画质检测、设备恢复、流式安装之类,立项时最容易被推迟,临近上线却最容易集中爆发。应尽早把它们视为一等公民,而非收尾阶段的琐碎杂务。

🔧 设计复盘 · 帧相位调度

为什么会这样设计? 因为对象之间的更新约束是”按阶段”的,不是”按对象”的——AI 要等事件收齐、物理要贴近模拟步、IK 要等最终骨骼。这些约束跨越了对象边界,只能由一个高于所有对象的全局编排器、按相位统一安排。把”何时算”的决定权从对象手里收归世界,是这些时序约束的必然结果,而非出于美学上的取舍。

踩过什么坑? 最隐蔽的坑是”看起来对、其实差一帧”。当某个对象在自己的 Tick 里读了另一个还没更新的对象的状态,画面上往往看不出错,直到联网要求各端一致、或回放要求逐帧重建时,这种”差一帧”才集中爆发成抖动、撕裂、不同步。等到那时再回去相位化,已经是伤筋动骨的大改。

传统方案差在哪? “每个对象一个 Tick、引擎遍历调用”——这是小游戏的标准写法,在开放世界里会从根上失效。对象数量一上来,谁先 Tick 谁后 Tick 变成不可控的隐式依赖;想加一个”必须在物理后做”的逻辑,只能四处堆砌标志位与延迟调用来打补丁;最终,整套帧逻辑沦为一团盘根错节、无人敢动的泥潭。它的症结不在性能,而在确定性与可维护性


第二章 · 世界与内容:装不下的世界怎么动态加载

这一章要解决的问题:世界比内存大几个数量级,怎么做到”看得到的才在内存里、看不到的卸掉”,而且切换的时候不卡。

这件事的本质是一个”无限大世界 vs 有限内存”的矛盾,所有开放世界都绕不过去。这套引擎给出的答案,核心是两个分离:可用性与生产分离成本与对象状态绑定

2.1 流式加载:把”资产可用”和”对象生成”拆开

最容易踩的坑,是把”加载资产”和”生成对象”当成一回事。它们看起来都叫”加载世界的一部分”,但其实是两个职责完全不同的系统,硬绑在一起会让两边都失去弹性。

这套引擎把它们彻底拆开:

  • 流式系统(streaming)负责”可用性”:根据玩家位置和一个”场景流式体积”,决定哪些资产(模型、贴图、碰撞、navmesh 片段)需要被请求加载、哪些可以卸载、加载完之后怎么清理。它只关心”这块资产现在在不在内存里、能不能用”。
  • population 系统负责”生产”:在资产已经可用的前提下,决定”这里现在该不该生成一个 NPC、一辆车”,生成谁、生成多少、谁来生成。
流式可用性 vs population 生产分离

两者之间有一条清晰的界线,图里那句”可用 ≠ 已生成”就是关键:一块区域的资产加载好了,不代表那里就该有 NPC;NPC 该不该出现,是 population 根据密度、预算、玩法需要单独决定的。

为什么这个分离这么重要?因为它让两边可以独立调节。你可以在资产全部可用的情况下,单独压低某个区域的 NPC 密度(比如性能吃紧时);也可以在 population 完全不变的情况下,单独调整资产的加载半径(比如换了更大显存的机器)。如果两者糊在一起,任何一个维度的调整都会牵动另一个,调参变成噩梦。

启发“资产可用”和”对象生成”是两个正交的维度,一定要拆开。 这是规模化的前提。很多自研开放世界一开始图省事把它们合并,到了优化阶段才发现没法单独调,只能推倒重来。

2.2 行为级 LOD:远处的 NPC 不是画得糙,是算得少

LOD(细节层级)这个词,大多数人第一反应是网格 LOD——远处的模型用低面数版本。这套引擎里有一个更关键、也更被忽视的概念:行为级 LOD

它改的不是”画多少”,而是”算多少”。

行为级 LOD

如上图,随着对象离玩家越来越远,它的”行为成本”是阶梯式递减的:

  • 近处:NPC 跑完整 AI——完整的感知、决策、任务流水线;车有完整的驾驶 AI;物件是完整的可交互对象。
  • 中距:NPC 切到精简 AI——只保留必要的行为,砍掉昂贵的感知和决策;车降级成”dummy”——只保留位置和大致运动,不跑完整驾驶模拟。
  • 远处:对象退化成最轻量的占位表示,甚至被完全剔除。

关键认知是:远处那个 NPC 显得”迟钝”,并非因为它被画得粗糙,而是因为它被”算得更少”。 它的感知范围被收窄、决策频率被调低、任务被简化。这是以 CPU 预算换取世界规模——你不可能为一公里之外的每个路人都运行完整 AI,那是对算力的挥霍。

这个设计和第一章的帧调度是呼应的:正因为对象更新是”世界级相位扫描”而非”对象自治 Tick”,世界才能在扫描时根据距离决定”这个对象这一帧跑哪一档行为”。如果是对象自己 Tick,它很难知道自己该降级到什么程度。

启发LOD 不该只在渲染层做,行为层同样需要 LOD。 做开放世界时,问自己:远处的 AI 在算什么?如果答案是”和近处一样多”,那你的 CPU 预算迟早被几百个远处路人吃光。把”算多少”做成一个可以随距离/重要性降级的维度,和”画多少”一样重要。

2.3 Scenario 系统:世界的”活”是数据摆出来的

一座城市要”显得活着”——路边有人抽烟、公园有人遛狗、码头有人钓鱼、广场有人发传单——这些 ambient(环境氛围)行为是怎么来的?

一个糟糕的答案是:脚本一个个写。给每个抽烟的点写一段脚本,给每个钓鱼的点写一段脚本。这在几十个点时还行,到了一座城市成千上万个氛围点时,彻底不可维护。

这套引擎的答案是 Scenario(场景)系统——一个数据驱动的”在这个点该做什么”的机制。

Scenario 数据驱动系统

它的组织是分层的:点 → 区 → 簇

  • 场景点(point):地图上一个具体的位置,附带”在这里该做什么”的数据——比如”这是一个抽烟点””这是一个倚靠点”。
  • 场景区(region):把点按空间组织成区,支持按需流式加载(远处的场景点数据可以不加载)、支持预约(reservation,避免两个 NPC 抢同一个点)。
  • 场景簇(cluster):把相关的点聚成簇,让一群 NPC 协同地使用一片场景(比如一桌人一起打牌)。

还有车辆生成(cargen)这类机制,本质也是同一套思路:用数据描述”这里该生成什么”,而不是用代码逐个生成。

核心认知是:世界的”活力”是数据驱动摆出来的,不是脚本一条条写出来的。 关卡设计师在编辑器里摆场景点、调密度、设规则,运行时由 Scenario 系统统一调度 NPC 去”认领”这些点并执行对应行为。要加一片新的氛围区,加数据就行,不用写一行代码。

启发凡是”内容密度高、模式重复”的东西,都该数据驱动,而不是脚本硬写。 氛围行为、车辆生成、巡逻路线……把”做什么”抽成数据,把”怎么调度”做成系统,是开放世界内容能规模化的关键。这一点这套引擎做得很彻底,值得照搬。

2.4 时间与天气:一个诚实的盲区

做这类开放世界,昼夜循环和动态天气几乎是标配——白天黑夜、晴雨雷雾,还要影响光照、影响 NPC 行为、影响交通。

但这里必须如实说明:在我读过的这套引擎的笔记里,时间/天气/时钟/timecycle 并没有专门的篇章。 它只在一些间接之处露过面——例如交通灯的亮度会受时钟与天气影响、人口循环(popcycle)会随时段变化——而”昼夜系统本身如何实现、天气如何过渡、timecycle 如何驱动全局色调”这些关键,我并未读到一手的实现证据。

这是一个我不会特意填补的真实空白。基于现有的间接证据,只能做一个有限的推断:时间与天气在这套引擎里更接近一个全局的、被多个系统共同消费的状态——光照、交通、人口等子系统都在读取它;但它的内部实现机制(驱动方式、过渡逻辑、与渲染色调的耦合),不在我已读透的范围之内。

诚实边界:把这个盲区如实标在这里,是因为这篇文章的价值恰恰在于”分得清读透了什么、没读透什么”。如果你要做开放世界,时间天气是必做项,但别指望从这篇文章里拿到它的实现细节——这一块,我们没读到。

🔧 设计复盘 · 流式 / population 分离 + 行为级 LOD

为什么会这样设计? 因为”资产可用”和”对象生成”是两个变化频率、变化原因都不同的维度。资产加载跟着玩家位置走,NPC 密度跟着玩法和性能预算走——把它们拆开,才能各调各的。同理,行为级 LOD 把”算多少”做成对象状态的一个可降级维度,是因为你根本不可能给一公里外的每个路人都跑完整 AI,CPU 预算逼着你按距离/重要性给行为分档。

踩过什么坑? 把流式和 population 合在一起的坑,是到了优化阶段才发现”调不动”——想单独压低某区域 NPC 密度,结果连资产加载半径也跟着变了;两个维度纠缠在一起,任何一处调参都牵一发动全身。LOD 这边的坑则是降级”看得见”:升降档的时机没和可见性绑好,玩家就会眼睁睁看着远处 NPC 突然”活过来”或”僵住”,破坏沉浸感。

传统方案差在哪? “把世界整体加载进内存”——这是封闭关卡的做法,搬到开放世界便径直撞上内存墙:世界有多大、内存就要有多大,根本无从实现。退一步采用”加载即生成、可见即全量计算”同样难以为继:数百个远处路人全部运行完整 AI,CPU 预算被一群你根本看不清的小人耗尽,近处真正重要的演出反而卡顿。其根本误区在于把”无限大的世界”当作”有限的关卡”来对待


第三章 · 模拟核心:开放世界的灵魂

这一章要解决的问题:几百个角色、上百辆车、成片的物体,怎么让它们”像活的”,又不能每个都全量算。

这是全篇证据最厚、篇幅最大的一章。开放世界引擎真正的”内功”都在这里。我按一条线索展开:对象怎么组织 → 对象怎么被物理驱动 → 角色怎么活 → 复杂行为怎么编排 → 车怎么开。

3.1 实体怎么组织:Type × Owner 双正交(开放世界规模化的真秘密)

先问一个最底层的问题:在这套引擎里,世界里的一个东西——一个 NPC、一辆车、一个箱子——到底是什么?

如果你来自 UE 背景,第一反应是”它是一个 Actor”。但这套引擎的实体(Entity)不是 Actor。它更像是”世界托管的一条记录”。 这个差别看似抽象,却决定了整套规模化策略。

我们来看实体的组织。它沿两个完全正交的维度同时分类:

实体类层次 + Type×Owner 双正交

纵向是类层次(Type,”是什么”):最顶层是 Entity(世界里能被托管的一切),往下派生出 Physical(有物理存在的实体),再往下分成 Ped(角色)、Vehicle(载具)、Object(物件)等。这条线回答”这个东西是什么类型”。

横向是所有权维度(Owner,”谁拥有/从哪来”):同样一个 NPC,它可能是 population 系统自动生成的环境路人,可能是任务脚本专门 spawn 的剧情角色,可能是网络从别的玩家机器上复制过来的克隆体。这条线回答”这个东西归谁管、从哪来”。

关键在于:这两个维度是正交的。 一个 Ped(类型)可以是”环境生成的”,也可以是”脚本拥有的”,还可以是”网络克隆的”——类型不变,所有权可变。反过来,”脚本拥有的”这个所有权属性,可以挂在 Ped 上,也可以挂在 Vehicle 上。把这两个维度拆成正交的,意味着你不需要为”脚本拥有的 NPC””环境生成的 NPC””网络克隆的 NPC”各写一个类——类型负责行为,所有权负责生命周期管理,两者交叉组合。

这个设计最深的用意,是把”清理策略”编码进了实体状态。世界要持续运转,就必须不停地清理——把玩家看不到的、不重要的对象卸掉。但”哪些能清”取决于所有权:环境生成的路人可以随时清;脚本拥有的剧情角色绝对不能被随便清(否则任务就断了);网络克隆的对象的清理要和”真身”那台机器协调。如果所有权不是实体状态的一部分,清理系统就没法判断一个对象能不能动。 把它编码进状态,清理就变成了一个可以安全自动化的过程。

实体还有一个重要的”存在 vs 激活”分离:一个对象可以”物理上存在于世界”(占着位置、参与碰撞),但”行为上未激活”(不跑 AI、不更新)。这又是一种成本控制——远处的车可以物理存在(你能撞到它)但行为冻结(它不开)。

启发实体不该只有”类型”一个维度,还要有”所有权/来源”维度。 这是开放世界规模化最容易被忽略、却最关键的设计。当你的清理系统、你的联网、你的任务系统都需要知道”这个对象归谁、能不能动”时,你会无比庆幸当初把所有权做成了实体的一等属性,而不是事后到处打补丁判断。

3.2 dummy⇄real:把表示切换做成双向、分摊到多帧

承接上一节的”成本编码进状态”,最能体现这个思想的,是物件(Object)的 dummy⇄real 双向切换

dummy⇄real 表示切换

一个物件在世界里有两种表示:

  • 代理表示(proxy,对应 DummyObject):远处的物件用这个。它非常轻——基本只记位置和身份,不参与完整模拟。一座城市里成千上万的路灯、垃圾桶、长椅,远处时全是代理。
  • 完整实例(对应 Object):玩家靠近、可能交互时,物件”升格”成完整实例——有完整物理、能被推动、能交互。

两者之间的转换是双向的:玩家走近,dummy 升格成 real;玩家走远,real 降格回 dummy。而且这个转换被分摊到多帧完成——不能在某一帧里把一片区域的几百个 dummy 一起升格,那会造成一个巨大的帧尖峰。引擎把这些转换排队,每帧只处理一小批,让开销平滑。

转换还是所有权和可见性感知的——它会考虑这个对象归谁、玩家能不能看到,来决定升降格的时机,避免在玩家眼皮底下”啪”地变出一个物件。

这套机制的精髓是:它把”一个物件该用多重的表示”做成了对象自己的状态,并让这个状态随距离平滑迁移。 世界因此能容纳海量物件——绝大多数时候它们是几乎免费的 dummy,只有玩家身边那一小撮才付出完整模拟的代价。

启发表示切换要做成双向 + 分摊多帧 + 可见性感知。 单向的”加载就升格”很容易写,但会留下两个坑:一是没有降格,内存只增不减;二是不分摊,批量升格造成卡顿。把这三点一开始就做对,省去后面无穷的优化。

3.3 物理 gameplay 侧:pre/post 相位与伤害归因(damage attribution)

物理在这套引擎里也是”相位化”的——这又一次呼应了第一章。物理相关的工作被拆成”移动前”和”移动后”两个阶段,夹着中间的物理求解。

物理 pre/post 相位
  • 移动前(pre-physics):准备工作——施加力、设置约束、决定这一帧物理要怎么走。
  • 物理求解:引擎的物理引擎跑模拟步(这一段的底层求解器,是我的盲区,下面会说)。
  • 移动后(post-physics):模拟跑完、位置定稿之后的工作——伤害归因、force guard(力的保护性钳制,防止数值爆炸)、各种”物理后标志位”的重置、以及依赖最终位置的 Verlet 约束和浮力计算。

这里特别要拎出来讲的是伤害归因为什么必须在物理后做。一次伤害要算清楚”谁打的、用什么打的、打在哪个部位、穿过了什么”——这些信息很多要等碰撞实际发生、物理实际求解之后才完整。如果你在物理前就急着结算伤害,拿到的是不完整甚至错误的碰撞信息。所以伤害结算被放在 post-physics,等模拟把”到底撞上了什么”算清楚了再归因。

关于盲区:这套引擎的底层碰撞求解器——broadphase(粗筛)、solver 迭代(约束求解的内层循环)——我没有逐行读透。笔记里明确标注了这部分”未深读”。我能讲清楚的是”物理在整帧里的接入点和相位编排”,但”约束求解的数学内核”不在我的覆盖范围。这里如实标注。

启发凡是依赖”最终结果”的计算(伤害归因、贴地 IK、浮力),都要排到模拟之后的专门相位。 这是第一章帧相位思想在物理上的具体落地。把”什么时候算”想清楚,比把”怎么算”写对更影响架构。

3.4 自然运动 / Ragdoll:倒地反应与起身恢复

这类开放世界角色有一个标志性的体验:被撞、被打、从高处摔下来时,角色会瘫成一团”布娃娃”(ragdoll),而且——关键是——它还能从瘫倒状态自己爬起来,重新接管控制。这背后是一套物理驱动的自然运动动画系统。

自然运动状态流

它的状态流大致是:

  • 正常动画驱动:平时角色由动画系统驱动,姿态来自预制动画。
  • 触发转入 ragdoll:当受到足够的冲击(中枪、被车撞、坠落),角色”武装”(arming)进入 ragdoll——身体交给物理模拟,像真实人体一样瘫倒、翻滚。
  • 物理驱动的反应:不同的诱因有不同的反应预设——中枪是中枪的瘫法,坠落是坠落的姿态,被撞是被撞的翻滚。这些反应是物理模拟的,所以每次都不完全一样,显得真实。
  • 恢复(getup):当物理稳定下来,角色”爬起来”——从 ragdoll 的最终姿态平滑过渡回动画驱动,重新接管控制。

这套系统的难点全在状态过渡(transition):从”动画驱动”切到”物理驱动”要无缝(不能有一帧的姿态跳变),从”物理驱动”切回”动画驱动”更难(要从一个任意的瘫倒姿态,平滑地混合回一个标准的起身动画)。这套引擎在这两个方向上都做了大量工作——武装条件的判定(什么情况该转 ragdoll)、blend-from-NM(从自然运动混回动画)、各种诱因(shot/fall/impact)的专门反应。

关于盲区:自然运动系统的内部物理算法——它具体怎么用肌肉模型驱动关节、怎么平衡——属于更底层的物理动画范畴,我读到的是”它在整个角色系统里怎么被触发、怎么过渡、怎么恢复”,而不是”它内部的物理求解”。这里也标注一下。

启发倒地反应与起身恢复的真正难点不是 ragdoll 本身,是两个方向的状态过渡。 ragdoll 谁都能开(把骨骼交给物理就行),但从 ragdoll 平滑恢复到动画驱动,是体验好坏的分水岭。做这类系统,预算要重点压在过渡上。

3.5 NPC 的行为决策:感知—决策—任务流水线(perception-decision-task pipeline)

现在进入角色 AI 的核心。这类开放世界的 NPC 之所以让人觉得”像活的”,靠的不是一棵巨大的行为树,而是一条职责分离的认知流水线

感知→决策→任务流水线

这条流水线从感知到行为,分成几个明确的环节,每个环节只负责一件事:

  1. 扫描器(scanner):感知层。扫描周围环境——附近有什么人、什么车、发生了什么事件、有没有威胁。它只负责”看到”,不做任何决策。
  2. 智能体处理(Intelligence::Process):每个 NPC 的”大脑”入口。它在每帧的处理相位被调用,驱动后面的决策流程。
  3. 事件仲裁(EventHandler):感知收集到的事件可能有很多——听到枪声、看到尸体、被人推搡。事件处理器负责仲裁:这些事件里,哪个最重要、该响应哪个。
  4. 决策器(DecisionMaker)加权:决定”针对这个事件,这个 NPC 倾向于怎么反应”。注意这里是加权的——同一个事件,不同性格/关系/状态的 NPC 会有不同的反应倾向。一个胆小的路人看到枪战会逃,一个警察会上前。
  5. 响应工厂(ResponseFactory)路由:根据决策结果,把”该做什么”路由到一个具体的任务(Task)。
  6. 任务(Task):最终的行为执行者。前面几步决定”做什么”,任务负责”怎么做”。

这条流水线里藏着一个极其重要、也极其反直觉的设计:事件、决策、工厂这几个环节,没有一个是”最终行为的拥有者”。它们都只是中间裁决者。而且——一个完全合法的决策结果,可以是”什么都不做”(no-op)。

这句话值得展开。在很多 AI 系统里,”感知到事件”几乎等于”必须产生行为”。但这套引擎里,决策器完全可以裁定”这个 NPC 对这个事件的合理反应就是无视它”——比如一个见惯了世面的路人对远处的小摩擦无动于衷。“故意不反应”本身是一个一等的、合法的决策输出,而不是流水线的失败。这让 NPC 的行为有了真实世界的”惰性”——不是所有事件都引发反应,这恰恰是”像活的”的关键。

这比”行为树”深在哪?行为树是把”判断”和”行为”耦合在树的节点里——一个节点既判断条件又执行行为。而这条流水线把认知的每一拍都拆成独立的职责:感知归感知、仲裁归仲裁、加权归加权、路由归路由、执行归执行。每一拍都可以独立调整、独立替换、独立调试。你想改”这种 NPC 对这种事件更敏感”,去调决策器的权重就行,不用动感知,也不用动任务。

启发NPC 的”智能”是一条认知流水线,不是一棵行为树。 把”看到什么—觉得重要不重要—倾向怎么反应—具体做什么”拆成职责分离的几拍,比把它们塞进一棵树里耦合,要清晰得多、可调得多。尤其是“合法地什么都不做”要被设计成一等输出——这是 NPC 显得有”惯性”和”性格”的关键。

3.6 事件仲裁(event arbitration):多个事件同时涌入时怎么裁决

上一节的流水线里,”事件仲裁”这一环值得单独放大看,因为开放世界里 NPC 面对的从来不是单一事件,而是一堆事件同时涌入。

事件仲裁链

想象一个街角的 NPC,在同一帧里可能同时:听到了远处的枪声、看到了一辆超速的车、被路过的人撞了一下、注意到地上有个可疑包裹。这四个事件都想要它的注意力。事件仲裁链做的就是:把这些涌入的事件按优先级排序,决定该响应哪个,丢弃或推迟哪些。

排完序之后,胜出的事件进入决策器加权,再由响应工厂路由到具体任务。整条链路是”漏斗式”的——很多事件进来,经过仲裁和加权,最后只有一个(或少数几个)变成实际行为。

这个仲裁机制的价值在于,它让 NPC 的反应具备了优先级:枪声一响,它便不会还从容地去拾取那个包裹;遭到撞击时,它会优先处置眼前这一击。倘若没有仲裁,NPC 要么对所有事件同时作出反应、陷入顾此失彼,要么严格按收到的先后逐一处理、显得机械迟滞。

启发多事件并发时,”优先级仲裁”是 NPC 行为合理性的关键一环。 别让 NPC 平等对待所有事件——给事件分优先级,让重要的压住次要的,行为立刻就合理了。

3.7 任务系统:任务树 + 数值优先级

如果说感知-决策是”决定做什么”,那任务系统就是”怎么把要做的事组织成可执行、可嵌套、可打断的行为”。这是整套引擎证据最厚的一块——光是带”任务”的笔记就有近百篇。

多棵任务树 + 数值优先级

它的组织有两个要点。

第一,复杂行为是任务”树”。 一个高层任务(比如”去攻击那个目标”)会分解成子任务(移动到掩体、瞄准、开火、换弹),子任务又可以再分解。任务以树的形式嵌套,高层任务管宏观意图,叶子任务管具体动作。一个 NPC 身上同时挂着多棵这样的树(图里画了四棵,对应不同的行为子系统——比如运动、战斗等并行的关注点)。

第二,任务之间靠数值优先级仲裁。 这是最关键的设计:当多个任务想要控制同一个 NPC(或 NPC 的某个部分)时,谁说了算?不是靠 if-else 写死的规则,而是每个任务带一个数值优先级,高的压低的。一个”逃命”任务的优先级会高于”闲逛”任务,所以枪一响,逃命自然压过闲逛接管控制——你不需要在闲逛任务里写”如果听到枪声就停止闲逛”这种耦合逻辑,优先级机制自动处理了抢占。

这个”数值优先级仲裁”的意义,怎么强调都不为过。它把”什么情况下该做什么”从命令式的 if-else 堆,变成了声明式的优先级竞争。你加一个新行为,只需要给它定一个合理的优先级,它就会自动在该出现的时候压过低优先级行为、在不该出现时让位给高优先级行为。系统的复杂行为不是被”写”出来的,是被任务树按优先级”仲裁”出来的。

任务系统里还有一类特殊的任务——能感知网络的任务(我们在第六章会专门讲)。这里先埋一个点:任务的基类设计里,就预留了”这个任务在网络环境下是本地真身还是远程克隆”的区分。也就是说,网络感知不是事后包一层,而是从任务这个最基础的行为单元就内建了。这是这套引擎联网设计的精髓之一,第六章展开。

启发复杂 AI 行为应该是”任务树 + 数值优先级仲裁”,不是 if-else 堆。 用优先级让行为自动抢占/让位,比手写状态转移条件,可维护性高一个数量级。每加一个行为只需定一个优先级,而不是去改一堆已有行为的判断条件——这是 AI 系统能持续长大而不腐烂的关键。

3.8 载具:车拥有 AI,NPC 只下发意图

最后是载具。这套引擎在”NPC 开车”这一问题上的设计,是全文最具启发性的反直觉拆分之一。

直觉上,”一个 NPC 开车”应该是:NPC 拥有”开车”这个行为,车只是个被操作的对象。但这套引擎反过来:车拥有持久的驾驶 AI,NPC 只负责”下发意图”。

载具"车拥有 AI、NPC 下发意图"

具体来说:

  • 载具自己有一个智能体(VehicleIntelligence),它拥有一个持久的驾驶循环——怎么巡航、怎么避障、怎么过弯、怎么遵守交通。这个循环属于车,不属于开车的人。
  • NPC(驾驶员)只提供”意图”:去哪里、用什么方式开(巡航?追击?逃跑?)。它把意图下发给车,车的驾驶 AI 接过意图,自己执行具体的驾驶。
  • 两者通过一个双向的”装任务”机制连接:NPC 身上挂一个”控制载具”的任务,车上挂对应的驾驶执行任务,两边配对协作。

为什么这么设计?因为驾驶逻辑的归属,本质上属于车而不是人。 一辆车怎么开——它的转向特性、它的避障、它和路网的交互——是车的属性。如果把驾驶循环放在 NPC 身上,那么每次 NPC 上车下车,这套复杂的驾驶状态就要在人和车之间搬来搬去;而且同一辆车换个司机,整套驾驶逻辑要重新初始化。把驾驶循环固定在车上,人只是”换了个发指令的”,车的驾驶状态保持连续——这干净得多。

这又一次体现了全文的第二条副线——分层所有权。”谁拥有驾驶循环”这个问题,答案是车,不是人。把所有权放对位置,整个系统的耦合就解开了。

启发“操作者”和”被操作系统的控制循环”未必归属同一方——想清楚控制循环到底属于谁。 “NPC 开车,所以 NPC 拥有驾驶逻辑”是个想当然的错误。让车拥有驾驶 AI、人只下发意图,是更干净的所有权划分。做任何”主体操作复杂对象”的系统时,都值得反问一句:控制循环放在主体身上,还是放在被控对象身上更合理?

3.9 载具执行器:一个矩阵覆盖所有载具类型

车拥有驾驶 AI,但”开车”这件事本身有很多种类型——巡航、前往某点、追击、警察拦截、编队、降落(对飞行器)——而载具又有很多种:汽车、船、直升机、飞机、潜艇。这套引擎用一个执行器矩阵来组织它们。

载具执行器矩阵

如上图,把它理解成一张二维表:行是载具类型(车/船/直升机/飞机/潜艇),列是驾驶行为(巡航/前往/追击/警察行为/编队/降落)。 每个格子是一个具体的执行器——”直升机怎么追击””船怎么巡航””飞机怎么降落”各是一段专门的实现。

这种矩阵式组织的好处是职责清晰、易于扩展:要加一种新载具,就填一行;要加一种新驾驶行为,就填一列。每个格子互相独立,改”汽车的追击”不会影响”船的巡航”。同时,所有这些执行器都接在第 3.8 节那套”车拥有 AI、人下发意图”的框架下——上层只管下发”追击”这个意图,具体是”直升机的追击”还是”汽车的追击”,由矩阵对应的执行器负责。

载具这一块还连着交通系统——路网、路口、红绿灯。NPC 车辆在城市里跑,靠的是预生成的车辆路网(这条路网怎么来的,正是第七章”离线内容管线”的产物之一),加上路口的通行决策、红绿灯的状态循环。一辆 AI 车在十字路口要不要停、什么时候走,是路网数据 + 红绿灯状态 + 驾驶 AI 三者共同决定的。

启发当一个行为要覆盖多种对象类型时,”类型 × 行为”的矩阵式分解,比继承爆炸或大 switch 都清晰。 每个格子独立实现、独立维护,加类型加一行、加行为加一列,是这种多对多组合问题的优雅解法。

🔧 设计复盘 · 模拟核心(实体双正交 / 认知流水线 / 任务优先级 / 所有权拆分)

为什么会这样设计? 这一整章都被同一个压力塑造:海量对象既要像活的、又不能每个都全量算。实体加上 Owner 维度,是为了让清理系统能安全判断”谁能动”;dummy⇄real 把表示重量编码进状态,是为了让世界默认便宜、只在玩家身边变贵;NPC 用认知流水线而非行为树,是为了让”合法地什么都不做”成为一等输出,NPC 才有惰性和性格;任务用数值优先级仲裁,是为了让新行为靠定优先级就能自动抢占/让位;车拥有驾驶 AI、人只下发意图,是因为驾驶循环本就属于车——这些全是”分清所有权 + 把成本编码进状态”两条副线的具体落地。

踩过什么坑? 最深的坑都在所有权没分清的地方。如果实体只有类型、没有来源,清理系统会误删剧情角色、任务直接断掉;如果 dummy⇄real 只升不降、或不分摊多帧,要么内存只涨不跌、要么一片区域同帧升格造成卡顿尖峰;如果把驾驶循环放在 NPC 身上,每次上下车都要在人和车之间搬运一大坨驾驶状态,换个司机就得重置整套逻辑。这些坑共同的根源是:把”谁拥有这个状态/循环”想当然了。

传统方案差在哪? “用一棵巨型行为树管理所有 NPC 行为” + “以 if-else 堆叠判断何时该做什么”,是最常见的 NPC AI 写法,其症结在于腐烂:每新增一个行为,都得回头修改大量既有节点的判断条件,耦合层层累积,到最后整棵树已无人敢动。”NPC 拥有驾驶逻辑”这一想当然的所有权划分,则会陷入状态搬运的泥潭。而”实体只有类型、没有所有权”会在清理与联网两处接连失守——你永远说不清一个对象究竟能否被回收、它的真身又在何处。


第四章 · 玩法系统:把模拟变成游戏

这一章要解决的问题:模拟跑起来了——NPC 会活、车会开、物理会响应——但这还不是”游戏”。怎么给它加上”有目标、有规则、有反馈”的玩法层?

模拟核心解决的是”世界自己怎么运转”,玩法系统解决的是”玩家在这个世界里能做什么、做了会怎样”。这一章的几块——武器、伤害、通缉、存档、脚本——就是把模拟变成游戏的规则层。

4.1 武器系统:从控制到表现的一条链

武器是动作类开放世界的核心交互。这套引擎的武器系统不是”一把枪一个类”那么简单,而是一条从数据到控制到表现的链:

  • 武器控制(weapon control):管理”当前装备什么武器、怎么开火、怎么换弹、怎么切换”的运行时状态。
  • 元数据工厂(metadata factory):武器的属性——伤害、射速、后坐力、弹道——是数据驱动的。工厂根据元数据创建武器实例。要加一把新枪,主要是加数据,不是写代码。
  • 武器轮盘 / 电台轮盘(weapon/radio wheel):玩家切换武器的那个轮盘 UI,以及结构类似的电台选择。
  • 准星与精度(reticule / accuracy):准星的表现,以及命中精度的计算——精度不是固定的,受移动、瞄准、武器属性影响。

这套设计的核心还是数据驱动——武器的差异主要由元数据表达,控制逻辑是通用的。这让武器系统能容纳大量武器而不膨胀代码。

4.2 伤害系统:伤害是一条归因链

如果说武器是”输出”,伤害系统就是”结算”。这套引擎的伤害系统有一个很值得学的视角:伤害不是一个数字,是一条归因链。

伤害归因链

一次伤害要依次回答一连串问题:

  1. 谁打的(attacker):伤害的来源是哪个实体?这关系到后续的击杀归属、通缉定性。
  2. 用什么打的(weapon):什么武器/什么弹种?不同武器有不同的伤害特性。
  3. 打在哪(hit location):命中哪个部位?爆头和打腿的结算完全不同。
  4. 穿过/挡了什么(armor/resistance):护甲、抗性、耐力(endurance)如何削减伤害?
  5. 最终算多少(final damage):综合以上,算出实际扣血。
  6. 归属与定性(kill tracker / crime):如果致死,击杀归给谁?这次伤害是否构成犯罪、该不该触发通缉?

把伤害做成一条链,而不是一个 TakeDamage(int) 函数,好处是每一环都可以独立扩展:要加一种”特殊弹种穿透护甲”的逻辑,去改”挡了什么”那一环;要加一种”特定部位暴击”,去改”打在哪”那一环。而且这条链天然地把”伤害”和”它的后果”(击杀归属、犯罪定性)连了起来——你打死了谁、算不算犯罪,是同一条链的末端,信息完整。

启发把伤害设计成”归因链”而非”一个数字”。 attacker → weapon → location → mitigation → amount → consequence 这条链,让伤害的每个环节可独立调整,也让”伤害的后果”(击杀、犯罪)有完整上下文。做战斗系统时,先把这条链画出来,再填每一环。

4.3 通缉与派遣:玩家犯罪后,世界如何响应

这类开放世界最有标志性的玩法循环之一是通缉系统——玩家一旦犯罪,警察就会前来追捕,玩家逃逸,警力随之逐级升级。这套引擎里,它不是”刷几个警察”那么简单,而是一个派遣编排(dispatch orchestration)系统

通缉/dispatch 派遣编排

整个流程是:

  1. 玩家实施犯罪:伤害链末端定性出”这是犯罪”,或者玩家直接做出违法行为。
  2. 通缉等级:根据罪行严重程度,通缉等级上升。等级决定了世界要派多强的力量来对付你。
  3. 事件队列:犯罪进入一个队列,被组织成”事件”(incident)——比如”在某地有一起需要响应的犯罪”。
  4. 派遣编排:这是核心。一个派遣管理器根据通缉等级,决定派什么单位、派多少、怎么协同。低等级派巡警;等级升高,派出警车围堵、设路障(roadblock);再高,SWAT、军队、帮派依次登场。
  5. 逐级升级:随着你持续逃脱、对抗,派遣的强度按预设逐级升级。

关键认知是:“玩家犯罪→世界如何响应”是一个带编排逻辑的派遣系统,而非简单地在玩家周围生成警察。 它有”事件”的概念(何处发生了何种情况、需要怎样的响应)、有”单位类型”的层级(巡警/SWAT/军队/帮派)、有”协同”的逻辑(如何围堵、如何设置路障)、有”升级”的节奏。正是这套编排,让通缉体验拥有了层次感与压迫感的递进,而不是单调地不断增援。

这套派遣系统其实可以抽象成一个更通用的模式:“世界对玩家行为的规模化响应”。不只是警察——任何”玩家做了某事,世界要组织一波有结构的反应”的玩法,都可以套这个”事件队列 + 单位分级 + 编排升级”的框架。

启发“世界对玩家的反应”值得做成一个独立的派遣编排系统。 把”刷敌人”升级成”事件队列 + 单位分级 + 协同编排 + 逐级升级”,体验的层次感完全不同。这是开放世界”对抗感”的核心,远比单纯调数值重要。

4.4 脚本桥:玩法逻辑怎么挂到引擎上

前面讲的武器、伤害、通缉,都是引擎层提供的”能力”。但一款游戏的具体玩法——这个任务要你去哪、那个事件触发什么剧情——是用脚本写的。脚本怎么和引擎连接?这套引擎用了一个清晰的四层桥

脚本四层桥

从上到下:

  1. 游戏脚本(script):玩法逻辑本身,由设计/脚本人员编写——”任务流程””触发条件””剧情编排”都在这层。
  2. 线程管理(thread):脚本是以”线程”的形式运行的,有一个管理器负责调度这些脚本线程、管理它们的生命周期。
  3. 处理器(handler):管理脚本运行中产生的资源、ID、状态。
  4. 引擎桥(native command):最关键的一层。脚本要操作世界(移动一个 NPC、生成一辆车、检测射线),是通过”原生命令”(native command)调用引擎能力的。这些原生命令就是脚本世界和引擎世界之间的桥——脚本说”把这个 NPC 移到那里”,原生命令把它翻译成引擎的实际操作。

这些原生命令是按领域分组的:操作实体/NPC/载具/物件的一组、操作任务/玩家控制/输入的一组、做路径/射线检测/物理查询的一组、管流式/内饰的一组。分组让庞大的命令集有条理。

这个设计的意义在于,它划清了“玩法逻辑”和”引擎能力”的界线:脚本层不直接碰引擎内部,只能通过原生命令这道窄桥调用引擎暴露的能力。这让玩法可以快速迭代(改脚本不用动引擎),也让引擎能力的暴露面是受控的(只有原生命令暴露的才能被脚本用)。这和第一章 1.1 节”引擎与游戏解耦”是同一种思想,在脚本层的再一次体现。

启发玩法逻辑和引擎能力之间,要有一道”原生命令”式的窄桥。 脚本通过受控的命令集调用引擎,而不是直接戳引擎内部——这让玩法能高速迭代、让引擎接口可控。这道桥的设计质量,直接决定了你的玩法迭代速度。

4.5 存档:一个被低估的异步状态机

存档(save/load)听起来像是件边角杂务,但在开放世界里,它是一个独立的大型子系统——而且这套引擎将它打磨得相当考究:一个异步队列状态机

存档异步队列状态机

为什么存档要做成异步状态机?因为开放世界的存档数据量大(整个世界状态、玩家进度、载具、属性……),如果同步写盘,会让游戏卡住好几秒。所以存档被设计成一个不阻塞主流程的异步流程:

  1. 请求(request):玩法触发一次存档。
  2. 排队(queue):存档请求进队列,不立即执行,等合适的时机。
  3. 分块写(block):把庞大的存档数据切成块,分批序列化,避免一次性占用过多。
  4. 落地到设备/云(device/cloud):写到本地存储,或同步到云端。云存让玩家换设备能继续。
  5. 完成(complete):状态机走到终态,通知玩法存档成功。

整个过程是一个状态机——每一步都是一个状态,异步推进,任何一步出错都能被捕获和处理。此外它还要处理迁移(不同版本/平台之间的存档导入导出)——这又是一层复杂度。

把存档做成异步状态机 + 云存 + 分块 + 迁移,意味着它早就不是 stats(统计数据)的附属品,而是一个有自己生命周期管理的独立子系统。这是很多人低估的地方——以为存档就是”序列化个结构体写文件”,实际上一个商业开放世界的存档,复杂度堪比一个小型数据库的写入引擎。

启发存档是独立大子系统,要按”异步状态机”来设计,不是”序列化写文件”。 异步(不卡主线程)、分块(不爆内存)、云存(跨设备)、迁移(跨版本)——这四个要求决定了它必须是一个有完整生命周期的状态机。早点正视它的复杂度。

关于盲区:玩法系统里有几块我没有读到专门的实现——拾取(pickup)系统(武器/钱/补给的掉落拾取,只在录像的镜像层里被间接引用,没有独立的运行时主笔记)、车辆改装系统本体(外观/性能改装的数据流,只有一点点擦边)、贴花/弹孔损伤(decals,没见到专篇)。这几块在这类开放世界里都是实打实要做的,但不在我这次的覆盖范围里,如实标注为空白。

🔧 设计复盘 · 玩法层(伤害归因链 / 派遣编排 / 脚本桥 / 异步存档)

为什么会这样设计? 玩法层的设计压力是”把模拟变成有规则、有反馈、可迭代的游戏”。伤害做成归因链,是因为”算多少伤害”和”这次伤害的后果(击杀归谁、算不算犯罪)”本就是同一条信息流,拆成函数会丢上下文;通缉做成派遣编排,是因为”世界对玩家行为的反应”需要层次与升级节奏,而非平铺式地堆叠敌人;脚本通过原生命令窄桥调用引擎,是为了让玩法能高速迭代而引擎接口可控;存档做成异步状态机,是因为开放世界的存档数据量大到同步写盘必然卡死主线程。

踩过什么坑? 存档是重灾区:以为”序列化个结构体写文件”就行,结果世界状态一大,同步写盘卡顿几秒;加了云存又要处理冲突,跨版本又要处理迁移——每一步都比想象的复杂。伤害这边的坑是过早结算:在物理求解完成前就急着算伤害,拿到的是不完整甚至错误的碰撞信息,所以伤害归因必须排到 post-physics 相位(这又回到第一章的相位思想)。

传统方案差在哪? 通缉系统”在玩家周围直接生成警察”是最常见的省事做法,其短板在于缺乏层次——警力不断增援,体验却毫无递进,玩家感受到的只是数量的堆叠,而非压迫感的升级。存档”同步序列化写文件”则受制于卡顿与不可扩展:单机小存档尚能应付,开放世界的大存档一次写入便是数秒卡死;更何况日后要追加云存、要处理跨版本迁移时,一个没有生命周期管理的同步函数根本无从扩展。


第五章 · 表现层:玩家看到、听到、操作的(点到为止)

这一章要解决的问题:把模拟的结果,呈现给玩家——画面、声音、界面、操作。

这里要先说清边界:这一章我只到结构层。 渲染的相位骨架、UI 的层次组织、输入的抽象方式,我能讲清楚;但 shader 的数学、GPU 的具体算法、音频的 DSP,是我的盲区。凡是涉及算法内核的地方,我都会明确标注——能讲的讲透,不能讲的标清楚。

5.1 渲染管线:四段骨架与相位 DAG(只讲所有权和顺序)

渲染在这套引擎里也是相位化的——又一次呼应第一章。一帧的渲染大致分四段:

渲染四段 + render-phase DAG
  1. 扫描(scan):可见性判定。从相机出发,确定这一帧哪些东西可能可见——视锥剔除、遮挡剔除。
  2. 渲染列表(render list):把扫描出来的可见对象,组织成有序的渲染列表。
  3. 预渲染(prerender):渲染前的准备——更新需要渲染的状态、准备资源。
  4. 渲染相位(render phase):实际的绘制,组织成一个 DAG(有向无环图)——延迟光照、级联阴影、多种反射……每个渲染阶段是 DAG 的一个节点,节点之间有依赖顺序。

我能讲清楚的是这四段的所有权和顺序——谁在什么时候做什么、阶段之间的依赖关系。这其中有一点值得记住:“主更新”并非一帧的全部,渲染自有一套相位。 模拟结束并不等于一帧结束,渲染还要走完它自己的 DAG。这再次说明,”一帧”是被精心切分为多个有序阶段的,而非混作一团、不加区分地一并处理。

关于盲区(明确标注):渲染管线里所有的算法内核都是我的盲区——延迟光照具体怎么算、级联阴影的实现、各种反射的技术细节、shader 里的数学。笔记里这部分明确停在”材质库/绘制列表状态”的结构层,没有深入 GPU 数学。所以这一节我只敢讲”渲染相位怎么编排”,不敢讲”像素怎么算出来”。这是一条清晰的能力边界。

启发即便不碰 shader 数学,理解”渲染相位的所有权和顺序”也极有价值。 渲染不是”调一次绘制”,是一个有依赖关系的相位 DAG。知道这个骨架,至少能让你在排查”为什么这东西没渲染对””为什么顺序错了”时有谱。算法可以交给专家,但相位编排是架构师必须懂的。

5.2 音频:帧编排清晰,DSP 是盲区

音频系统我同样只到结构层。能讲的是它的帧编排——音频根引擎在每帧里以特定的顺序调度各个音频子系统;以及它的组织:语音/对话/扫描器(NPC 说话、警察广播)、载具音频(引擎声、动态混音)、电台/环境音/音乐。

但音频的算法内核——DSP、混音、遮挡计算(声音怎么被墙挡住、怎么衰减)——是盲区。我读到的是”音频系统在整帧里怎么被编排、分成哪些子系统”,没读到”声音信号具体怎么被处理”。这里如实标注。

5.3 UI 与前端:从 HUD 到手机的层次

UI 这块证据相对结构化,能讲清楚它的层次。一个开放世界的前端比想象的复杂得多:

UI 前端层次

从底到上大致是:

  • HUD 核心:血条、小地图、武器指示这些常驻元素的注册、更新、渲染外壳。
  • 矢量 UI 基底:一套矢量 UI 运行时(基于类 Flash 的技术),负责把 UI 资源渲染出来。
  • 内置 HTML/CSS 栈:这是个罕见的发现——引擎里居然自带了一套 HTML 解析器、文档模型、CSS 渲染器、视口。某些前端界面是用引擎内置的这套 HTML 栈渲染的。在游戏引擎里塞一个浏览器级的 HTML 渲染栈,并不常见。
  • 暂停菜单 / 上下文菜单 / 警告屏 / 页签栈:复杂的菜单系统,有自己的数据、动态布局、设置项、上下文管理。
  • 小地图 / 雷达 / GPS / 路线:开放世界的导航 UI 是一个专门的子系统——小地图的核心 + 渲染线程协作、GPS 路线计算、矢量地图、航点。这套”导航体验”独立于通用 HUD。
  • 手机 / companion app:这类开放世界标志性的手机交互——它是用一个渲染目标(render target)画出来的,里面有各种 app,甚至包括自拍相机。这是一个独立的前端应用层。

UI 这一层的特点是子系统极多、各管一摊。HUD、菜单、小地图、手机,每个都是相对独立的前端子系统,共用底层的渲染基底(矢量 UI 运行时 / HTML 栈),但上层逻辑各自独立。

启发开放世界的前端远不止”一个 HUD”——导航 UI、手机、菜单都是独立子系统。 别低估前端的工程量。小地图/GPS 是专门的导航体验、手机是独立的应用层,它们和血条不是一个量级的东西。规划 UI 时按”多个独立前端子系统 + 共享渲染基底”来组织。

5.4 输入与设备:能玩的前提,常被忽视的硬功夫

最后是输入。这是一整层在很多技术分享里被完全忽略、但实打实是”能玩”前提的东西。

输入/设备抽象

它的核心是一个统一输入抽象

  • 底层是各种物理设备:键盘鼠标、手柄(带振动/体感)、触摸板。
  • 中间是统一抽象层:把不同设备的输入归一化成游戏能理解的”动作”。游戏逻辑不直接问”A 键按了吗”,而是问”跳跃动作触发了吗”——具体是键盘的空格还是手柄的 A,由抽象层映射。
  • 上层是一系列配套能力:重映射(玩家自定义按键)、手柄校准、振动反馈、IME/虚拟键盘(多语言文本输入)、输入禁用门控(某些状态下屏蔽某些输入)。

这一层的价值是跨平台和可定制:同一套游戏逻辑,通过输入抽象层,能同时支持键鼠和各种手柄;玩家能重映射按键;多语言玩家能输入文本。这些都不是”锦上添花”,是”能不能玩”的硬门槛——一个手柄校准不对、一个 IME 不支持,对应平台/语言的玩家就被挡在门外。

启发输入抽象是”能玩”的前提,要早做、做扎实。 游戏逻辑应该对”动作”编程,而不是对”具体按键”编程,中间隔一个映射层。重映射、手柄校准、IME 这些看着琐碎,但少一个就少一批玩家。这是最容易被推迟、最不该被推迟的一层。

🔧 设计复盘 · 表现层(渲染相位 DAG / UI 子系统 / 输入抽象)

为什么会这样设计? 渲染做成相位 DAG,是因为延迟光照、阴影、反射之间有硬依赖顺序,必须显式编排(这又是第一章相位思想在 GPU 侧的延续);UI 拆成 HUD/菜单/小地图/手机多个独立子系统、共享同一渲染基底,是因为它们的迭代节奏与团队归属各不相同,强行并入”一个 HUD”只会彼此掣肘;输入做成”设备 → 统一抽象 → 动作”,是为了让一套游戏逻辑能跨键鼠和各种手柄、还能让玩家重映射。

踩过什么坑? 这一章我最大的”坑”其实是知道自己读到哪为止——渲染的相位编排我能讲,但 shader 数学、级联阴影、延迟光照的算法内核是盲区;音频的帧编排我能讲,但 DSP / 混音 / 遮挡计算是盲区。把这条能力边界标清楚,本身就是避免”想当然踩坑”的方式。真要说工程坑:输入若直接对”具体按键”编程,后期想加手柄、加重映射就要满地改判断,这是最常见的返工。

传统方案差在哪? “在游戏逻辑里直接判断按键”(if 按下空格 then 跳)是原型阶段最快的写法,其短板在于跨平台与可定制性——一旦要支持手柄、要允许玩家改键、要兼容多语言输入,每一项都得回头翻遍代码逐处修改判断,最终被层出不穷的平台与无障碍需求拖垮。把所有元素一股脑并入”一个大 HUD”同样难以为继:小地图、手机、菜单的逻辑量根本不在一个量级,一旦糅作一团,便再也无人能改。


第六章 · 联机与在线服务:多人同步 + 商业运营

这一章要解决的问题:多个玩家共享同一个世界,状态怎么同步?以及,怎么支撑起一整套商业运营?

联机是开放世界从”单机体验”走向”持续运营的服务”的分水岭。这一章的核心亮点,是一个把”网络感知”做到骨子里的复制架构。

6.1 复制:语义 vs 传输的三层 clone

多人游戏的核心问题是:我这台机器上的一个对象(比如我的角色),怎么”出现”在你那台机器上?最朴素的做法是”把对象的状态打包发过去,你那边重建一个”。但开放世界的对象太复杂、太多,朴素做法会在带宽和一致性上双双崩溃。

这套引擎的做法,是把复制拆成语义、同步树、传输三层,层层分离。

语义 vs 传输三层 clone
  • 语义层(谁拥有状态):每个被复制的对象,在某一台机器上是”真身”(authority,拥有权威状态),在其他机器上是”克隆”(clone,镜像)。语义层管的是”这个对象的真身在哪、谁说了算”。真身机器上对象状态的改变,才是权威的;克隆机器上的对象只是反映真身的状态。
  • 同步树/节点层(怎么序列化):一个对象的状态不是铁板一块,而是被拆成多个同步节点(sync node)——位置是一个节点、生命值是一个节点、当前任务是一个节点……每个节点可以独立地、按需地序列化。一个同步树把一个对象的所有同步节点组织起来。这样做的好处是按需同步:只有变化的节点才需要发送,没变的不发,极大节省带宽。
  • 传输层(网络对象克隆):实际的网络对象(NetObj)和它们的管理器,负责把序列化后的节点数据在网络上收发、负责对象的创建/删除/作用域(scoping,谁该看到这个对象)/迁移(migration,真身从一台机器转移到另一台)。

这三层分离的威力,在于它把”语义一致性”和”传输效率”解耦了:语义层只关心”谁是真身”,不关心怎么传;传输层只关心”高效地把数据搬过去”,不关心语义。中间的同步树层做翻译。

但真正令人击节的,是这套架构一处更深的设计——网络感知,是从”任务”这一最基础的行为单元便已内建的。还记得第三章 3.7 节埋的那个点吗?任务的基类设计里,就区分了”本地真身的任务”和”远程克隆的任务”。也就是说,当一个 NPC 被复制到其他玩家机器上,它身上跑的任务”知道”自己是个克隆——克隆上的任务不会去做权威决策,而是去反映真身上对应任务的状态。

这意味着网络不是事后”加一层同步”包在游戏逻辑外面,而是从任务层、从行为的最小单元就分清了”语义 owner 和传输”。 这是极高的设计起点。大多数游戏是先做单机逻辑,再痛苦地往上套网络同步;而这套引擎是从最底层的行为单元开始,就把”我是真身还是克隆”内建进了设计。这种”网络原生”的架构,是它能支撑大规模在线开放世界的根本。

启发网络复制要从”语义 owner vs 传输”的分离做起,而且越早内建越好——最好下沉到行为的最小单元。 “先做单机再套网络”是大多数项目的痛苦之源。如果你知道游戏要联网,从一开始就让你的核心对象、核心行为单元带上”真身/克隆”的概念,把”谁拥有权威状态”作为一等设计,会省下后期无数的同步地狱。这是全文第二条副线(分层所有权)在网络上的终极体现。

6.2 同步树与节点(sync tree / node):把对象状态拆成可独立同步的片

上一节提到同步树,这里放大看一下,因为它是”高效同步”的关键机制。

同步树/节点结构

核心思想很简单但很有效:一个对象的状态,不作为一个整体同步,而是拆成多个独立的同步节点。

想象一个被复制的角色。它的状态包括:位置/朝向、生命值/护甲、当前动作/任务、外观、持有的武器……如果每次同步都把整个角色打包发送,哪怕只有位置变了也要发全部,浪费巨大。

同步树的做法是把这些拆成独立节点——位置节点、生命值节点、任务节点等等——每个节点独立追踪自己有没有变、独立序列化。这一帧只有位置变了,就只发位置节点;生命值掉了,就只发生命值节点。没变的节点一个字节都不发。

这种”细粒度按需同步”是带宽优化的核心。在一个有几十个玩家、每个玩家周围有大量被复制对象的开放世界里,带宽是最稀缺的资源。把状态拆成节点、只发变化的节点,是让带宽撑得住的关键。

启发同步的粒度要细到”状态字段级”,按需发送。 别整对象打包同步——拆成独立追踪、独立序列化的节点,只发变化的部分。在玩家数和对象数都大的开放世界里,这是带宽能不能撑住的分水岭。

6.3 Replay:镜像而非权威

这套引擎里还藏着一个庞大的子系统——录像/回放(Replay),证据厚到能单独写一篇。它支撑了游戏内的录像功能和视频编辑器。它的设计有一个核心哲学值得讲:镜像,而非权威。

Replay 镜像层

什么意思?录像系统在游戏运行时,“镜像”地记录世界的状态——它在一旁观察并记录每个相关对象(角色、载具、物件、相机……)的状态变化,但它不是这些状态的权威。游戏照常运行,录像只是把发生的一切”抄录”下来。回放时,录像系统用记录的数据重建当时的场景。

这个”镜像而非权威”的定位,和第六章 6.1 节的网络 clone 是同一种思想的另一个应用——观察者(observer/recorder)和拥有者(owner)的分离。录像系统是个纯粹的 observer/recorder,它不拥有任何游戏状态,只是旁观和记录。这让它能”挂在”正常游戏流程旁边,而不干扰游戏本身的运行。

录像系统的实现是分层的:核心 + 缓冲 + 一系列适配器(针对游戏/相机/物件/角色/载具/拾取各有一个适配器,负责”怎么记录这类对象”)+ 各种数据包族 + 存储 + 预加载 + 压缩 + 叠加显示。这套东西的完整度,已经相当于一个内建于游戏的录制引擎。

而在它之上,还有一个完整的视频编辑器——时间线、标记、回放控制、文件浏览、导出、水印。这等于在游戏里塞了一个小型的视频编辑软件。这部分我只到结构层,但能看出它的规模:一个游戏里藏着一个视频剪辑产品。

启发“观察者/记录者”和”拥有者”的分离,是个能反复复用的强模式。 录像系统作为纯 observer 挂在游戏旁边、不拥有状态,所以能无侵入地记录一切。任何”需要旁观记录但不该干扰主流程”的需求(录像、回放、调试快照、遥测),都该用这个”镜像而非权威”的定位。注意它和网络 clone 是同构的——都是 owner 之外的一个非权威视角。

6.4 在线服务、社交与经济:开放世界即服务

联机不只是”几个人一起玩”,对一款持续运营的商业开放世界,它背后是一整套在线服务、社交、经济。这部分我只到结构层,但它的存在本身就说明了”开放世界即服务”的商业模式是有完整代码体现的:

  • 社交(Social Club 式):动态消息流(feed)、收件箱、新闻、战队(crew)、在线状态(presence)。这是把玩家连成一个社区的社交层。
  • 商城与经济:商品目录、库存、交易、店面浏览。游戏内购买、虚拟经济,都在这一层。
  • 落地页与店面:玩家进入在线模式时看到的落地页、独立店面、广告位——这是把”在线模式”包装成一个有商业入口的产品门面。
  • 云与可调参数(tunables)与 UGC:云端管理、可远程下发的调参(让运营能不发版就调整游戏参数)、用户生成内容(UGC)的查询和管理。
  • 统计、货币、成就:玩家数据、货币接口、成就系统、账号数据。
  • 审核与举报:内容审核、举报/表扬、车牌(自定义内容)的合规、法律限制。

这一整圈,是把一款游戏变成一项”持续运营的服务”所必需的基础设施。可远程下发的 tunables 让运营能实时调整、云存让进度跨设备、社交流让玩家留存、商城让商业模式成立、审核让 UGC 不失控。这些代码的存在,本身就是”长青在线开放世界”这类商业模式的技术地基。

启发如果目标是”持续运营的开放世界”,在线服务/社交/经济这一整圈是地基,不是附属。 尤其是可远程下发的调参(tunables)——它让运营能不发版就调平衡、做活动,是长期运营的命脉。立项做在线开放世界,这一圈要和玩法同等重视。

🔧 设计复盘 · 联机(三层 clone / 节点级同步 / 镜像而非权威)

为什么会这样设计? 复制拆成”语义 / 同步树 / 传输”三层,是为了把”谁拥有权威状态”和”怎么高效传输”彻底解耦——语义层只管真身在哪,传输层只管搬数据,互不污染。状态拆成节点级、只发变化的部分,是因为几十个玩家、每人周围一大片被复制对象时,带宽是最稀缺的资源。而把网络感知下沉到任务这个最小行为单元(任务自己知道”我是真身还是克隆”),是这套架构的最高明处——它让”网络原生”成为事实,而不是事后补丁。录像定位成”镜像而非权威”同理:它作为纯观察者挂在游戏旁边,所以能无侵入地记录一切。

踩过什么坑? 联机最大的坑是”先做单机、再套网络”。等单机逻辑全写完,才发现核心对象、核心行为里到处是”想当然只有一份、想当然本地说了算”的假设——要联网就得逐个回去拆”谁是真身、状态怎么同步”,这是伤筋动骨的返工,也是无数项目陷进”同步地狱”的根因。带宽这边的坑则是整对象打包同步:哪怕只有位置变了也发全量,几十个玩家一上线带宽瞬间爆。

传统方案差在哪? “把整个对象状态打包发送、由对端重建一份”——这一朴素复制方案在开放世界里同时受困于带宽与一致性:对象既复杂又众多,全量同步根本发不起,且缺乏清晰的”真身/克隆”语义,状态冲突时谁也说不清该信哪一端。”先单机、后联网”的开发顺序则败在架构起点太低——网络感知未能内建到行为的最小单元,后期补救便是无尽的打补丁,每个系统都得单独再处理一遍”本地还是远程”。


第七章 · 工程与运营支撑:让它能上线、防作弊、能造内容

这一章要解决的问题:一款商业开放世界,除了”能玩”,还要”能上线、防破解、能持续造内容、能知道哪里出了问题”。

这一章收的是三块玩家永远看不见、但决定项目成败的工程:安全、遥测、以及——这篇文章最想为它正名的——离线内容生产管线

7.1 安全 / 反作弊 / DRM:玩家看不见的地基

在线开放世界的命脉之一是完整性——游戏不能被随意篡改,否则作弊会摧毁所有人的体验、破解会摧毁商业模式。这套引擎有一组”玩家永远看不见”的安全设施。

安全/反作弊/DRM 三件套

大致三块:

  • 完整性监控 / 反篡改:一套安全插件,持续地监控游戏的关键内存和代码有没有被篡改——通过哈希、运行时校验等手段。一旦检测到篡改,它不会简单地”立刻弹窗报错”,而是有更隐蔽的反应策略——比如设置一些被混淆的标志位、延迟地、不易被定位地做出反应。这种”不立即、不直白”的设计是为了对抗破解者的逆向:如果检测到篡改就立刻崩,破解者很容易定位检测点;延迟而隐蔽的反应让检测逻辑难以被摸清。
  • 内存校验 / 签名:关键数据有校验记录(同样经过混淆),有专门的签名生成机制。确保关键的游戏数据没有被改动。
  • 授权 / 所有权(DRM):验证玩家是否合法拥有游戏——平台激活、商店验证等。加上崩溃转储(crash dump)的上报,让开发能收集线上崩溃。

这一层的设计哲学是隐蔽和纵深——不是一道墙,而是多层互相交织、且故意不直白的检查。这部分我只到结构层(具体的混淆手法、哈希算法不是我的覆盖重点),但它的存在和组织方式很说明问题:在线游戏的安全不是”加个反作弊 SDK”,而是从引擎层就编织进去的一整套完整性体系。

启发在线开放世界的安全要做到”隐蔽 + 纵深”,而非”一道直白的墙”。 一检测到篡改就立刻崩溃,无异于告诉破解者检测点在何处;唯有延迟的、混淆的、多层交织的反应,才经得起逆向。这是玩家永远看不见、却一旦缺失整个在线生态便会崩坏的根基。

7.2 遥测 / 指标 / 性能:不监控就不知道哪卡了

一款上线运营的开放世界,必须能回答”现在跑得怎么样”——帧率、内存、网络、玩家硬件分布。这套引擎把遥测/性能监控当作一等公民。

遥测/性能监控

它分几个层次:

  • 运行时遥测 / 帧指标:实时采集 CPU、内存等指标,能以图表(EKG 式的波形)呈现。开发能直观看到性能的实时曲线。
  • 性能 overlay / 预算显示:屏幕上的性能叠加——帧率、内存占用、draw call 数、光照预算等。让开发在游戏运行时就能看到”现在哪项资源吃紧”。
  • 在线遥测 / 网络指标:把性能和行为数据采集、缓冲、上报。这让运营能从海量真实玩家那里收集到”游戏在各种真实环境下实际表现如何”。
  • 硬件普查 / benchmark:调查玩家的 PC 硬件分布、提供面向终端用户的 benchmark(跑分、帧率报告)。这让开发知道”我们的玩家都用什么配置”,从而做对优化取舍。

核心认知是:遥测是”上线运营”的一等公民,不是开发期的调试工具。 没有遥测,你不知道线上玩家在哪卡、不知道内存在哪泄漏、不知道该为什么硬件优化。一个持续运营的开放世界,必须有一套”持续知道自己跑得怎么样”的神经系统。这套引擎把它做得相当完整——从实时 overlay 到在线上报到硬件普查,覆盖了”开发期看得见、运营期收得到、决策时有依据”。

启发遥测要从一开始就当一等公民建,而不是上线前临时加。 帧预算 overlay 让开发期就看得见瓶颈、在线遥测让运营期收得到真实数据、硬件普查让优化有的放矢。”不监控就不知道哪卡了”——这套神经系统的有无,直接决定你能不能持续把一款开放世界运营好。

7.3 离线内容生产管线:被严重低估的大发现

现在来到这篇文章最想讲、也是整套逆向里最让我意外的发现。

回想前面六章——我们讲了世界怎么动态加载、NPC 怎么活、车怎么沿路网开、反射怎么呈现、navmesh 怎么支撑寻路。但有一个问题一直被悬置着:这些”世界数据”——地形的高度、可行走的 navmesh、车辆的路网、反射用的环境贴图、远处的光照、LOD 用的简化模型——它们是从哪来的?

答案是:它们绝大多数不是手工一点点摆出来的,是用一整条离线工具链”烘”出来的。

离线内容管线流向

如上图,这条管线的形态是:源资产 → 各类烘焙/生成工具 → 运行时可直接消费的数据。 具体的工具包括(这些在笔记里有约三十篇的证据):

  • 地形高度图生成器:把地形数据处理成运行时用的高度图。
  • navmesh 生成器:从碰撞几何离线生成可行走的导航网格——NPC 能在世界里寻路,靠的就是这份预烘的 navmesh。(注意:navmesh 的生成算法内核是我的盲区,我读到的是”它从碰撞几何离线导出”这个接入点,没读到 build 内部的逐行实现。)
  • 路网生成器:生成车辆 AI 用的道路网络——第三章那些 AI 车在城市里怎么知道路怎么走?靠这份预生成的路网。
  • 反射 cubemap / 纹理工具:生成环境反射用的立方体贴图、处理 DDS 纹理、置换贴图(displacement map)。
  • 光照提取 / 光照探针:把场景光照预计算、提取成运行时数据——distant light、light probe。
  • 几何采集 / 场景几何捕获:收集、处理场景几何。
  • tile / 符号 / 噪声等工具:分块处理、符号提取、程序化噪声生成等一系列辅助工具。

把这些串起来看,一个震撼的事实浮现:这座”看起来浑然天成”的开放世界,它的底层数据是被一条庞大的离线管线系统性地生产出来的。 地形不是美术一座山一座山堆的,是高度图烘出来的;navmesh 不是手工标的,是从碰撞生成的;车辆路网不是手画的,是工具生成的;反射不是实时全算的,是 cubemap 预烘的;远处的光不是实时打的,是提取好的。

这就是为什么这条管线被严重低估:玩家看到的是运行时的世界,看不到背后这条”内容工厂”。 而恰恰是这条工厂,决定了世界的规模上限。运行时引擎再强,如果没有这条管线持续地”烘”出地形、navmesh、路网、光照,它就没有世界可跑。做开放世界,运行时是发动机,离线内容管线是整个供应链——没有供应链,发动机空转。

这也回扣了第二章的伏笔:第二章我们说”世界的活力是数据驱动摆出来的”——那些数据(场景点、车辆生成点、路网)正是这条离线管线的产物。第三章的 AI 车要沿路网开,路网来自这里;NPC 要寻路,navmesh 来自这里。前面所有章节消费的”世界数据”,源头都在这条管线。

启发做开放世界,离线内容生产管线是被严重低估的真工程量——它决定世界的规模上限。 很多团队把预算压在运行时引擎上,却低估了”内容怎么被生产出来”。地形高度图、navmesh、车辆路网、反射 cubemap、distant light、LOD——这些全要靠离线管线生成。没有一条高效的内容管线,再强的运行时也只是个跑不满的空壳。 如果你要立项做开放世界,请把”内容管线”和”运行时引擎”放在同等重要的位置——甚至,先想清楚内容怎么来,再想运行时怎么跑。

🔧 设计复盘 · 工程支撑(安全纵深 / 遥测神经 / 离线内容管线)

为什么会这样设计? 安全做成”隐蔽 + 纵深”而非一道直白的墙,是因为对手是会逆向的破解者——检测点一旦直白就会被定位绕过,所以反应要延迟、要混淆、要多层交织。遥测从一开始就当一等公民,是因为上线后你根本不可能凭感觉知道几百万真实玩家在哪卡、内存在哪泄漏,必须有一套”持续知道自己跑得怎么样”的神经系统。而离线内容管线之所以庞大,是因为开放世界的体量决定了内容必须被系统性生产——地形、navmesh、路网、反射、光照量大到不可能手工逐个摆。

踩过什么坑? 最大的认知误区,是以为世界是”做”出来的,实则是”烘”出来的。新手往往以为地形靠美术一座一座堆砌、navmesh 靠人工逐处标注、路网靠手工绘制——直到世界一旦铺开,便发现根本摆不胜摆,只能回头补上一整条离线管线。遥测的坑则在于”临到上线才仓促添加”:等线上出了问题,才想起未曾埋点,届时茫然无据,唯有盲目揣测。安全的坑则是”一检测到便立刻崩溃”,无异于向破解者递上一张检测点地图。

传统方案差在哪? “手工摆放整个世界的内容”是封闭小关卡的做法,搬到开放世界便受困于体量——再多美术也烘不完一座城市的地形/navmesh/路网/反射,没有离线管线,就没有可玩的世界。”上线之后再谈性能”则失守于缺乏神经系统:线上一旦卡顿,没有遥测便无从定位,只能凭玩家的截图揣测。”加一个反作弊 SDK 充当一道墙”则败在单层直白——破解者一次逆向便摸清检测点,墙形同虚设。这三者的共同症结在于:把”商业开放世界的上线与运营”误当成了”做完一个 demo”。


结语:这张地图给了我们什么

我们从一个思想实验出发——”给你一个能跑 demo 的引擎,做开放世界,最重的瓶颈会出现在哪”——走过了七大类、十八个子系统。现在回头看,那个问题的答案已经清晰:真正最重的瓶颈,从来不是画面,而是当海量异构对象一起活着时,没有人知道该信谁的状态、该先算谁、谁该被清掉、谁的真身在哪。

而这整套引擎,本质上就是对这一个问题的、贯穿始终的回答。把全文收束成一句话:

做一款开放世界游戏,难点从来不是”东西多”,而是怎么让海量异构对象不各自为政——既要在一帧固定预算里有序推进它们,又要分清谁拥有状态、谁只是桥接,还要把”造世界、能联网、能上线运营”这一整圈工程都接住。

开篇定的三条副线,在七章里反复浮现,现在可以收口了:

  • 世界级调度,而非每对象 Tick——从第一章的帧相位,到第三章的物理 pre/post、第五章的渲染 DAG,”一帧被切成有序相位、由世界统一推进”是反复出现的同一个骨架。
  • 分层所有权——从第三章”实体的 Owner 维度””车拥有驾驶 AI”,到第六章”网络的真身/克隆””录像的镜像而非权威”,”想清楚谁拥有状态、谁只是观察/桥接”是反复救场的同一把钥匙。
  • 把成本编码进对象状态——从第二章的行为级 LOD,到第三章的 dummy⇄real、实体的”存在 vs 激活”分离,”让对象自己的状态决定它该被算多少”是海量对象不爆的同一条底层逻辑。

如果要把这趟旅程浓缩成几条能直接拿去用的启发,我会留下这五条:

  1. 优先用世界级相位调度,而非让每个对象各自更新。 有强时序约束的系统(AI、物理后处理、IK)越早从各自为政的更新方式里抽出来、交给统一的相位顺序驱动,越省心。这是“世界统一推进”和“每个对象各更各的”两种思路的根本分界。
  2. 实体要有”所有权/来源”维度,不只是类型。 清理、联网、任务管理都需要知道”这个对象归谁、能不能动”。把所有权做成实体的一等属性,而不是事后到处打补丁。
  3. NPC 行为是认知流水线,不是行为树堆。 把”感知—仲裁—加权—路由—执行”拆成职责分离的几拍,并且让”合法地什么都不做”成为一等输出。复杂行为用”任务树 + 数值优先级仲裁”,不用 if-else。
  4. 网络从任务层就要分语义 vs 传输。 “先做单机再套网络”是痛苦之源。如果要联网,从最底层的行为单元就内建”真身/克隆”的概念,把”谁拥有权威状态”作为一等设计。
  5. 内容管线(离线烘焙)是被低估的真工程量。 地形、navmesh、路网、反射、光照、LOD 全靠它生成。它决定世界的规模上限,值得和运行时引擎同等重视——甚至更早想清楚。

最后,必须把诚实的边界再说一遍,因为这正是这篇文章想守住的东西。我读透的是”运行时怎么被组织起来”——帧调度、实体、任务、AI 流水线、网络复制、所有权拆分这些架构层面的”为什么”。我没读透的是底层算法的内核:碰撞求解器、shader 数学、navmesh 的 build、寻路求解的内部、音频 DSP——这些我只到结构层或接入点,并且每一处都标了出来。还有几个真正的空白:时间天气系统、内饰/portal、拾取系统、车辆改装本体、贴花损伤——这些在这类开放世界里都是要做的,但不在我这次的覆盖里。

把读透的讲透、把没读透的标清楚——这张地图的价值,不在于它无所不包,而在于它分得清自己的边界

这是一张广度地图。它把”做一款开放世界游戏要解决哪些问题、它们大致怎么被解掉”完整铺开。而这只是一个开始——这篇全景式的解析,是一个系列的第一篇

接下来:从”看懂”到”做出来”

地图画完了,真正的旅程才开始。接下来,我会沿着这张地图,把每一个深点单独拆开,一篇一篇地深挖下去。大致的路线是这样:

  • 第一站,把核心机制讲到能动手的程度。 帧相位调度到底怎么把一帧切成有序的阶段、每个阶段该扫哪些对象;任务树 + 数值优先级仲裁的抢占是怎么发生的;实体的 Type × Owner 双正交在内存里长什么样、清理系统如何借它做决策;网络的三层 clone 怎么把”谁是真身”一路贯穿到任务这个最小行为单元;离线内容管线又是怎么把地形、navmesh、路网一步步”烘”出来的。每一个,都会从”是什么”讲到”为什么这么设计”,再讲到”换我来做该怎么落地”。
  • 第二站,补上这篇没敢深讲的地方。 凡是这篇标了”盲区”的——碰撞求解、shader 与渲染管线的算法、寻路内核、音频处理——会在它们各自的专题里,尽我所能往下挖一层;凡是标了”空白”的——时间天气、内饰、拾取、改装——也会去把它们补全,让这张地图不再留白。
  • 终点站,落到今天的引擎上:怎么在 UE 里把这些重新做出来。 这是整个系列真正想抵达的地方。前面讲的所有”为什么”,最终都要回答一个问题——如果用 UE(或自研引擎)从零做一款开放世界,这些设计该怎么重新定制? Actor Tick 模型撞上世界级相位调度时怎么改造;UE 的对象/组件体系如何承载”所有权维度”;网络复制如何从任务层就内建语义 owner;内容管线怎么和 UE 的资产流程对接。我会把”读懂别人怎么做”一步步转化为”自己在 UE 里怎么做”,能给出可落地的方案便给出方案,给不出的则如实标注为开放问题。

易言之,这篇是”地图”,后续是”逐段的实地勘探”,最终是”在你自己的引擎里重走一遍这条路”。如果你也在做、或想做一款开放世界,欢迎跟着这个系列一路走下去——每一篇,我们都往”真的把它做出来”靠近一点。

世界很大,但把它做出来的道理,是可以一层层讲清楚的。我们下一篇见。


*本文基于对一款成熟商业开放世界游戏的引擎与客户端代码的源码级逆向阅读整理(约 700+ 篇笔记),覆盖范围从底层引擎一直延伸到玩法、联机、存档、UI、遥测等游戏端系统。文中所有架构、命名均已通用化处理,讲的是”这一类开放世界游戏的典型设计”,而非特指任何一款具体产品。读透的深讲、没读透的标注盲区——这是写作的纪律。*