[GDC2019][01]-Working with deterministic simulation in 'For Honor'

前言

    GDC2019前段时间已经落幕。接下来的一段时间我会把看过的视频的简要内容记录下来,方便日后的查阅。

    由于个人的能力有限,无法太详细记述亦可能会有所错误,建议简要阅读然后再去看原视频。视频地址我也会一同贴在帖子中。

概述

    第一篇介绍的是《Back to the Future! Working with Deterministic Simulation in 'For Honor'》。

    总体来说,分享的内容偏向经验介绍,技术方面的内容不多。由于个人没有相关网络游戏的开发经验,所以只会大致翻译介绍简要内容。

    由于没有相关经验,在看视频之前我大致学习了有关帧同步的内容,主要是以下的几篇博文:

  • 两种同步模式:状态同步和帧同步 
  • 从《王者荣耀》聊聊游戏的帧同步 
  • 帧同步联机战斗(预测,快照,回滚) 

    但是要注意的是这个视频不会介绍网络技术,偏向于介绍底层的gameplay构架。

[GDC2019][01]-Working with deterministic simulation in 'For Honor'_第1张图片

内容

前期分析

    在项目之初,要确定使用的技术首先应该分析项目的特点确定需求。

    对《荣耀战魂》来说,游戏有以下几点限制:

  • 设计上

  1. 需要精确的Gameplay,因为游戏中,玩家进行的是近距离且具有技术性的战斗 
  2. 由于是动作游戏,所以需要快速的反馈
  3. 它是一款多人游戏
  • 技术上

  1. P2P(Peer-to-peer)的 

  2. 低带宽占用(育碧的服务器大家懂得的)

  3. 公平的 

    最初的设计是LockStep的确定性模拟(即我们常说的帧同步技术),但是LockStep带来的问题是当有玩家的网络状况不好时,会影响其他玩家的体验(所以这种技术现在基本上已经没有在使用了)。因此最终确定的是带有回滚功能的帧同步,在这里被称为Buffered Deterministic Simulation。

[GDC2019][01]-Working with deterministic simulation in 'For Honor'_第2张图片

主讲人这里还推荐了2018年的一个分享《8 Frames in 16ms: Rollback Networking in 'Mortal Kombat' and 'Injustice 2'》,有兴趣的可以看一看。

技术简述

  1. 服务器只发送/转发用户输入
  2. 每个客户端模拟出结果
  3. 无权威

    这里的确定性(Deterministic )指的是在相同输入下,输出的结果始终是一致的。每个客户端独立运行,没有正确和错误之分,所以要保证每个客户端的结果都一致。因为我们没有权威(我的理解是一份正确的状态的拷贝作为正确状态),所以一旦出现问题(不一致)时便无法恢复状态。

    这里还提到一个History buffer,这个缓冲区是客户端自己的缓冲区,共记录5秒的状态信息。在一般情况,所有的客户端在独立运行,当某个客户端接收到其他客户端的输入时,客户端会回到输入发生的那一帧然后重新模拟直到当前的时间。因为其他端的输入传入时总是滞后于当前客户端的,所以需要缓冲区来回滚。

确定性问题

  • 不要假设你的引擎是确定的 ,举了几个例子
  1. 浮点数 

  2. 物理(havoc) 

  3. 多线程 

  4. 时间(荣耀战魂中有session时间和simulation时间,其中session时间客户端独立不同步的,所以要分清两种时间使用的场合) 

  5. 载入/动态生成(一个新生成的object就绪的时间是不固定的) 

  6. 随机数

  • 不要试图让所有的东西都是确定的 
  1. 动画(动画对Gameplay是不必要的,它只是让游戏更加生动) 
  2. 视觉效果,UI和声音

这些都是对输入的模拟的反馈,荣耀战魂中处理反馈有两种方式,Immediate和Finalized。

在游戏模拟过程中,有一个观察者,能够知道模拟中发生的所有的事情,并且在模拟结束之后播放模拟的feedback。它会和rewind的结果进行对比,比如说一次攻击,在之前的模拟结果中是命中的,那么我们播放流血特效,但是rewind的结果显示对方格挡了这次攻击,所以立即停止播放命中的反馈,转而播放火花。 

Imediate和Finalized的区别在于反馈的时机。立即的反馈可能被后续的输入取消,所以可能会看到glitch(上面举的例子就是Imediate的),而finalized的则有更高的延迟因为它需要等到输入确定才会发生(rewind之后)。 

