实时对战网络游戏--基于帧同步的最佳实践

2016-12-05
LOGO

网络游戏概述

网络游戏的发展始于90年代。历经超过20年的发展,游戏结构和内容发生了天翻地覆的变化。自2005年以后,网络游戏的结构逐渐趋于稳定。网络游戏从联网特性上,可以大致分为弱联网和强联网两大类。弱联网,如大部分的页游,部分的手游。除此之外的网络游戏,如MMORPG,FPS/TPS,RTS等,都属于强联网。弱联网游戏,结构相对简单,已经有大量前人的文章进行了分析,这里就不再赘述。本文将对强联网游戏进行分析,并针对其中的帧同步方式,进行深入解说。

强联网游戏,游戏种类比较典型的有MMORPG,FPS/TPS,RTS这3大类。

MMORPG游戏

一般使用权威服务器结构,典型的CS架构。游戏使用基于视野控制的区域数据同步,游戏的模拟精度较低,游戏整体延迟容忍度较大。一般单个场景,可以有2000个玩家,单位数量在10000以内,玩家可视同步数量在60-100,同步间隔在500ms。服务器承担游戏核心数据的运算。服务器上通常只有场景的可行走性,区域,触发器等关键数据,无整体空间数据。客户端作为游戏世界的窗口,将游戏内容呈现给玩家,并对玩家输入,进行反馈。游戏运算逻辑的分担设计,主要考虑玩家体验,服务器负载及反外挂需求。如果核心逻辑全部都由服务器计算,则安全性最佳,但客户端玩家操作的模拟,及多客户端之间由于同步延迟导致的画面不一致,将比较巨大。典型如国内武侠类游戏,刀光特效都在1米以上,有效攻击检测基本都在2米以上,这样做是为了就算两个玩家看起来位置很接近,但实际离很远,也能保证命中。不同游戏,主要是在位置移动同步,技能表现,动作表现这块进行调优,但所使用的策略基本大同小异。

FPS/TPS游戏

战斗部分一般使用HOST/CLIENT架构。游戏使用全场景活动单位的同步(状态,差值),游戏模拟精度较高,游戏对延迟的容忍度低。一般单个场景,活动单位数量在40个以内,同步间隔在50ms以内。主机承担移动验证及广播同步,命中计算(验证)及广播同步等逻辑,主机有完整的游戏场景数据。客户端负责游戏玩家的可视及操作反馈,移动逻辑一般在客户端直接执行,主机只负责验证。该类游戏为了玩家的操作的体验,甚至可以牺牲部分安全性,将射击等部分关键逻辑,直接在客户端运算执行,服务器只进行后置有效性验证。

RTS游戏

一般使用基于帧同步的架构,包含帧同步服务器及玩家端。帧同步服务器,只负责控制命令的队列化及帧数据的广播。每个参与战斗的玩家端,都运行整个游戏世界。游戏使用帧同步技术,游戏的精度高,数据同步量低,游戏场景活动执行的单位数,不受网络同步限制,可以在1000以上。每个玩家,作为对等端参与游戏的执行,玩家的操作将作为控制命令,发送到帧同步服务器,服务器把命令插入某个游戏帧,广播给所有客户端。

以上大致介绍了3种类型游戏的结构及同步方式。这里对同步方式进行下总结,MMORPG同步60个单位,每秒同步2次。FPS/TPS同步40个单位,每秒同步30次。RTS游戏,同步游戏逻辑帧,不同步游戏单位(游戏单位数量对网络同步量无影响),每秒同步30次。

3种方式,没有优劣的区别,只有适应某种类型游戏与否。从开发及维护难度上,各有自己所面临的挑战。但帧同步,因为部分特殊性,在3种类型的方式中,在维护难度上可能是最大的。以下我们开始详述帧同步所面对的挑战及解决方法。

帧同步的挑战

大家可以想象一下,N个客户端独立运行整个游戏,他们之间唯一的交集,就是有一致的驱动帧(控制命令)。每个客户端的游戏世界独立模拟,在模拟过程中,如果发生了哪怕一个非常细微的差异,在最后的游戏演变及结果上,都会造成巨大的不一致。这个就是帧同步开发及维护最大的挑战。

在开始说明我们要怎么做之前,这里对帧同步,这个比较模糊的概念,下一个更为明确的定义。我们这里所要讨论的,实际上可以定义为: 基于帧同步的对等计算。所有参与计算的对等端,有一致的开始,一致的变化过程,最终会有一致的结果。

