今天起开个新坑,准备维护一个开源项目,用来做为模板快速开发io类游戏。
一直很喜欢io类游戏,经常在国外的聚合网站上尝试种类繁多的io游戏。苦于没有专用的加速器,大部分的游戏是没法顺畅游玩的。而国内的io游戏又十分稀少,而且大多都是商业发行的游戏,相较于网页免费版的io游戏,不管是数量还是创意上都相去甚远。io游戏是种对抗性强,又十分容易失去新鲜感的游戏,所以我认为网页版的io游戏以丰富的数量和创意真正的体现出了io游戏的内涵。
我打算维护一个开源项目,用于快速的开发网页io类型游戏,尽量用最简单的方法解决网络同步等问题,让开发者可以把精力专注在游戏玩法之上,希望国内也有大量创意满满又可以流畅玩耍的网页io游戏(也可能已经有很多,只是我没发现)。
网络同步方式
首先是选择一个网络同步方式,目前主流的网络同步方式有两种,状态同步,帧同步 。
- 状态同步是指将游戏中的物体状态(位置,大小,旋转角度等)监控起来,一但发生改变就通知其他玩家,以保证不同玩家的游戏中的物体的状态是一致的。
- 帧同步是指将所有玩家的游戏帧同步增加,每向前执行一帧都要等到所有玩家的输入(这里指键盘鼠标手柄等输入设备的输入值)到齐后再进行计算。这种严格的帧锁定有个显而易见的问题就是网速慢的玩家会拖慢其他玩家的速度,大家都得等最慢的玩家的数据包到达才可以继续,所以我们一般使用非严格的帧同步,即服务器每隔固定时间间隔缓存所有玩家的数据不,间隔时间到了就将缓存的所有数据包分发大所有玩家进行下一帧计算,如果网速慢的玩家没有在时间间隔内将数据包传递给服务器,那就只能丢弃或者算作下一帧的数据。这样只有网速慢的玩家会感觉到自己的操作拖慢并不会影响到其他玩家。
两种同步方式各有优缺点,不同的游戏方式适合不同的同步方式,以下从几个不同的角度对比下两种同步方式。
服务器代码编写复杂程度:帧同步较优。
状态同步需要大量的服务器代码开发,几乎所有的游戏逻辑,物理碰撞等都需要在服务器端进行处理,客户端仅仅将用户输入传给服务器,并将返回的结果进行渲染,几乎不做逻辑处理。因为如果分别在客户端本地进行碰撞等逻辑处理的话就会因为网络延时导致判断结果的不一致性。
比如玩家A发出的子弹在本地页面碰到了玩家B,本地判断玩家B死亡,但是由于玩家A的网络不佳,玩家A攻击到的其实是1s前的玩家B的位置,这时候在玩家B的客户端中根本没有被任何人攻击到,但是却莫名其妙的死亡。如果说被击这么重要的判断应该在本地也就是玩家B的客户端中进行判断,那就会造成玩家A对着玩家B打半天却没有效果,同样的在玩家B的客户端里对着玩家A打半天也不会对其造成分毫损伤,因为他还卡在前1s。
为了保证逻辑判断的一致性,必须将重要的逻辑判断放到服务器。这就导致了大量的服务器端的代码编写,而且一个游戏要编写一套服务器代码,不同游戏不可以通用。如果是游戏服务器开发者还好,对于使用Unity3D,cocos2d等游戏引擎的单机游戏开发者而言简直是噩梦。服务器代码编写基本上是用socket.io加一些开源的js物理引擎,没有任何图形界面的ide,想象下做好了一个精致的tilemap,怎么才能将地图上的重要的碰撞区域放到服务器端里呢。其实由于下面提到的原因大部分的io游戏都是状态同步,这也是为什么io游戏都看起来场景简单,甚至没有场景,一群人在空旷的范围里大乱斗。
我曾尝试用其他玩家C作为主机,进行逻辑判断,再分发给所有人就行渲染。这样就可以把逻辑用Unity3D等游戏引擎编写,服务器只管分发。玩家A->服务器->玩家C,判断完成后->服务器->玩家A,但是双倍的网络延时,即使是国内的网络也没法流畅的玩游戏,也许再优化些会好点。
帧同步就不需要服务器进行逻辑处理,服务器只需要简单分发数据包就好了。玩家A的攻击操作(例如鼠标左键)同步到玩家A和玩家B的客户端,两个客户端同时进行玩家A的攻击判定,得到相同的结果。因为每一帧都是同步的,所有两个客户端的玩家位置都相同,做出的攻击判定也肯定相同,那么说玩家A延时高卡了1s呢?那么A会在按下攻击键1s之后才看见攻击效果。
客户端代码编写复杂程度:状态同步较优。
状态同步的客户端仅做显示,不做逻辑处理,任何游戏引擎都可以简单胜任。
帧同步就复杂的多,必须保证每个玩家客户端计算结果一致,这里就有随机数问题。比如玩家A攻击玩家B造成10-20之间的随机伤害,在A的客户端随机出20的伤害在玩家B的客户端随机出了10点伤害。我们需要一个随机方法在相同的地方会得到相同的随机结果。好在有伪随机函数(线性同余生成器),我们只需要在游戏开始的时候将相同的种子分发到每个玩家手里,使用相同的种子,每次得到的随机数在每个玩家手里都是一样的,这样玩家AB都可以得到相同的伤害数字。但是问题没有那么简单,为了得到相同的随机结果,不同客户端的执行顺序必须完全一致,如果玩家A在计算伤害之前比玩家B先使用过一次随机函数,就会导致两边伤害不一致。为了保证执行顺序的严格一致,在编写过程中,一些没有固定顺序的数据结构比如js的map C#的dictionary都要慎用,因为这些都不是保证顺序一致的,可能因为内存地址等原因在遍历的时候返回的先后顺序不一致。
除了随机问题,还有物理引擎问题。大部分的物理引擎都不具有确定性,因为使用了大量的浮点数,浮点数由于精度等问题在不同的机器上计算得到的结果是不同的。即使两个玩家之间的计算结果之相差小数点后面几位,但是在长时间的计算里会产生蝴蝶效应导致两个客户端的计算结果大不相同,由于帧同步只同步玩家操作,不同步游戏内物体的状态,导致两个客户端的差异无法得到修正。
中途加入游戏:状态同步较优
状态同步对于中途加入的玩家十分友好,只需要把所有的物体状态同步给新玩家就好,几乎不用做任何额外的工作。
帧同步就比较复杂一些,由于服务器中没有物体状态,所以中途加入的玩家需要将从第一帧到加入时的所有历史输入帧都重新执行一边才可以同步到与其他玩家一样的状态。这个追到最新帧的过程叫做追帧,要追上别人肯定需要更快执行速度,这个追帧的过程就相当于快放。
追帧对于开始时间较短的情况还可以接受,像大部分io游戏一局可以玩几十分钟,要是中途加入即使最快的速度去追帧,也得等上几分钟甚至十几分钟才可以追上。
所以使用帧同步的游戏基本上都是不能中通加入的,例如王者荣耀,开始的时候进行匹配,人满开始中途不可加入。而大部分的io游戏都是随时进入随时退出,无需等待匹配,也不用担心人少了匹配不到。
服务器带宽要求:帧同步要求较低
帧同步只同步玩家输入,每次传送的数据量就是玩家数量的倍数,数据量小,小带宽就足以满足多人同时在线。
状态同步每次传送的数据量是根据游戏中物体的数量来计算,需要同步状态的物体越多,传输的数据量越大,基本上这个数量是远大于玩家数量的,所以使用状态同步,要实现几百人的同时在线,服务器带宽就是必须要考虑的问题了,服务器带宽也不便宜,是服务器最主要的成本。
帧同步不用考虑游戏丰富度与带宽的压力影响,可以做出内容更加丰富的游戏,相比之下状态同步就要相对单调些。
客户端游戏体验:帧同步较优
帧同步由于所有的碰撞都是在本地进行,所以反应会更加灵敏,打击感更强。
状态同步在客户端输入之后还要等待服务器返回才可以看到结果,肯定是延时比较严重,即使在国内较好的40ms的延时,返回到客户端也有不小的延时手感。
所以帧同步可以做出格斗游戏般的手感,但是状态同步对于大部分io游戏也够用了。
反作弊:状态同步较优
状态同步由于所有判断都在服务器,机会没有作弊的可能。帧同步所有的判断都在本地,而且浏览器又是个完全开放的环境,防止作弊基本上不可能。不过对于io游戏这种娱乐休闲游戏,反作弊似乎也不是主要考虑的问题。
项目介绍
为了能使游戏开发者快速使用,我选择先实现帧同步的版本,主要是考虑到一个服务器可以给各种游戏使用,所有游戏逻辑都在客户端编写,这样尽量的保证原有的开发流程,减少学习成本。状态同步的等以后有机会再实现。
服务器采用nodejs+colyseus,colyseus(https://github.com/gamestdio/colyseus)是一个开源的游戏服务器,由于帧同步不用编写服务器代码,基本上可以写一次,所有游戏都使用。
游戏引擎使用cocos2dx creator,考虑到对web支持好点。Unity3D,Godot,Egret 如果需要的人多后面还可以增加。
下一期主要是用一个简单的游戏实现帧同步,主要是包括服务器,cocos2dx creator的帧锁定,玩家输入的分发,测试下box2d的物理引擎是否会导致不一致的情况。
以上如果想起什么遗漏的随时添加。
欢迎游戏开发者和游戏爱好者提出意见和建议,你们的关注才是我创作的动力,感谢。
物理引擎九成九不是确定性计算的。
@funcman:cocos2dx creator自带的box2d 不确定问题相当严重,在同一设备上,不同的执行频率都会导致肉眼可见的位置偏移。后悔选帧同步了,也没搜到什么可以用的确定性引擎,难不成得自己写个简单的物理引擎。
@83872309:不知道你这个游戏需要多强的物理效果。如果简单的话,你可以用定点数做一套碰撞反弹的运算库。即使这样也不简单。基本上帧同步需要你用定点数,并且保证每一处的运算顺序。你还好只是要做2D的,如果3D的话,定点数的物理引擎,容易耗尽CPU。我过去两年做这块,有一定了解。
@funcman:是的,另外实现物理效果也对原有引擎的工作流程改变太大,即使做出来也挺复杂。基本放弃用在帧同步里使用物理引擎计算,除去不能使用物理引擎,帧同步用来做回合制或者战棋类游戏还是很不错的。