请选择 进入手机版 | 继续访问电脑版

【3D技术宅公社】XR数字艺术论坛  XR技术讨论 XR互动电影 定格动画

 找回密码
 立即注册

QQ登录

只需一步,快速开始

调查问卷
论坛即将给大家带来全新的技术服务,面向三围图形学、游戏、动画的全新服务论坛升级为UTF8版本后,中文用户名和用户密码中有中文的都无法登陆,请发邮件到324007255(at)QQ.com联系手动修改密码

3D技术论坛将以计算机图形学为核心,面向教育 推出国内的三维教育引擎该项目在持续研发当中,感谢大家的关注。

查看: 1358|回复: 0

[网络技术] 【转】网络同步在游戏历史中的发展变化(二)—— Lockste...

[复制链接]
发表于 2020-8-9 22:19:42 | 显示全部楼层 |阅读模式
本帖最后由 夜行的猫仔 于 2020-8-9 22:20 编辑

前言:

网络同步属于游戏开发中比较重要且复杂的一部分,但是由于网上的资料内容参差不齐,很多人直接拿别人的结论写文章,导致很多人对这一块的很多概念和理解都是错误的。本文参考了大量的相关论文和资料(三十篇以上),从网络同步的基本概念讲起,进一步深入到服务器架构与同步算法的实现细节,可以帮你系统的梳理网络同步技术的发展与应用。本篇核心内容为“锁步同步lockstep(帧同步)”,首发在网易雷火的知乎账号上。

注:在公众号后台回复“网络同步”可获取文中所引用的论文和资料

目录(第二篇):

三.锁步同步lockstep(帧同步)

   1.早期的Lockstep

   2.Bucket Synchronization

   3.锁步同步协议 Lockstep protocol

   4.RTS中的Lockstep

   5.Pipelined Lockstep protocol

   6.TimeWrap

   7.Lockstep与"帧"同步

   8.Lockstep小结