一致的开始: 游戏有多个玩家,多个阵营。每个玩家,运行整个游戏世界,游戏逻辑相关的所有数据及程序逻辑模块完全一致。

一致的变化过程:a. 游戏中的逻辑驱动流程完全一致。b. 游戏中逻辑数据变化,完全一致。

每个参与游戏的玩家,可以说是通过某个特定阵营窗口,对游戏世界进行的观察及操作。游戏世界,所有人都一致,游戏窗口可以有不同的表现。

以上给出了明确的定义。在设计上,我们可以把游戏拆分为逻辑部分及表现部分。游戏逻辑部分,称为服务端逻辑;游戏表现相关,称为客户端表现逻辑。这里强制加入服务端,客户端概念,是希望让开发者,在进行开发设计时,把这两个作为独立部分进行处理。在编码上,甚至可以把它们放在两个独立工程下。服务端逻辑部分,表示完整的游戏本身,在校验服务器进行逻辑后校验时,也只会执行逻辑模块相关内容。客户端表现部分,会只读使用服务端的数据(本来就在同一个进程内),并通过一个特殊接口发送控制命令给服务端。由此在相同的服务端逻辑基础上,客户端表现可以是2D,也可以是3D,在实际开发中我们做过类似的适配。

现在可以开始我们的旅程了: 如何确保我们开发的逻辑在所有对等端都有一致的开始及一致的变化。如果发生了不一致,我们如何能检测到。我们需要一个框架来帮助我们解决这些问题,我们先把这个框架命名为IGameKernel。

游戏一致的开始

游戏框架服务端逻辑部分,在所有对等端中,根据一致的初始化参数GameStartDocument,进行一致的初始化构造,如场景,对象,逻辑模块等。如此游戏会有一致的开始。

游戏数据变化一致性

游戏数据类型,包括int,bool,float,string。其中int,bool,string运算不会有一致性问题。

对于需要确定性的帧同步,浮点数是一个坑。在不同的CPU架构,不同编译器版本,在乘法,除法,精度控制上,都可能具有一定程度的不一致。虽然理论上,只要编译器及处理器都遵循IEEE754标准,就能够确保浮点数的一致性。但在手游环境下,芯片种类繁多,执行的标准,尤其是对浮点数运算器的优化,都有一定的差异。因此要确保浮点数运算结果的完全一致,就算强制调用类似_controlfp(_PC_24,_MCW_PC);_controlfp(_RC_NEAR, _MCW_RC);代码,也不一定有效果。因此,能不使用浮点数,就不使用浮点数。如果必须使用浮点数,则一定要使用float64。在我们项目中,大约万次级别的测试中,游戏单位数量200以上,平均战斗3分钟,游戏过程及结果都能保持一致。

浮点数的完全确定性,在手游环境下是难以做到的。但在实际运行环境中,float64精度引起的误差,也许1亿盘游戏里会有一盘直接影响游戏结果。

另外浮点数相关的函数库,也是一个问题。数学库,如cos,sin等,甚至浮点数fmod,都需要自己实现一套,与环境编译器无关的实例。另外开方等高阶数学计算,因为存在cpu及数学库的依赖性,我们也需要自己手动实现相关函数,并保证结果的一致性。

其他关于数据运算,如随机函数等,也需要使用框架提供的接口,以保持一致性。

开方函数

游戏逻辑驱动一致性

游戏驱动,必须完全由IGameKernel框架完成。帧同步的细节由框架全部隐藏,在用户接口层,完全不体现关于帧的概念,开发者只关注通用意义上的驱动点。在结构设计上,框架把游戏逻辑实体分为GameObject,GameModule,GameObject包含所有数据,GameModule负责逻辑执行。框架提供的逻辑驱动点,只会包含command, heartbeat, event,critical, rechook这些。command:控制命令,heartbeat:心跳,event:框架内部事件,critical:游戏对象属性变更事件,rechook:游戏对象表格数据变更事件。通常的执行流程如下,框架执行某一个逻辑帧,逻辑帧包含两个部分,时间及玩家控制命令。框架会投递玩家控制命令进行执行,然后内部心跳管理器进行心跳更新。心跳更新时,所有注册的心跳被驱动,在心跳执行逻辑时,可以通过发送command执行各种定制行为;当有GameObject属性或表格数据发生变更时,也发送变更事件,并执行相关逻辑。该框架提供的机制强度,足够支持类似WAR3这种复杂度的游戏开发了(实际使用过程中开发过类似复杂度游戏)。

为了保证执行过程的一致性,框架只是第一步。我们还需要确保游戏逻辑调用是一致的。这里关注几个常见项: 排序算法是稳定一致的,容器遍历顺序是一致的。

