文章目录
- 一、服务器架构
- 二、两种同步模式:状态同步和帧同步
- 1.同步
- 2.状态同步和帧同步的区别
- 三、流量
- 四、回放&观战
- 七、开发效率
- 八、使用帧同步的知名游戏
- 九、断线重连
- 十、注意点
共享信息(数据)。
那么共享信息,他需要具备什么特性呢?
仲裁人(权威)。
服务器的权威程度取决于他执行逻辑的百分比。
比如FPS游戏,从理论上来说,服务器应该去计算弹道,计算走位,但由于一些限制,如网络、算力。我们不得不让客户端自行计算,服务器做简单校验。这种就很容易出外挂,参考吃鸡。这种类型的游戏就是弱服务器类型。
再比如RPG游戏,他的走位是一定要放在服务器计算的,但由于每个客户端的网络情况、客户端表现能力等不同,客户端需要做一些优化,比如:移动预演。
移动预演的意思是,客户端和服务器同时计算路径(或者服务器大概计算路径),客户端不通过服务器校验返回的情况下,直接开始行动。等到客户端完成走路行为时或某个关键时候(比如转向等),把结果发给服务器验证,通过则不理睬,如果有偏差,服务器就会强行修正,表现得行为为:拉回。
大家都见过一种情况,玩LOL或魔兽世界的时候,突然网卡了,但你看到周围的玩家仍然在运动,但过一会网络恢复,那些玩家就会被强行拉回正确的位置,这就是因为客户端执行的结果与服务器预期不同(或超过容忍程度),而被强行拉回到正确的坐标。
再说棋牌或桌游类型,我们可以统一说成回合制,这种类型因为算法简单,没有高频的同步信息,只需要偶尔播放一下场上的变化,如发牌、胜利、失败等。他们的逻辑可以完全放在服务器且服务器根本不会有任何压力。
一句话:太多的东西丢在客户端计算,就很容易出外挂。
我们需要根据游戏中计算复杂度、网络、逻辑重要性等因素来综合权衡这段逻辑是否放在服务器上执行。
所以服务器他本身的作用就是帮客户端计算,然后把计算的结果返回给客户端,就像是换了一个地方执行某些逻辑一样。
扩展成多个客户端,无非就是服务器把刚才计算的结果,告诉所有需要被通知的客户端。
什么叫需要被通知的客户端?
很简单,在RPG中,同个屏幕里的玩家可以互相看到对方,那么每个人路径的计算结果服务器就需要广播给同屏的所有人。
如果某个人拥有隐身技能,那么这个隐身的人的信息也不应该被广播给其他人,所以抽象点说,就是根据游戏逻辑和玩法决定的。
在房间类游戏,如棋牌游戏,服务器广播的对象就变成了房间内的所有人。如果是LOL,则还会根据视野来确定是否广播行为。
说到LOL的广播,就引出一些很有意思的细节。 比如EZ的Q可以用来探草丛,为什么,因为那是服务器告诉你,弹道在某处停了下来,或者说他命令客户端播放了命中的声音。但是如果是雪人站在草丛里放大,你还真不容易察觉,因为服务器并没有命令无视野的客户端播放大招动画。再不如塞恩的Q,指示器是没有广播给无视野的客户端,所以你经常被Q飞起来但并未提前察觉草丛里是否有个塞恩。
当然了,还有一种很偷懒的办法,那就是帧同步,RPG游戏大多都是状态同步。
帧同步,顾名思义就是服务器只管收集客户端每帧的输入,想象成键盘或手柄的指令,然后做广播,所有客户端做同样的运算,然后得出同样的结果。当然,服务器得做一些非常简单得校验。 帧同步需要用同样的随机种子,因为如果客户端执行的函数中有随机数的逻辑,我们同样得保证这个随机数大家随机出来的结果都相同。
帧同步的服务器一般是解决一些算法非常复杂,实时性要求比较高的游戏。具体怎么判断?
当网络卡住的时候,如果所有玩家都定住没有做任何行为,则可能为帧同步,帧同步掉线重连之后还需要补帧,这个就不做扩展了。
这个大概就是在补帧吧……
补帧的意思基本与状态同步相反,状态同步,服务器会把游戏的主要数据,比如场景,玩家属性,场景上的物体等全部存起来,这样玩家断线重连的时候,只需要恢复当前玩家周围的情况就行了。
但帧同步不同,服务器并没有存储游戏中的状态数据而是客户端输入数据,所以当玩家断线重连的时候,客户端为了获取游戏中最新的状态,则需要服务器把执行下发下来,客户端重新演算一次。
所以假如没有上图的这个等待画面或者没有屏蔽前端表现得话,你会看到,游戏就像按了快进键一样飞速地播放着。
不过帧同步有个好处,那就是做录像回放非常方便,只要代码中的逻辑没有太大变动,那么我们只需要记录从游戏开始到结束的所有指令就好了,播放录像的时候,把这些指令按照顺序依次输入到逻辑,然后就能还原这场比赛,最重要的是录像文件还非常的小。
一、服务器架构
就如最开始一篇文章中提到的,我并不太想空乏的说服务器的架构,因为这不是这个系列的初衷。但为了满足一下好奇心,我找到了几年前设计的“德州扑克”游戏的服务器架构给大家看看。
Public:公共服务器。
GameServer:游戏大厅里的玩法之类的。
FightServer:打牌的房间服务器。
这个设计其实很臃肿,开发也麻烦,所以我后面的主张是,如无必要,勿增模块。 这里的模块指的是非相同逻辑的进程,因为在分布式设计中有一个永远绕不过去的难题,那就是异步的信任问题。
简而言之就是,如果有并行执行的模块,每次执行的回调都需要校验结果是否复合预期。更需要考虑模块通信是否会失败,在哪一步失败我们又怎么处理。
就好像现今的一种很流行的中间件:MQ(消息队列),用户下单之后,为了保证快速响应,我们需要把下单的过程异步化,异步的将单子的信息传递给其他模块。
那么如果异步中,某些环节出了问题该怎么办?就像德州扑克服务器架构图里面一样,我申请了匹配,但是我突然退出了或者无征兆断网,匹配服务器Public怎么处理?如果我在游戏中突然断网,FightServer怎么处理,玩家重连上来又如何可以找到当时的FightServer是哪一个实例?我在战斗中胜利或失败了,FightServer需要同步战报到GameServer,战报会影响到金钱,当时正有一个GameServer的活动正在送金钱,该如何处理?我在GameServer上有1块钱,我在FightServer中没钱了,我需要从GameServer中补充钱1块钱上来,GameServer校验的时刻,钱明明是够的,但是在和FightServer通信过程中(FightServer实际还未增加余额),GameServer有个活动扣除了金钱,但实际上金钱已经被FightServer拿走了,FightServer上增加了本场游戏余额,但GameServer上的活动也扣除了,钱扣了两次。
头都绕晕了,但无外乎就是异步带来的问题。
不管是多进程,多线程,都是在致力于高效率的同时,解决数据不同步的问题。
你们可以想想数据库存取钱的问题。
A取钱时,余额读出来为100,于是A准备做-100的操作。
在这个过程中,B存钱时,发现余额是100,于是B准备做+100操作。
A先操作,数据库中的余额变成了0。
B后操作,数据库中的余额变成了200。
这不是空手套白狼吗。所以数据库中引入了一个事务和事务隔离级别的概念,如果了解的同学可以发现,最高隔离级别是串行执行,串行就是同步执行,先后顺序明确。
再给大家说一个例子,假设有个逻辑。
A需要调用B,但是A逻辑的结果取决于B的返回,那么A就要想一个问题,如果调用B失败了,B并没有返回给我结果怎么办,好,我catch一个异常。
B这个时候纳闷了,我告诉你我执行的结果,可是我怎么知道我的结果传达到你A身上了?好,B也catch一个异常。
为了让B放心,A决定在收到B返回的时候,再给B一个信息,说我收到了。
那么问题来了,A怎么知道他发给B的信息,B又收到了?于是又catch一个异常。
B收到了A的确认,他也得告诉A啊,不然A不知道他收没收到刚才的信息。
发现没有,无限循环。
整个故事就是:
“我告诉你我知道了。”
“我怎么知道你告诉我知道了?”
“我怎么知道我告诉你然后你知道了?”
“我怎么知道你告诉我‘我告诉你你然后知道了’?”
如果你还不信,那么我直接告诉你去看看TCP的三次握手和四次挥手,你瞬间就明白了,面试这个的时候根本不用死记硬背,明白为什么这么做,随便他怎么问,你都是OK的。
说了这么久,其实别人早就发现这个不可解决的问题,并给了个名字。
拜占庭将军问题(Byzantine failures),是由莱斯利·兰伯特提出的点对点通信中的基本问题。含义是在存在消息丢失的不可靠信道上试图通过消息传递的方式达到一致性是不可能的。
所以,如无必要,不要增加模块,不要增加模块,不要增加模块!
二、两种同步模式:状态同步和帧同步
1.同步
所谓同步,就是要多个客户端表现效果是一致的,例如我们玩王者荣耀的时候,需要十个玩家的屏幕显示的英雄位置完全相同、技能释放角度、释放时间完全相同,这个就是同步。就好像很多个人一起跳街舞齐舞,每个人的动作都要保持一致。而对于大多数游戏,不仅客户端的表现要一致,而且需要客户端和服务端的数据是一致的。所以,同步是一个网络游戏概念,只有网络游戏才需要同步,而单机游戏是不需要同步的。
2.状态同步和帧同步的区别
最大的区别就是战斗核心逻辑写在哪,状态同步的战斗逻辑在服务端,帧同步的战斗逻辑在客户端。战斗逻辑是包括技能逻辑、普攻、属性、伤害、移动、AI、检测、碰撞等等的一系列内容,这常常也被视为游戏开发过程中最难的部分。由于核心逻辑必须知道一个场景中的所有实体情况,所以MMO游戏(例如魔兽世界)就必须把战斗逻辑写在服务端,所以MMO游戏必须是状态同步的,因为MMO游戏的客户端承载有限,并不能把整张地图的实体全部展现出来(例如100米以外的NPC和玩家就不显示了),所以客户端没有足够的信息计算全图的人的所有行为。
具体到客户端和服务端通信上,在状态同步下,客户端更像是一个服务端数据的表现层,举个例子,一个英雄的几乎所有属性(例如血量、攻击、防御、攻速、魔法值等等)都是服务端传给客户端的,而且在属性发生改变的时候,服务端需要实时告诉客户端哪些属性改变了,客户端并不能改变这些属性,而是服务端传来多少属性就显示多少属性(虽然可以改变客户端数值达到表现上的效果,例如无限血量,但是服务端那边的血量属性为0时,一样要死)。再举个例子,一个英雄要释放一个非指向性技能(例如伊泽瑞尔的Q),具体的过程就是,客户端通知服务端“我要释放一个技能”-》服务端通知客户端“在某地以什么方向释放某技能”-》客户端根据这些信息创建一个特效放在某地,然后以某个方向飞行-》服务端根据碰撞检测逻辑判断到某个时刻,这个技能碰到了敌方英雄,通知客户端-》客户端根据服务端信息,删除特效,被打的英雄减血同时播放受击特效。
而在帧同步下,通信就比较简单了,服务端只转发操作,不做任何逻辑处理。 以下图为例:
现在同一局里有4个玩家,也就是4个客户端,这时客户端A释放了一个技能x,此时将操作传递给服务端,服务端不做任何判断,直接把A的操作全部分发给ABCD,则ABCD同时让客户端A控制的英雄释放技能x。
三、流量
状态同步比帧同步流量消耗大,例如一个复杂游戏的英雄属性可能有100多条,每次改变都要同步一次属性,这个消耗是巨大的,而帧同步不需要同步属性;例如释放一个技能,服务端需要通知客户端很多条消息(必须是分步的,不然功能做不了),而帧同步就只需要转发一次操作就行了。
四、回放&观战
帧同步的回放&观战比状态同步好做得多,因为只需要保存每局所有人的操作就好了,而状态同步的回放&观战,需要有一个回放&观战服务器,当一局战斗打响,战斗服务器在给客户端发送消息的同时,还需要把这些消息发给放&观战服务器,回放&观战服务器做储存,如果有其他客户端请求回放或者观战,则回放&观战服务器把储存起来的消息按时间发给客户端。
#五、安全性
状态同步的安全性比帧同步高很多,因为状态同步的所有逻辑和数值都是在服务端的,如果想作弊,就必须攻击服务器,而攻击服务器的难度比更改自己客户端数据的难度高得多,而且更容易被追踪,被追踪到了还会有极高的法律风险。而帧同步因为所有数据全部在客户端,所以解析客户端的数据之后,就可以轻松达到自己想要的效果,例如moba类游戏的全图挂,吃鸡游戏的透视挂,都是没办法防止的,而更改数据达到胜利的作弊方式(例如更改自己的英雄攻击力)可以通过服务器比对同局其他人的战斗结果来预防。
六、服务器压力
状态同步服务器压力比较大,因为要做更多运算。
七、开发效率
首先要说,状态同步的游戏占主流,其次就是状态同步开发起来比较难。而帧同步服务器开发难度低,同一套方案可以给很多不同类型的游戏使用,反正都是转发操作;减少了服务端客户端沟通,老实说,没有扯皮的时间,开发效率最起码提高20%,状态同步的方案下,同一个功能至少需要一个客户端和服务端共同完成;PVP和PVE基本用的是同一套代码,做完PVP很容易就可以做单机的PVE。
八、使用帧同步的知名游戏
王者荣耀、魔兽争霸3、所有格斗类游戏
九、断线重连
状态同步的断线重连很好做,无非就是把整个场景和人物全部重新生成一遍,各种数值根据服务端提供加到人物身上而已。帧同步的断线重连就比较麻烦了,例如客户端在战场开始的第10秒短线了,第15秒连回来了,就需要服务端把第10秒到第15秒之间5秒内的所有消息一次性发给客户端,然后客户端加速整个游戏的核心逻辑运行速度(例如加速成10倍),直到追上现有进度。
十、注意点
需要保证每次随机的数字都相同,所以需要自己实现一套随机数,不能用unity自带的那个随机数接口,而且需要服务端发送相同的随机种子;因为非常微小的误差就有可能产生蝴蝶效应,所以所有float型的参数必须变成int型,保证计算结果一致。