GDC2019前段时间已经落幕。接下来的一段时间我会把看过的视频的简要内容记录下来,方便日后的查阅。
由于个人的能力有限,无法太详细记述亦可能会有所错误,建议简要阅读然后再去看原视频。视频地址我也会一同贴在帖子中。
第一篇介绍的是《Back to the Future! Working with Deterministic Simulation in 'For Honor'》。
总体来说,分享的内容偏向经验介绍,技术方面的内容不多。由于个人没有相关网络游戏的开发经验,所以只会大致翻译介绍简要内容。
由于没有相关经验,在看视频之前我大致学习了有关帧同步的内容,主要是以下的几篇博文:
但是要注意的是这个视频不会介绍网络技术,偏向于介绍底层的gameplay构架。
在项目之初,要确定使用的技术首先应该分析项目的特点确定需求。
对《荣耀战魂》来说,游戏有以下几点限制:
设计上
技术上
P2P(Peer-to-peer)的
低带宽占用(育碧的服务器大家懂得的)
公平的
最初的设计是LockStep的确定性模拟(即我们常说的帧同步技术),但是LockStep带来的问题是当有玩家的网络状况不好时,会影响其他玩家的体验(所以这种技术现在基本上已经没有在使用了)。因此最终确定的是带有回滚功能的帧同步,在这里被称为Buffered Deterministic Simulation。
主讲人这里还推荐了2018年的一个分享《8 Frames in 16ms: Rollback Networking in 'Mortal Kombat' and 'Injustice 2'》,有兴趣的可以看一看。
这里的确定性(Deterministic )指的是在相同输入下,输出的结果始终是一致的。每个客户端独立运行,没有正确和错误之分,所以要保证每个客户端的结果都一致。因为我们没有权威(我的理解是一份正确的状态的拷贝作为正确状态),所以一旦出现问题(不一致)时便无法恢复状态。
这里还提到一个History buffer,这个缓冲区是客户端自己的缓冲区,共记录5秒的状态信息。在一般情况,所有的客户端在独立运行,当某个客户端接收到其他客户端的输入时,客户端会回到输入发生的那一帧然后重新模拟直到当前的时间。因为其他端的输入传入时总是滞后于当前客户端的,所以需要缓冲区来回滚。
浮点数
物理(havoc)
多线程
时间(荣耀战魂中有session时间和simulation时间,其中session时间客户端独立不同步的,所以要分清两种时间使用的场合)
载入/动态生成(一个新生成的object就绪的时间是不固定的)
随机数
这些都是对输入的模拟的反馈,荣耀战魂中处理反馈有两种方式,Immediate和Finalized。
在游戏模拟过程中,有一个观察者,能够知道模拟中发生的所有的事情,并且在模拟结束之后播放模拟的feedback。它会和rewind的结果进行对比,比如说一次攻击,在之前的模拟结果中是命中的,那么我们播放流血特效,但是rewind的结果显示对方格挡了这次攻击,所以立即停止播放命中的反馈,转而播放火花。
Imediate和Finalized的区别在于反馈的时机。立即的反馈可能被后续的输入取消,所以可能会看到glitch(上面举的例子就是Imediate的),而finalized的则有更高的延迟因为它需要等到输入确定才会发生(rewind之后)。
这里出现了一个词desynchs,它是desynchronization的缩写, 即丢失同步。
在开发过程中,应该及时发现丢失同步的情况并且处理相关的bug。荣耀战魂在开发中,当有丢失同步发生时,都会发送报告并创建bug给开发团队,从而能有人去处理问题。
具体来说,在开发环境下
比较文件,发送bug
debug
生产环境下
每秒生成快照
发送CRC到端并比较
在“finalization time”后状态仍然不同时
首先试着恢复状态到一致的情况下。无法恢复的情况下,如果不同者是少数,那么踢出不同者(可能作弊?) ,否则结束会话。这种情况下,CRC对比报告会发送给开发者,但对开发者而言,可见性(Visibility)是我们知道事情发生了,但是我们不知道具体发生了什么。(CRC是不可逆的)
简单的实现=>回放所有的输入
或者
载入快照=>更少的模拟步数 (荣耀战魂采用的方式)
1. 在一帧内运行8次逻辑帧(好处是什么?解决了什么问题? )
因为需要做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. 移除无用的代码和变量
多使用断言,来防止写出错误的代码。80%丢失同步的情况都源于以下问题
1. Cosmetic 装饰物
实际上是一些不需要模拟的部分,或者有些是本地用,有些是模拟用,总之要确保本地用代码不会出现在模拟的线程之中,例如session time(本地),视觉效果,寻路(不需要模拟),射线检查和随机数(模拟和本地两个版本),骨骼(不需要)和摄像机(纯本地) 。
荣耀战魂使用一个宏用来标记代码是模拟不安全的,当在模拟线程或者模拟中调用这个带标记的函数会触发断言。
2. Multithread access violation 多线程访问冲突
用宏来确保线程访问的权限(例子中的current state是可以在任何地方读的,且它是只读的,而next state只能在模拟线程中使用)
3. 随机代码错误 (摊手,没有办法,慢慢debug吧)
包括但不局限于未初始化的值,静态变量(初始化的时序是不定的无法掌控的),指针,缺少的break。
4. 确认代码是自动生成代码
构建核心框架,我们需要做的事情是
定义清晰的规则,例如如果你在模拟中使用了一个变量,那么你就它必须被存放在模拟状态。没有任何的例外。
在你的代码构架中强制执行这些规则,定义一些结构和辅助函数,那么程序员在写逻辑时甚至不需要思考就能按照设计的规范来做。
The more you wait, the harder it becomes(没懂ORZ)