kernel事件驱动回调

帧同步一致性检测

以上我们大致讲解了如何保持帧同步游戏的一致性。但是对于帧同步开发,哪怕开发团队很有经验,也很难避免出现因为人为疏忽,或者经验原因,导致的游戏不一致。此时我们需要一种手段能在最短时间,以最小代价,通过可重现的方式,自动化的帮助我们找到不一致点。

腾讯王者荣耀团队,有一个后台自动战斗及日志比对系统,该系统能解决部分问题。但这种定位还不够精确。

我们希望能直接锁定发生不一致的帧,并且精确锁定哪些游戏对象的哪些属性发生了不一致,该帧在哪个调用流程发生了不一致。而且最关键的,我们希望能通过程序,直接调试该不一致发生的过程。满足以上特性的框架,能把帧同步开发最大的挑战,有效解决掉。

这个是能做到的么,当然可以。这里给出我们的解决方案。

游戏对象,类体系结构由框架维护,即GameObject的所有类型,属性,表格定义,由框架维护。框架给出类似Get/Set接口,对某一个对象的命名属性进行操作。整个游戏的运行,由框架驱动并检测。框架在每一帧执行完成后,抓取游戏内所有GameObject对象的数据,以及该帧游戏运行驱动流程数据,打包上传到帧同步服务器。帧同步服务器在收集到某帧所有玩家提交的游戏数据后,对数据进行比对,分别比对所有GameObject的属性及表格,并比对所有的事件驱动流。如果发生不一致,则通知玩家端,不一致发生的完整数据信息(把该异常帧,所有玩家的数据都下发)。玩家端收集完异常数据后,游戏结束,并输出游戏录像,同时输出帧同步异常比对文档。文档中打印出游戏内所有游戏对象的数据及游戏运行流程数据,并标注清楚相同及不同标记。

在项目测试阶段,使用PC模拟端,IOS设备,ANDROID设备,运行自动匹配战斗脚本。当有帧不一致发生后,自动输出录像及异常信息文档。测试完成后,把相关的录像及文档发给项目组,项目组可以安排人员进行问题调查及重现,对复杂问题,也能通过异常录像直接调试。

帧同步检测文档

帧同步的主要关键问题

帧同步技术挑战与解决方案,到这里可以告一段落。在实际应用场景中,我们通常还会关注以下关键问题: a. 帧同步结构。 b. 游戏重入性。 c. 网络优化。 d. 游戏录像。 e. 反外挂。 f. 第三方库使用。

帧同步结构

传统的帧同步,使用锁帧技术。这个技术的要点,是在进行广播某一帧时,在这之前必须收到所有对等端前置的通知应答。这么做的理由只有1个: 保证公平性(大家进程一致,要卡一起卡)。在当前游戏领域,我们更关注的是一个玩家卡了,不能把其他玩家也卡住。因此我们采用乐观帧锁定方式。玩家操作,会以command形式,发送给帧同步服务器,服务器会把command插入某一个逻辑帧frame,然后在合适的时机把frame广播给所有玩家,游戏世界一致执行。在这个结构上,帧同步服务器,是以固定间隔把frame进行广播,某一个玩家卡了或者挂了,对于其他玩家是完全不可见,也是无影响的。

游戏重入性

重入性,断线重连及客户端重启后游戏恢复,都是依靠帧同步服务器来保证的。在服务器上,保存游戏开始到当前的所有游戏帧。这样断线重连就很简单了,客户端在网络断开恢复后,请求服务器重新发送从X帧开始的帧。服务器收到请求后,发送X到最新的逻辑帧到客户端,客户端快速运行完成这些帧,然后游戏继续执行。客户端重启也类似,客户端重启后,服务器通知客户端正在游戏中,客户端开始加载游戏,并请求载入游戏帧,服务器发送所有游戏帧,客户端快速执行到最新帧,游戏就可以继续执行了。游戏旁观,玩家中途加入,也采用类似方法。

网络优化

帧同步对于网络优化,是一个大问题。这里涉及几个点: 网络延迟及抖动处理,网络同步量及服务器承载优化,玩家操作延迟优化。游戏世界由逻辑帧,直接驱动,在不做优化的情况下,如果网络发生抖动,逻辑帧的到来时快时慢,对于玩家体验而言是致命的。在这里,先直接给出我们的做法: 帧聚合及帧延后执行技术。

帧聚合可以简单理解为把多个帧,集合起来发送。帧延后执行,大致是本地缓冲部分逻辑帧,按照玩家本地速率执行,把网络抖动抹平。