不要低估丢失同步

    这里出现了一个词desynchs,它是desynchronization的缩写, 即丢失同步。

    在开发过程中,应该及时发现丢失同步的情况并且处理相关的bug。荣耀战魂在开发中,当有丢失同步发生时,都会发送报告并创建bug给开发团队,从而能有人去处理问题。

    具体来说,在开发环境下 

  1. know when(开发环境下会全程追踪对比各个客户端之间的同步状态) 
  2. 记录状态变化并生成一个Record Set(位于内存中),每100毫秒会对Record Set做一次确认,发送CRC(这段时间内所有状态变化的记录的内存所生成的CRC)到每个端并比较,如果finalization(我的理解是客户端接收完了所有其他端的输入并且重新模拟完毕后,认为此后状态是固定且不再改变了)之后还是不同,暂停游戏 
  3. 收集信息并发送xml文件用来记录状态变化。不同意的端会发送操作细节,通过提取不同点发现丢失的操作,不匹配的操作,输出XML的对比文件。 
  4. 比较文件,发送bug 

  5. debug 

    生产环境下 

  1. 每秒生成快照 

  2. 发送CRC到端并比较 

  3. 在“finalization time”后状态仍然不同时

首先试着恢复状态到一致的情况下。无法恢复的情况下,如果不同者是少数,那么踢出不同者(可能作弊?) ,否则结束会话。这种情况下,CRC对比报告会发送给开发者,但对开发者而言,可见性(Visibility)是我们知道事情发生了,但是我们不知道具体发生了什么。(CRC是不可逆的) 

中途加入的实现

简单的实现=>回放所有的输入 

或者 

载入快照=>更少的模拟步数 (荣耀战魂采用的方式)

优化

    1. 在一帧内运行8次逻辑帧(好处是什么?解决了什么问题? )

[GDC2019][01]-Working with deterministic simulation in 'For Honor'_第3张图片

因为需要做rewind,在平均的网络环境下,你收到输入大概有250ms(30FPS)的延迟,所有需要在一帧内重新模拟8次(和守望先锋的加速一个原理?没做过不知道)。

    2. 内容总是不断增长的,在内容增长的过程中要控制好性能。所以要与设计师交流好限制的问题。

    3. 多线程(确定性问题)的问题与解决办法。

        双缓冲。使用双缓冲来保护线程之间的交流是安全的。每个客户端的状态模拟都有两个备份,current state和next state,next state是模拟正在写入的状态,而current state是在模拟开始前的state,因为在模拟过程中current state是不变的,所有可以保证任何线程在读入当前的state时都能获得确定的结果(多线程下读写顺序是未知的)

        消息Messages。当别的线程想要修改next state的值时,会发送一个修改的消息。在每帧结束时把这些消息应用到next state上。

     4. 组织好每帧以保证gameplay的运行时间足够(在性能不够时,移除一些视觉效果之类不重要的东西以减少开销) 

     5. 移除无用的代码和变量 

For Honor新模式的介绍(一些挑战和问题以及解决方式)

分析工具和自动测试工具

防止人们犯重复的错误

    多使用断言,来防止写出错误的代码。80%丢失同步的情况都源于以下问题

    1. Cosmetic 装饰物 

实际上是一些不需要模拟的部分,或者有些是本地用,有些是模拟用,总之要确保本地用代码不会出现在模拟的线程之中,例如session time(本地),视觉效果,寻路(不需要模拟),射线检查和随机数(模拟和本地两个版本),骨骼(不需要)和摄像机(纯本地) 。

荣耀战魂使用一个宏用来标记代码是模拟不安全的,当在模拟线程或者模拟中调用这个带标记的函数会触发断言。 

[GDC2019][01]-Working with deterministic simulation in 'For Honor'_第4张图片

    2. Multithread access violation 多线程访问冲突 

用宏来确保线程访问的权限(例子中的current state是可以在任何地方读的,且它是只读的,而next state只能在模拟线程中使用)

[GDC2019][01]-Working with deterministic simulation in 'For Honor'_第5张图片

    3. 随机代码错误 (摊手,没有办法,慢慢debug吧)

包括但不局限于未初始化的值,静态变量(初始化的时序是不定的无法掌控的),指针,缺少的break。

    4. 确认代码是自动生成代码 

从最初的手写=>自动生成。自动生成gamestate和跨线程写入消息 。[GDC2019][01]-Working with deterministic simulation in 'For Honor'_第6张图片构架设计的经验

构建核心框架,我们需要做的事情是 

  1. 定义清晰的规则,例如如果你在模拟中使用了一个变量,那么你就它必须被存放在模拟状态。没有任何的例外。

  2. 在你的代码构架中强制执行这些规则,定义一些结构和辅助函数,那么程序员在写逻辑时甚至不需要思考就能按照设计的规范来做。 

  3. The more you wait, the harder it becomes(没懂ORZ) 

你可能感兴趣的:(GDC)