(第一篇:网络同步在游戏历史中的发展变化(一)— 网络同步与网络架构

三.锁步同步 lockstep

(帧同步)

Lockstep就是我们口中常说的“帧同步”。但严格来说,Lockstep并不应该翻译成帧同步,而是——锁步同步算法。(LockStep由军事语境引入,用来表示齐步行军,队伍中的所有人都执行一致的动作步伐)首次引入计算机领域[4],应该是用于计算机容错系统,即“使用相同的、冗余的硬件组件在同一时间内处理相同的指令,从而保持多个CPU、内存精确的同步”,所以一开始与游戏并没有任何关系。

1.早期的Lockstep

不过,早在1994年,FPS鼻祖Doom就已经采用了类似Lockstep的方式进行网络同步[5]。Doom采用P2P架构,每个客户端本地运行着一个独立的系统,该系统每0.02秒钟对玩家的动作 (鼠标操作和键盘操作,包括前后移动、使用道具、开火等) 采样一次得到一个 tick command 并发送给其他所有玩家,每个玩家都缓存来自其他所有玩家的 tick commands,当某个玩家收到所有其他玩家的 tick commands 后,他的本地游戏状态会推进到下一帧。在这里, tick command的采集与游戏的推进是相互独立的。

其实当时并没有Lockstep这个说法,doom的整篇论文里面也没有出现过这个词。不过后面概念逐渐清晰后我们会发现,Doom采用的同步方式就是我们常说的原始版本的Lockstep——“确定性锁步同步(Deterministic Lockstep)”。

2.Bucket Synchronization

1999年,Christophe Diot和Laurent Gautier的团队开发了一款基于互联网的页游——MiMaze,基于传统的Time Bucket Synchronization[6]他们在发布的论文里面提出了改进后的Bucket Synchronization同步方法[7]。Bucket Synchronization把时间按固定时长划分为多个Bucket,所有的指令都在Bucket里面执行。考虑到网络延迟的情况,每个玩家在本地的命令不会立刻执行而是会推迟一个时延(该时延的长度约等于网络延迟),用来等待其他玩家的Bucket的到来。如果超过延迟没有到达,既可以选择放弃处理,也可以保存起来用于外插值(Extrapolation)或者使用前面的指令重新播放。在这种方式下,每个玩家不需要按照Lockstep的方式严格等待其他玩家的命令在处理,可以根据网络情况顺延到后面的bucket再执行。Bucket Synchronization可以认为是我们常说的“乐观帧锁定”。

3.锁步同步协议 Lockstep protocol

前面提到的Deterministic Lockstep虽然简单,但是问题却很多,包括浮点数跨平台的同步问题、玩家数量增长带来的带宽问题以及显而易见的作弊问题(在P2P架构下几乎没有任何反作弊能力)。说到作弊这里不妨先简单谈一下游戏外挂,外挂这个东西原本是指为增加程序的额外功能而追加的内容,但随着网络游戏的诞生就开始与游戏绑定起来。针对不同的游戏类型有着各式各样的外挂工具,包括游戏加速、透视、自动瞄准、数据修改等。从技术上来讲,有些外挂是无法做到完全避免的。在CS架构下,因为大部分核心逻辑都是在服务器上面计算,很多作弊手段无法生效。但是P2P架构下作弊却变得异常简单,甚至不需要很复杂的修改工具。比如客户端A使用了外挂工具,每次都将自己的操作信息推迟发送,等到看到了别人的决策后再决定执行什么,这种外挂称为lookahead cheats。(或者假装网络信号不好丢弃第K步的操作,第K+1步再发送)

因此,在2001年,Nathaniel Baughman和Brian Neil Levine在IEEE上发表了论文,提出锁步同步协议 Lockstep protocol [8]来对抗lookahead cheat类型的外挂。这可能是第一次“Lockstep”一词被正式的用于游戏网络同步的描述中。不过要注意的是,这里的Lockstep protocol并不是我们前面提到的Deterministic Lockstep ,相比之前的在第K步(第K个Tick Command间隔)就直接发送第K+1步的明文操作信息,Lockstep protocol每一步都分两次发送信息。大概的流程如下:

  • 先针对要发送的明文信息进行加密,生成“预提交单向哈希(secure one-way commitment hash)”并发送给其他客户端。

  • 待本地客户端接收到所有其他客户端的第K步预提交哈希值之后,再发送自己第K步的明文信息

  • 等到收到所有其他客户端的第K步明文信息后,本地客户端会为所有明文信息逐个生成明文哈希并和预提交的哈希值对比,如果发现XXX客户端的明文哈希值和预提交哈希值不相等,则可以判定该客户端是外挂。反之,游戏正常向前推进。


这种协议的虽然可以对抗外挂,但是很明显带来了带宽以及性能的浪费,而且网络条件好的客户端会时刻受到网络差的客户端的影响。所以他们又在此基础上提出异步的Lockstep(asynchronous Synchronization lockstep)。大体的思路是利用玩家角色的SOI(Spheres of Influence,和AOI概念差不多),两个玩家如果相距很远互不影响,就采用本地时钟向前推进(非Lockstep方式同步),如果互相靠近并可能影响到对方就变回到严格的LockStep同步,这里并不保证他们的帧序列是完全一致的。

4.RTS中的Lockstep

同一年,2001的GDC大会上,“帝国时代”的开发者Mark Terrano和Paul Bettner针对RTS游戏提出了优化版的锁步协议[9]。早在1996年他们就开始着手于帝国时代1的开发,不过很快就发现在RTS游戏中的网络同步要比其他类型游戏(比如FPS)复杂的多,一是游戏中可能发生位置变化的角色非常多,必须要合理的减少网络同步带宽,二是玩家对同步频率极为敏感,每一秒的疏忽都可能影响局势。

所以,他们在传统 Lockstep(当时仍然没有Deterministic Lockstep这个概念)的基础上做了优化,首先保持每一步只同步玩家的操作数据,然后对当前的所有命令延迟两帧执行的方法来对抗延迟。具体来说,就是第K步开始检测到本地命令后会推迟到第K+2步进行发送和执行,K+1步收集到的其他客户端命令会推迟到K+3步去执行,每K步执行前会去判断本地是否有前两步的命令,如果有就继续推进。(关于具体的推进策略,论文里面写的不是很清楚,这里加入了作者自己的判断)

此外,为了避免高性能机器受低性能机器的影响而变“卡”,“帝国时代”里面每一步(称为一个turn)的长度是可以调整的,并且完全与渲染分开处理。每个客户端会根据自身的机器性能与网络延迟情况来动态调整步长时间,如果性能优良但是延迟高就会拉长每个turn的时间(多出的时间用于正常进行多个帧的渲染以及Gameplay的处理,虽然可能有误差),如果性能差但是网络正常就会把大部分的时间用于每个turn的渲染,在这种条件下每个客户端相同的turn执行的本地时间虽然不同,但是执行的内容是完全一致的。

5.Pipelined Lockstep protocol

流水线操作是一种高效的数据传输模型,在计算机技术里面随处可见(比如计算机存储系统中主存与Cache的交互)。2003年,Ho Lee、Eric Kozlowski等人对Bucket synchronization、Lockstep protocol等协议进一步分析并针对存在的缺点进行优化,提出了Pipelined Lockstep protocol[10]。他们发现只有当前玩家的指令行为不与其他人产生冲突,就可以连续的发送的自己的指令而不需要等待其他人的消息。举个例子,假如一个游戏只有7个格子,玩家A和B分别站在左右两边,每次的指令只能向前移动一格。那么A和B至少可以连续发送三个指令信息而不需要等待对面玩家的数据到来。

Pipelined Lockstep protocol基于Lockstep protocol,为了防止cheatahead外挂同样需要提前发送hash,这种操作同步、不等待超时玩家的确定性锁步的特性逐渐成为“Lockstep”的标准,被广泛应用于网络同步中。

6.TimeWrap

TimeWrap原本是指科幻小说中的时间扭曲,其实早在1982年就被D Jefferson等人引入计算机仿真领域[11],后续又被Jeff S. Steinrnan进行优化和调整[6][12]。TimeWrap算法基本思路是多个物体同时进行模拟,当一个物体收到了一个过去某个时刻应该执行的事件时,他应该回滚到那个时刻的状态,并且回滚前面所有的行为与状态。

前面提到的Pipelined Lockstep protocol可以流畅的处理玩家互相不影响的情况,但是却没有很好的解决状态冲突与突发的高延迟问题。参考TimeWrap这种思路,我们可以将本地执行过的所有操作指令进行保存行成一个快照(Snapshot),本地按照Pipelined Lockstep protocol的规则进行推进,如果后期收到了产生冲突的指令,我们可以回滚到冲突指令的上一个状态,然后把冲突后续执行过的事件全部取消并重新将执行正确的指令。这样如果所有玩家之间没有指令冲突,他们就可以持续且互不影响的向前推进,如果发生冲突则可以按照回退到发生冲突前的状态并重新模拟,保持各个端的状态一致。

7.Lockstep与"帧"同步

前面提到了那么多lockstep的算法,但好像没有一个算法使用到“帧”这个概念。其实“帧同步”属于一个翻译上的失误,宽泛一点来讲“帧同步”是指包含各种变形算法的Lockstep,严格来讲就是指最基本的Deterministic Lockstep。我猜测国内在引入这个概念的时候已经是2000年以后(具体时间没有考证),lockstep算法已经有很多变形,时间帧的概念也早已诞生,所以相关译者可能就把“lockstep”翻译成了“帧同步”。当然也可能是引入的时候翻译成了“按帧锁定同步”,后来被大家以简化的方式(帧同步)传递开来。但不管怎么说,“帧”在实际应用中更多的是指画面渲染的频率,lockstep里面的“step”概念要更宽泛一些才是。

了解游戏的朋友,都知道游戏是有帧率(FPS)的,每一帧都会运行相当复杂的运算(包括逻辑和渲染),如果运算规模达到一定程度就会拉长这一帧的时间,帧率也就会随之下降。所有影视作品的画面都是由一张张图构成的,每一个画面就是一帧,一秒能放多少个画面,就是有多少帧。在游戏里面,渲染器会不停的向渲染目标输出画面,并在帧与帧之间游戏处理各种逻辑,逻辑会改变游戏世界对象的行为,这些行为又会影响游戏画面的变化,这就是游戏的核心框架。早期的lockstep里面渲染和逻辑都是放在一个帧里面去处理的,这样一旦命令受到网络延迟的影响,玩家本地就会卡在一个画面直到消息的到来。为了解决这个问题,有一些游戏会将逻辑和渲染分开处理(分为逻辑帧和渲染帧),逻辑帧每隔固定的时间去处理所有逻辑事件。在不是严格锁帧的情况下,你本地即使没有收到网络数据也可以在继续执行其他的逻辑并维持高频率的渲染(正在移动的对象不会由于短暂的延迟而静止不动)。这里面的逻辑帧就是lockstep里面的“Step”,也可以叫做“turn”,“bucket”或者“步”。

8.Lockstep小结

了解了Lockstep的发展历程,最后我们再总结梳理一下。网络同步,其目标就是时刻保证多台机器的游戏表现完全一致。由于网络延迟的存在,时刻保持相同是不可能的,所我们要尽可能在一个回合(turn)内保持一致,如果有人由于某些原因(如网络延迟)没有同步推进,那么我们就需要等他——这就是Lockstep(或者说一种停等协议)。LockStep其实是很朴素的一种思想,很早就被用于计算机仿真模拟、计算机数据同步等领域。后来网络游戏发展起来后,也很自然的被开发者们拿到了系统设计中。

早期lockstep被广泛用于局域网游戏内(延迟基本可以保持在50ms以内),所以这种策略是很有效的。lockstep每个回合的触发,并不是由收到网络包驱动,也不是由渲染帧数驱动(当然渲染帧率稳定的话也可以以帧为单位,每N帧一个回合),而是采用客户端内在的时钟稳定按一定间隔( 比如100ms) 的心跳前进。游戏的一开始,玩家在本地进行操作,操作指令会被缓存起来。在回合结束前(网络延迟在50ms以内),我们会收到所有其他客户端的指令数据,然后和前面缓存的指令一同执行并推进到下一个回合。如果玩家在一个回合开始到50ms(网络延迟的一半)都没有任何操作,我们可以认为玩家发出了一个Idle指令,打包发给其他客户端[13][14]。

换个角度来看,假如一场游戏持续了20分钟,不考虑延迟的情况下整场游戏就是12000个回合(所有客户端都是如此)。现在我们反过去给每个回合添加指令,确保每个回合都收集到所有玩家的指令,那么就可以严格保证所有客户端每个回合的表现都是一样的。假如我们再把这些指令都存储起来,那么就推演出整场比赛,这也是为什么lockstep为什么做回放系统很容易。

至于lockstep为什么要发送指令而不是状态,其实是与当时的网络带宽有关。很多游戏都有明确的人数限制(一般不超过10个人),玩家在每个回合能做的操作也有限甚至是不操作,这样的条件下所有玩家的指令一共也占用不了多少带宽。如果换成同步每个角色的状态数据,那么数据量可能会膨胀10倍不止。从原则上说,锁步数据既可以是游戏角色的状态信息也可以是玩家的操作指令,只不过由于各种原因没有采取状态数据罢了。在下一章,我还会对状态同步做进一步的讲解,他与lockstep的发展是相辅相成的,也不是网上常说的那种对立关系。

  1. //lockstep操作指令的结构体
  2. struct Input
  3. {
  4.         bool up;
  5.         bool down;
  6.         bool left;
  7.         bool right;
  8.         bool space;
  9.         bool Z;
  10. }
复制代码
  • 仔细分析一下lockstep,其实就能发现不少缺点。首当其冲的就是网络条件较差的客户端很容易影响其他玩家的游戏体验。为了尽可能保证所有玩家的游戏体验,开发者们不断的对游戏进行更深一步的分析,先后提出了各种优化手段和算法,包括使用乐观帧锁定,把渲染与同步逻辑拆开,客户端预执行,将指令流水线化,操作回滚等。关于回滚还有很多细节[16][18],我会在下个章节里面进行更详细的阐述。

其次,lockstep的另一个问题就是很难保证命令一致的情况下,所有客户端的计算结果完全一致,只要任何一个回出现了一点点的误差就可能像蝴蝶效应一样导致两个客户端后面的结果截然不同。这个问题听起来容易,实际上执行起来却有很多容易被忽略的细节,比如RPC的时序,浮点数计算的偏差,容器排序的不确定性,随机数值计算不统一等等。浮点数的计算在不同硬件(跨平台更是如此)上很难保持一致的,可以考虑转换为定点数,随机计算保持各个端的随机种子一定也可以解决,但是具体实现起来可能还有很多问题,需要踩坑之后才能真正解决。


您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|小黑屋|3D数字艺术论坛 ( 沪ICP备14023054号 )

GMT+8, 2022-6-29 19:59

Powered by Discuz! X3.4

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表