总结 Overwatch Gameplay Architecture and Netcode 守望先锋的游戏架构与网络代码

原视频

https://www.youtube.com/watch?v=W3aieHjyNvw&list=PLG9sbQS_QV1-6MnaNaN-uO5fUkaTocN58&index=4&t=2901s

翻译链接

https://www.lfzxb.top/ow-gdc-gameplay-architecture-and-netcode/

还是看视频和中文解析吧。我自己翻译起来太麻烦了,主要是截图浪费时间, 上面的翻译链接已经足够好了。

这里我截图并且总结一下关键点。

选择ECS 

和传统的Actor model和最近的 Component model不同,ECS是一个独立的结构 Entity是一个ID,类似数据库查询的一个ID,可以找到这个ID对应的所有Component数据。

ECS 架构能够在快速增长的代码库上管理复杂性

ECS的结构解释

Component是一个纯数据(State状态),无任何逻辑(Behaviour行为)

System是一个纯逻辑,无任何数据

 

在一个World中包含N个System和M个Entity

 

多个System的Tick有Order执行

一个System可能需要多个Component,去组成一个ArchType

System并不关心Entity,System只关注Entity上的部分Component Tuple

 下图代码举例,去解释上面的System和Component关系,System Behaviour如何与Component data关联起来

 

下图:World  Entity System 三者之间的关系

EntityAdmin(World)里有

1.System数组

2.EntityId的映射,并且通过这个ID可以找到Entity包含的所有Component数。同时我们要管理不同Component的生命周期

3.System里会关联多个Component,如下代码图

 

 

举反例说明使用System Behaviour执行AFK判断的优点:

下面代码图是ECS实现的Connection State的判断

 如果使用OOP或者ComponentModel,我们要把Connection State的更新逻辑放到哪个模块呢?这么多和Connection有耦合的模块。

ConnectionState不是Behaviour,只是State。

OOP的每个Component是Behaviour+State

 

制作ECS遇到了2个问题,1.system里存储数据了  2.system里耦合其他system,并且需要读取system的数据

创建了一个 global Entity admin,然后可以获取system。

这造成了扩展困难。

当在实现回放的功能时,这个问题暴露出来了。

 

两个world: liveGame和replayGame

replayGame: server发送一个8-12秒的网络数据到client,然后像正常游戏一样工作。细节需要看另外一个分享。这里没有展开

 

取消了之前的做法:创建一个Shared EntityAdmin

新的规则:只有一个Admin (world).

这个Admin里可以有 Singleion Components。这些Singleion Components存在一个匿名的single Entity上。然后Admin可以直接访问这个AnonymousSingleEntiy,去获取上面的SignleComponent

 

举例 Input的数据作为一个SingleComponent,其他System读取这个InputComponent数据,去同步给Server 去执行本地的移动等。

SingleComponent大改占有了40%

 

在使用了ECSingletonInput数据后,我们就不存在System之间的耦合了

 

多个system Behaviour去共享static function。

这里的 side effects,我觉得可以理解为“写数据、发送网络请求”之类的意思,可能会影响这个Component数据的值。

举例:服务器和client都会调用相同的 static movement funciton,去修改entity的 position

两种共享utility functions的情况

 

简化你的行为(SIMPLIFY YOUR BEHAVIORS)

  • 在单一调用点表达行为(Express in a single call site)   把一个行为的逻辑集中在一个函数调用的地方表达出来,而不是在很多地方分散处理。

  • 将主要副作用局部化到该调用点(Localize major side effects to that call site)  副作用是指函数执行时产生的额外影响,比如修改状态、打印日志、发出网络请求等。这里强调的是,**把这些副作用集中在一个地方(调用点)**处理,而不是让副作用“到处散落”

 

延迟处理 Deferment

如果多个system都要用到一个Component数据

我们可以使用singleComponent, pending后延迟处理这个数据带来的效果

存储并推迟调用:storing the state required to invoke major side

 

下图是举例说明上面的延迟处理概念:

Singleton Contact 单例Contact效果

作者举例:枪械和贴花在墙面上的特效,为了避免在同一片区域多种贴花效果z fighting然后和TA battle的问题,使用了下图的方式。

1.先有个PendingContact数组

2.有耦合的system点,向pendingContact数组里添加数据

