解析《NBA2KOnline》同步和网络方案
一、背景介绍
《NBA2KOnline》项目是一个合作开发的篮球类竞技游戏,项目从立至今,前后经历了二年多时间,同步和网络方案前前后后也更新了好几个版本,这里就把我们项目在这块上的一些经验与大家简单分享一下,后续如果大家这块有进一步的想法也可以和我们项目的Dave, Lixin等人进一步讨论下。
作为一个运动类单局游戏,它的同步需求和RTS类游戏非常相似,都是强调高实时性。因此,接下来介绍老的客户端同步方案即和星际中所采用的同步方案类似,新的服务器同步方案把主机由服务器换成某个客户端后就类似魔兽3中所采用的同步方案,后者就是我们接下来要重点介绍的同步方案。
二、名词解释
帧同步:以帧为基本计时单位的一个同步方案,具体来说每个客户端都必须运行一样的逻辑帧顺序(每个客户端播放效果就像是看视频似的,允许有缓冲,但是帧序列都是一样的。)
Game Server(游戏服务器): 这个概念与其它游戏类似,游戏逻辑服务器,这里与Relay Server相对而言,在本篇文档中基本上汲及不多,这里主要是用于比赛开局结束控制,单局事件校验等逻辑功能。客户端使用TCP与它连接。
Relay Server(中转服务器): 帧同步算法的实现服务器,基本上与游戏逻辑无关,与业务关系也不大,同类游戏的逻辑都类似,因此独立成Relay Server,也方便就近分布,有利于降低延时。客户端会同时使用TCP与UDP与它连接,TCP用于发送控制命令,数据包主要通过UDP发送接收。
Player Input(用户输入): 用户输入表示一个用户自己的操作输入。可能由用户自己输入,也有可能由服务器帮忙伪造出一个用户的输入。
Frame Input(帧输入): 表示一帧的输入数据,由该局比赛某一帧的全部用户输入组成,客户端只能拿到Frame Input后,才能运算和渲染对应的某一帧的画面。
切帧:指的是中转服务器的切帧逻辑动作,中转服务器会以一定的固定频率进行切帧,如每秒进行30次的切帧,那么在每1/30秒内,它都会产生一个帧输入给比赛的玩家,如果此时有某个用户的输入因网络问题还没到达服务器,中转服务器会帮忙造出一个输入,从而补齐一个帧输入发送出去。
快播:正常情况下,客户端会均速每秒若干帧的播放帧输入序列中的包,但是因为网络质量的缘故,客户端收到帧输入序列并不是完全匀速的。当客户端的帧输入队列缓冲区中累积了较多的包时,就需要客户端能够快速的播放掉这些累积的帧(例如只运算不渲染这些帧,或者运算完后不做任何延时尽可能快的播放下一帧),从而降低后面的输入操作延时,这种行为就称为快播。
三、游戏架构
我们游戏在CE1到CE4的时候采用的是旧同步方案,而为了解决客户端之间的相互依赖等问题,在今年CE5的时候做了一个重大的更新,更新成了新的服务器同步方案。接下来我们简单看下两者的区别。
3.1、旧同步方案架构
该同步方案的特征是:
采用P2P进行通迅,单局内玩家增多的情况下,其维护的总连接也将是恐怖的增长。
中转服务器只是帮忙打通连接和转发包,没有任何组包分包策略。
从上图我们可以看到,采用了P2P方案,能够极大的减少服务器带宽的使用量,但是缺点就是高度依赖客户端之间的P2P网络,同时会产生短板效应,即最大延迟取决于客户端之间的最大网络延时,以及各个客户端之间的最低FPS,使得在绝大部分的单局体验都不是很令人满意,这种方案在NBA项目组经过CE1到CE4的玩家实际验证后就彻底废弃了,于是就演化成了下面新的同步方案。
3.2、新同步方案架构
新同步方案的特征是:
放弃了P2P通迅,每个客户端只与游戏服务器和中转服务器连接即可,极大的简化了网络模型。
中转服务器加入了切帧造帧逻辑,让每个客户端都可以互相独立。
放弃P2P,让玩家全部只与中转服务器相连能够保证网络是更加牢靠的,同时也能让单局人数规模扩大后客户端上下行的带宽占用基本可控,低价就是需要牺牲中转服务器的带宽,我们通过优化,相对于体验的提高,认为这部分是完全值得的。
同时让中转服务器有了切帧相关逻辑,使得每个客户端不需要去等待其它所有客户端发来的输入,能够让较好网络和配置的人不被同局其它短板人物所拖累,从而提高绝大部分用户的体验。
四、详细说明
4.1、前置条件
采用该同步方案客户端主要有两个前置条件需要先完成: 首先要求客户端具有完整的单局逻辑功能。以NBA为例,即比赛过程中的所有逻辑如投篮进球使用道具等客户端都必须有完整的逻辑(类似于星际魔兽等RTS游戏)。这和常规的MMO客户端有点不大一样,因为只有客户端具备所有的逻辑后才进行完整的运算。对于外挂风验,后面我们可以通过校验所有玩家的帧特征码事件来解决。 另一个前置条件是必须保证客户端的每一帧的运算结果只与上一帧结果和输入操作有关。这就意味着同一单局客户端的随便数种子必须一致,关键数据的运算精度也必须一致,这样当输入序列相同时才能够产生相同的结果。这里因为逻辑不断开发增加的原因,很容易引起各种的数据不一致BUG,会是将来客户端程序调试的重点。
4.2、服务器说明
4.2.1、游戏服务器(Game Server)
主要用于游戏逻辑控制,与普通的游戏逻辑服务器类似,客户端会采用TCP长连接与游戏逻辑服务器相连,并由游戏逻辑服务器控制整个游戏的运转。在比赛过程中,它主要会有道具技能校验功能,以及客户端事件上报校验等功能。
4.2.2.中转服务器(Relay Server)
客户端在每个单局比赛开局时会收到中转服务器地址,并与中转服务器建立TCP长连接,同时准备UDP通道。与中转服务器的TCP连接主要是用来管理客户端在中转服务器上的生命周期,以及发送少量的控制命令。而单局中最重要的用户输入操作和帧输入操作都是通过UDP来发送和接收的。因为下行的帧输入序列是不允许丢包的,需要在逻辑层面实现UDP的重传和定序功能。
4.3、流程图
l 首先在与中转服务器连接开局后,每个客户端发送自己的输入给中转服务器
l 中转服务器按照一定的频率和切帧算法进行切帧,如果此时没有收到某个用户的输入,它将帮忙造出输入补齐成帧输入,并发送给所有客户端。
l 客户端收到相应的帧输入后,即可进行运算和渲染
l 重复新一帧的发送和接收。
需要注意的是我们的上下行都是采用UDP发送数据,其中上行是允许丢包,下行发送的是帧数据,是不允许丢包的,如果出现丢包,需要等待请求重发。
4.4、客户端缓冲区调整算法
4.4.1.目的
在实现的网络模块中,有各种各样的网络延迟,抖动,以及丢包等。我们主要是设计出一个算法,能够对大部分玩家的网络模型做出比较好的体验。明显,少量的丢包我们通过冗余来解决,大量的连续丢包网络模块我们只能让其等待重发,这也是合理的。
因为网络是不稳定的,因此输入速度也是不稳定的,长久来看是平均在30FPS(假设服务器以30FPS进行切帧),但某些时刻会远远大于或者小于30,因为我们的原则是设计出一个算法, 让输入在不太稳定的情况下,尽量的让输出画面稳定。
4.4.2、算法简述
经过实际实验,我们采用MAPDV2的算法,通过得到网络帧数据的时间来抽象出网络的参数,在网络条件较好时进行极限的播放(缓冲区中基本上缓存数据);如果网络抖动较大,则缓冲合适的数据用于平滑抖动。本算法的基本原理可以描述为:
l 首先根据服务器向客户端发送的数据包的下行延迟,计算出下行通道的网络抖动值。
l 根据下行网络抖动,以及为保证玩家顺畅体验而量化的网络延迟容忍上限,共同决定缓冲区内可以保留的数据帧的数量。
l 根据第二步中得到的应该保存的数据帧数量,实际数据帧的数量,共同决定当前数据真的播放状态:正常播放,快速播放,或者停顿等待。上述三点在具体实现方法上可以分为三块:抖动计算,播放决策,以及快播数量控制。
五、扩展优化
5.1、客户端FPS扩展
服务器的帧率是固定的,而客户端配置是千奇百怪的。我们需要在不同的客户端上执行不同的帧率。因为当每收到服务器的一个帧数据后,会根据客户端需要的FPS情况(高配机希望执行高画质和高FPS),将其复制成1~N份,然后传递给客户端的逻辑层进行处理。这样就能动态的扩展客户端的帧数。但是,这种情况会受到同一局比赛中所有客户端必须一致的制约。
通过以上两个函数进行服务器帧与客户端帧号的相互转换,达到让客户端FPS得到扩展。在我们实际的测试过程,经过扩展后的游戏,帧率提高了,同时手感并不会降低,因此比赛将会更加的流畅。
5.2、下行冗余策略
下行冗余采用的是动态冗余策略。
5.3、上行冗余策略
上行冗余和下行冗余的情况类似,上行Player Input虽然可以丢包,但是太多的丢包也会造成手感不佳,上行包较小,增加冗余流量带来的成本不高,因为有较大的包头(约40byte)。
5.4、丢包重发策略
采用的是丢包重发策略,与之对应的超时重传策略实现上复杂不少,但效果类似。
六、反外挂与校验
6.1、校验原因
因为服务器没有单局比赛相关逻辑,都是由客户端来独立运算的。因此如果客户端没有一定的校验,很容易被外挂作弊,为此我们加了客户端之间的相互校验逻辑。
6.2、校验原理
如果初始状态一样和随机种子一样,那么只要每帧的输入高度一样(因为是由服务器切帧分发的,可以保持每个客户端的输入序列是一样的),那么每帧的运算结果也是一样的。为此让每个客户端验证每帧的结果是不是与其它几个客户端是一模一样的,我们就可以拿来做为校验原因。只要有不一样的结果,即有人做弊。
6.3、校验的数据
这里通俗的讲,我们可以校验当前帧的画面,生成一个画面的特征码(如当前关键数据的MD5码),如果每个客户端的每帧特征码都一模一样,那就是正确了。
当然一方面我们产生画面的特征码比较困难,另一方面我们也没必要校验这么高度的数据(比如不同客户端之间允许画质不同,UI可以不同等)。因引,我们抽像后只须校验整个游戏最重要的逻辑数据,比如球员的坐标位置,属性速度,技能数值等,把这些数据通过一个算法生成一个特征码后,就方便后面的校验了。而不在这个特征码数据内的内容,就允许各个客户端灵活配置,也不会对游戏产生平衡影响。
6.4、校验的算法
原则上,我们上行是允许丢包的,因此校验数据有时可能会没有掉,同时服务器会帮客户端切帧(如果网速差距大),那么服务器帮补的包中采用的是上一帧的数据,特征码也还会是上一帧的,因为有时会同一帧的特征码发几次。所以,我们允许有时不校验帧,有时同一帧的特殊码发几次,只要校验一直保持,就一定能检测出有不一致的数据。
我们定义了一个玩家输入结构,同理,我们会把上一帧的特殊码信息也放到这个结构中。
因为szBuffer结构体细究下来会是这样的一个结构:前四个字节标记为帧特征码的帧号,接着32个字节为帧特征码,最后才是输入的序列。原则上,帧特征码与帧号 必须连在一起才有意义。这样不管重发还是服务器重组包都能拿来做校验。
6.5.游戏逻辑的事件校验
以上校验主要是用于非常及时的发现出客户端中有第一现场的数据不一致,同时配合日志,来帮助前台开发程序解决数据不一致问题,其实对于反外挂来说,用不到如此高的精度。我们游戏中最主要的解决作弊的手段是每个单局发生中发生的任何事件(如投篮,进球,得分等)都会上报给游戏服务器,如果每个玩家的事件队列是一致的那么这局比赛就是正常的,如果事件不一致,即可以有认为有人作弊,后续可以配合运营措施来一起打击。
七、CE5实际数据
上表是我们游戏CE5测试期间某一天全部比赛的平均延时数据。从上表来看,平均延时在4帧左右(含一些极端差的玩家拉了后腿,大部分玩家可能更低些),操作延时就比网络延时大1~2帧,情况还是比较理想的,总体达到了我们同步算法的设计目的。