关于网络优化,帧同步使用UDP还是TCP, 这个问题是被很多人所讨论的,这里也给大家一个参考结论。帧同步,需要可靠传输,因此网络传输层一定要具有可靠性。另外,部分基于帧同步制作的游戏,还希望要能有最短的用户操作反馈延迟,比如100ms以内。综上,我们简单分析下两个协议。TCP是可靠传输协议,但是TCP协议,对于网络丢包重传机制,是基于网络公平性,总流量较少来设计的,其RTO机制,在发生丢包时,需要较长时间恢复。TCP底层实现过于复杂,无有效参数来直接设置RTT或者RTO超时性,对于减少丢包后的恢复时间,基本无解。UDP是不可靠传输协议,甚至可以认为只是在IP层上做了一个基于分包的简单处理,不对链接管理,传输可靠性进行任何处理。这个也是UDP的优点,纯净无添加。在进行帧同步的底层开发时,也可以使用UDP协议,自己开发链接管理及可靠性传输(这里不需要开发NAT穿透等功能,因此难度一般),并把RTO,RTT按照最快速度恢复策略,定制编码即可。最终实现一个简化的,基于最快速度恢复丢包的,TCP like协议。

最终,帧同步网络底层,因为都是使用可靠传输协议,使用TCP还是UDP,在框架库中,只是一个初始化参数的问题。接口层可以完全一致,因此对于最终开发用户,TCP还是UDP,只是运行期配置参数的问题。至于到底选哪个,要根据项目情况而定。如果是希望玩家操作延迟最低,则可以配置为使用UDP。如果是希望帧同步服务器负载能力最大化,则配置为使用TCP(用UDP封装简化的类TCP网络库,系统运行期网络消耗及CPU消耗,都会大于原生的TCP接口)。

网络服务层服务句柄

游戏录像

游戏录像功能,是帧同步天赋属性(其他游戏种类都不具备该属性)。唯一需要关注的,就是录像压缩。在帧聚合中,把多个帧压缩为了一个帧,但这还不够,我们在录像中,只记录存在有效帧的数据。然后再把该录像序列化,再通过类似zip等压缩方式,做整体打包。通过以上方法,类似皇室战争复杂度的游戏,一场战斗,处理过的录像大小,基本能控制在1k字节以内。

反外挂

反外挂是一个很大的议题。帧同步结构中,所有数据都在玩家本地,理论上玩家可以任意修改这些数据。这里不讨论传统的加壳及反调试技术。这里讨论在实际开发中,帧同步框架能够通过什么方法来解决该问题。框架能提供至少3种保护: a. 关键数据保护,b. 虚拟化, c. 服务器后验证。关键数据保护可以有很多技术,框架对核心数据,可以做内存加密,内存多拷贝冗余保护等。框架提供虚拟化技术,也是一个不错的选择,部分代码可以在虚拟机(lua)中直接执行,破解难度会增加(前提是资源保护足够)。服务器后验证是杀手锏,验证服务器能运行游戏录像,并直接得出游戏战斗结果,任何作弊都无所遁形。

因此对于帧同步,反外挂相对是一件比较容易的事情。游戏过程中,玩家作弊只会影响到自己,不会影响到他人。游戏结算时,当服务器检测到玩家之间游戏结果不一致时,通过验证服务器,对游戏录像进行验证计算,很容易就能发现是哪个玩家发生了作弊。

第三方库使用

游戏开发中,通常会用到很多第三方库。比如物理引擎,寻路引擎等。在帧同步开发的项目中,逻辑模块所使用的任何库,都需要满足确定性原则,即对于某个输入,必须要有一致的输出。因此对于第三方库的使用上,一定要慎重。而这也潜在的增加了项目开发及维护的成本。比如绝大部分物理库,寻路库,如havok,kynapse就无法直接使用。

笑脸-来源网络

帧同步总结

到这里,关于帧同步的开发已经讲解的差不多了。帧同步技术很容易用,但要用好,却不是那么简单,而且如果用不好,对项目甚至可能是致命的(有做射击游戏的,使用帧同步,项目死在物理库上的)。本文将实践中,遇到的最常见的挑战与解决方案,与大家进行了分享。如果在开发中碰到困难,可以联系我们进行咨询。如果本文所阐述的内容有谬误的,也欢迎指出。

声明:游资网登载此文出于传递信息之目的,绝不意味着游资网赞同其观点或证实其描述。

最新评论
暂无评论
参与评论

商务合作 查看更多

编辑推荐 查看更多