3.在最终的ResolveContactSystem里执行(lod mix等)效果

有些数据流的感觉,前面都是修改数据,最后才归纳所有数据,分析执行,得到最后的效果

优点除了解除耦合外,如果做异步加载也很容易(大量相同的effect延迟分段生成)。

 

Overwatch左上角的信息

    • FPS: 70
      当前的帧率(Frames Per Second)为 70,表示画面每秒刷新 70 次。数值越高,游戏越流畅。

    • PNG: 248 ms
      表示 Ping 值,即你客户端到服务器之间发送一个数据包并接收到回复的时间。
      248ms 有点偏高,可能会感到延迟。

    • RTT: 266 ms
      Round Trip Time,也就是完整的来回耗时,理论上 RTT ≈ Ping,但有时候 RTT 会包括更多网络开销和排队延迟。

    • IND: 24 ms
      这是 Interpolation Delay,也称为插值延迟,是客户端用来平滑补偿网络抖动的延迟。值越大说明你看到的画面越滞后于真实情况。24ms 属于正常范围。

 

下图是接下来要讲的网络方面的大纲:

BREADTH(广度)

    • Novel techniques  使用了一些新颖的技术手段

    • Reduced complexity through ECS  通过使用 ECS(实体-组件-系统)架构,来减少复杂性
      ECS 是一种常用于游戏开发的架构模式,有助于提升性能和模块化程度。在网络同步方面,ECS 也能帮助分离数据与逻辑,简化状态同步。

    • 不会讲的内容(Not covering):

      • General replication of entities  不会讲通用的实体同步机制(比如 transform 复制那种标准操作),因为这不是本次主题重点。

      • Remote entity interpolation 不会涉及远程实体插值的细节。这通常用于客户端平滑远程玩家的位置以应对网络延迟。

      • Details of backwards reconciliation 不会讲反向校正的细节,即在客户端预测错了之后,如何回滚并重放输入来修正状态。这属于网络同步中的高级话题。

 

 Determinism

DETERMINISM(确定性),主要讲的是在网络游戏中如何实现“客户端与服务器状态一致性”的关键策略

Synchronized Clock(同步时钟)

所有客户端和服务器使用一个统一同步的时钟基准
这样才能保证大家在相同的逻辑时间点处理同样的输入,例如第 100 帧时所有人执行的是同一批命令。

    • 在 lockstep 或 deterministic replay(决定性回放)系统中尤为关键;

    • 通常会通过 NTP、Ping 反馈、帧号对齐等方式校准时钟。

Fixed Update(固定更新步长) 使用固定时间步长(如每 16ms 更新一次),而不是帧率驱动逻辑。

Quantization(量化)将浮点值(比如位置、角度)进行离散化处理,通常是为了:

  • 降低浮点误差对状态同步的影响;

  • 在重播/回放或状态同步中保证一致性;

  • 降低网络带宽(压缩为 8bit、16bit 等)。

  • 16ms command frames(16毫秒命令帧)意味着客户端每 16ms 发送一次输入命令,服务器也以同样频率接收和处理这些命令。这个节奏可以协调多个客户端的输入并保持同步。

Overwatch中,虽然它是一个 高动作性非 lockstep 的 FPS 游戏,但为了优化同步效率、减少带宽、提高一致性,他们仍然在某些逻辑层中应用了“确定性设计思想”。

在游戏逻辑中,时间被量化成命令帧

 

下图说了是怎么把time量化成fixed frame id的

    • Loop clock => Fixed frames  循环时钟(真实运行时间)映射为固定帧(逻辑帧)

      • Add loop clock time to previous remainder  将当前循环的时钟时间加入上次未处理完的剩余时间

      • Increment command frame 如果累计时间足够,推进一个逻辑命令帧

      • Roll-over remainder to next frame 多出来的时间保留到下一个循环处理

    • System::UpdateFixed  所有固定步长逻辑在 System::UpdateFixed() 中执行

 

 

下图模拟了正常情况下Client发送数据给Server的方式,可以看出Client是领先Sever并且提前执行预测。

下图的Half RTT很好理解

下图Buffer的话是Server Command Buffer。

server这里cache了一帧,server读取buffer里的Command,去权威的执行client的行为,并加入到要发送的快照队列里去

 

 

接下来三张图:说明服务器下发的快照和Client预测的运动冲突后,Client需要回滚执行权威的服务器版本,从第17帧开始

图1:服务器计算第17帧的行为,玩家被冰冻了

 当第17帧的快照到达Client,Client的预测和服务器权威快照有冲突

Client从第17帧开始回滚执行

 

下图:Server没有正常的接收到Client的Command请求,耗尽了CommandBuffer里的数据。

Server简单的使用之前的输入数据进行Fake 模拟,并且回包中标记为当前是LostCommand的,接下来会告诉client

 客户端收到这个输入丢失的标记后,会提高send频率,也许类似unity fixedupdate 修改fixedDeltaTime之类的

Server同时会增大Command buffer

如下面2张图

 经典的发送冗余input, 大大降低输入丢包。如下图

 

 

大致提了Ability也是可以预测并且根据时间去回滚的,工作原理和上面的movement差不多,需要看另外一个GDC分享,这里没有细说。

 

 

Hit Registeration 命中检测

首先伤害的结算使用延迟处理 Deferment在服务器进行。就像上面提到的effect contact一样。

命中的预测是在client进行的。

服务器收到命中请求后,会在对应的时刻(倒回rewound)效验

 下图举例说明服务器倒回Rewound去检测命中和产生伤害。

首先在0.5s内给每个 敌人目标设置时间段的box,并且检测shooting raycast是否和这个box相交。

如果相交,再rewound指定的 敌人。这也算一种优化吧。并不是rewound所有的entity。只关注可能的部分。

 

下图也是一个例子。在模拟60%丢包率情况下,

绿色和蓝色是client下的 target和bullet

黄色和紫色是server下的模拟。

 

当RTT超过220,客户端预测命中开始变得不太有效了。视频的40分钟左右。

不在进行预测命中,直接交给Server去处理。

原因:客户端上的Target外插值太久了,虽然看起来是及时响应的(及时反馈了飙血Effect,但是真正的HP bar和命中点并没有显示),但客户端预测命中实际上没什么效果。 

 

 

 

retrospective 回顾

主要是讲了ecs的一些使用上的优点

1.定义ComponentTuple,清晰的知道要做什么

 2.明确的知道system需要哪些数据。进行并行化。举例:transform组件,明确的知道哪些System只是Read Transform,然后并行安全。

 

 

Entity lifeTime 

延迟创建和延迟销毁。

延迟创建带来了麻烦的一帧的问题。

 

 

规则:

 

 

ECS不是一种强制性设计原则。如果有些代码不适合ECS,不要强制塞入ECS

 

update system在主线程上

有些独立的功能是在work thread上的。

比如projectile的模拟,比如nav 路径的重建(和ecs无关)

 

 

CLOSING(总结)

    • ECS 是“粘合剂”      ECS(Entity Component System)作为一种架构,是用来连接游戏不同子系统(比如输入、动画、物理、技能逻辑、网络等)的一种“中介”结构

    • ECS 可以最小化耦合度   ECS 的好处是能让你写出非常 解耦、模块化、可组合 的代码 

      • 系统之间互不依赖;

      • 网络代码只处理组件的变化;

      • 渲染代码、输入处理、模拟逻辑彼此隔离。

    • 要对你的粘合代码(glue code)加以约束(上面提到的规则)

      “粘合代码”指的是把多个子系统“连起来”的中间层代码(例如:网络→ECS、技能系统→特效、输入→行为控制器)

      这类代码很容易变成“烂泥山”:

      • 逻辑穿插、职责混乱、耦合过高;

      • 很难调试或替换某一模块;

      • 网络变化时,影响波及全系统。

    • 网络代码很复杂,所以一定要把它解耦

      网络代码尤其难写(tricky),原因包括:

      • 要考虑丢包、延迟、乱序、带宽限制;

      • 还要兼容回滚、预测、补偿;

      • 而且所有这些不能污染游戏核心逻辑

      所以最好设计成:

      • 网络模块 → 只处理收发消息、解包、缓冲、回放;

      • 游戏逻辑模块 → 完全通过命令数据驱动(例如 ApplyCommandFrame())

 

 

提问时间:

在Component上 有没有实double buffer?

答:没有用到,movement和input是一个ring buffer.你说的这个double buffer也好实现的,我们没用到

 

你说子弹的命中用的发射者去判断。 如果子弹的生命周期中发射者在不停地变或者离开了怎么办?

答:这是个设计问题,我们没用到,具体问题具体对待

 

游戏是一定60Hz帧的吗?如果只有30hz的模拟怎么办呢?

答:是的,逻辑帧一定是60的。 

如果客户端CPU性能很差,我们可以做一些优化,去避免Death Spiral。

首先 ,本地预测玩家是重点(优先处理),所以本地预测(local player prediction)是最精确、最及时的;这是高优先级的模拟,占用 CPU 比较多是合理的。

### ② 远程角色(其他玩家)使用“预算化模拟”

关键词:Budgeting(设定每帧上限)

“远程角色脚本模拟最多只能花 1.5ms”

解释:

  • 远程玩家的状态,客户端通常是插值+预测出来的,不需要超高精度;

  • 可以设置每帧最多处理几个远程实体、或者限制脚本调用复杂度

  • 如果时间不够,可以“跳过一些非关键计算”或“延后一帧处理”。

常见策略包括:

  • 模拟远程玩家动画、姿态而不是精确动作逻辑;

  • 远程技能只表现结果,不模拟路径;

  • Tick 频率降低(例如远程实体每两帧 Tick 一次)。

### ③ Spike Smoothing(峰值平滑)技术

解释:

  • 有时帧突然很“重”(比如10个角色同时释放技能),你不能一股脑处理完;

  • 可以将部分模拟负载**“分帧处理”**,让逻辑稍微延后,但保持帧率稳定;

  • 平滑掉 CPU usage 的 spike,避免掉帧和“death spiral”。

技术方式可能包括:

  • 逻辑排队,按预算逐帧执行;

  • 异步/分批脚本执行;

  • 行为拆解,拆成多个帧去执行(比如动画分段执行、伤害延后一帧触发);

 

很慢的导弹也做预测了吗?

答:目前没考虑过不做预测,有趣有趣。

 

量化空间有多精细?那物理引擎在这个精度下会遇到问题吗?有多少游戏逻辑会受到物理引擎的影响?

答:

  • 空间量化的精度是大约 1 米 / 1024(≈1mm)

    • 也就是一个浮点数表示的位置,会被压缩为大概 10-bit 的离散格。

  • 他们将“视觉物理引擎”和“游戏物理引擎”分离了:

    • 视觉物理(client physics)主要是为了特效、击中效果等视觉表现

    • 游戏模拟物理(simulation physics)才是用于权威状态判断、逻辑碰撞

  • 两个物理引擎用的是同一套底层逻辑代码(同一个人写的):

    • 所以行为一致性高,且跨平台行为一致(AMD/Intel/Linux等)。我们用了很明确的指令/手法控制浮点行为。

    • 客户端和服务器共享关键逻辑代码(或者用统一库编译成 DLL、共享模块)
    • 非常稳定,不会因为平台浮点差异出问题。

  • 他们担心浮点编译器差异,比如 Clang 优化 & 执行乱序

    • 但由于用了量化,很多浮点误差问题被规避了;

    • 如果有更具体的疑问,可以去第二天某位讲者的分享深聊 IEEE 浮点和一致性问题。

 

你们客户端预测依赖于确定性(determinism),动态导航网格(navmesh)看起来是异步的,那怎么保持一致?

 答:整个模拟系统是基于固定时间步长(fixed timestep),所有系统都在统一节奏下运行,是实现一致性的关键基础。

他们不是 100% 的确定性(不像 StarCraft/Halo 那样)

开发者 Igor 做了一个“预测差异调试器”

  • 可以看到哪一帧服务器和客户端不一致;

  • 可以精确地定位是哪个数学或逻辑步骤出现偏差;

  • 通常是“寻找角色脚部的射线(ray)”导致的问题。

关于 Navmesh:

  • Navmesh 的结果在客户端和服务器给相同输入时会一致;

  • 重建 Navmesh 的耗时可能不一致,这并不会导致严重问题;

  • 因为大多数玩家移动技能不依赖 Navmesh

  • 即使错预测,也会被服务器矫正回来,不影响最终一致状态;

  • 不会为 Navmesh 做完整状态回滚(太大,占 40MB),仅回滚必要内容。

 

posted @ 2025-04-09 16:36  sun_dust_shadow  阅读(214)  评论(0)    收藏  举报