网络游戏的移动和视野管理
2022-11-25
移动的管理
网络游戏中,玩家的移动涉及到数据同步的问题,也就是说,客户端A的玩家角色移动时,客户端会向服务器上传一个走路包,服务器收到这个走路包后,会对本次玩家移动进行校验,比如校验一下本次移动的距离是否合理、是否遇到阻挡点或者处于限制状态导致不能移动等,如果校验通过则把把玩家A的新坐标存储起来,并将客户端A玩家角色的走路包再广播给所有玩家。这就是一次玩家移动所要经过完整流程。
那怎么去表达一次玩家移动呢?我们可以用以下几个信息来构成一次移动包:
- 玩家移动前的坐标(x,y)
- 玩家当前的朝向
- 玩家的移动速度
换言之,一个玩家的移动包,可以使用当前pos+移动向量来表示,如下图所示。
客户端需要在什么时机才向服务器同步一次移动呢?是否角色每走一步就同步一次移动包呢?这就是涉及到同步时机策略的问题。首先客户端不可能角色每移动一步就发一次移动包,客户端可以在这些时机发移动包的:
- 转向
- 变速
- 停止
- 移动距离过长或者移动时间过长
这个移动向量表示方式有很多。比如我们的游戏是这么表示的,首先他是这个2D游戏,而且移动速度v是恒定的:假设我们需要从(x1,y1)点移动到(x2,y2)点,玩家的走路包需要包含2个信息字段:
- 当前坐标(x,y)
- 移动向量path,我们定义为string类型
path的定义如下:
- path为0,向右下走一步
- path为1,向左下走一步
- path为2,向左上走一步
- path为3,向右上做一步
- path为4,向下走一步
- path为5,向左走一步
- path为6,向上走一步
- path为7,向右走一步
假设玩家的一个移动包是当前坐标是(x1,y1),path为"074",那么玩家的移动轨迹就是:(x1,y1) -> (x1+1,y1+1) -> (x1+2,y1+1) -> (x1+2,y1+2)
玩家移动后的坐标为(x2,y2) = (x1+2,y1+2)。 移动轨迹如下图所示。
服务器拿到这个移动包后,根据玩家的当前坐标按照移动向量path一步一步地计算轨迹位置,并进行移动校验。比如需要校验以下信息:
- 上传的x,y是否跟服务器记录的x,y差距过大。
- 玩家当前的状态是否可移动的。
- 在按步计算时,计算到移动轨迹到达一个坐标时,判断此时是否会发生碰撞或者阻挡。
- 计算得到移动后的坐标,校验玩家的本次移动距离是否合理,比如是否短时间进行了瞬移或者非正常加速。
网络游戏中的玩家移动,因为需要向服务器上传移动包,以及需要接收服务器广播下来的移动包,因此整个流程会受网络波动的影响。网络延迟高时,客户端看到的角色移动轨迹就会比较奇怪。
比如,网络延迟高时,我们会看到这样的情景:客户端角色A向前移动了一段距离,但是1秒后,被拉回了原地。我们分析下这个情景出现的原因:
- 角色A向前移动了一段,客户端上报移动包M1。
- 移动包M1由于网络不佳,导致丢包。
- 客户端A再次向左移动了一段,客户端上报移动包M2。
- 移动包M2顺利到达了服务器,但是服务器进行移动校验时发现,玩家上传的(x,y)跟自己记录的(X,Y)差距很大,因此校验不通过,直接下发一个位置纠错包,告诉客户端你当前的位置是(X,Y)。
- 客户端收到位置纠错包,直接把角色拉回(X,Y),玩家看到的情况就是角色突然被扯回几秒前的位置。
上面用到的是服务器直接拉扯的方法来纠正位置不一致的问题,当然我们也可以加一个策略,也就是我们约定好客户端如果角色是在移动的话,需要每0.5秒就需要向服务器发移动包,如果客户端没有收到服务器的回复或者收到服务器下发的位置修正包,那么也会触发位置拉扯,这样就可以避免玩家走了很长一段距离才被拉扯回原地的奇怪画面。
网络延迟其实细分为上行延迟和下行延迟,我们刚举的例子就是上行延迟,假设发生了下行延迟,就是客户端很多移动包都收不到或者都收得很慢,这个主要就是影响客户端收广播的移动包,玩家的感觉就是周围的人都不动了,或者周围人移动起来很诡异很飘,此时客户端可以采取运动补偿和追赶移动来弱化网络延迟带来移动诡异现象。
视野的管理
首先先问一个问题,为什么需要视野管理?一个根本原因是,客户端和服务端性能都无法处理全地图的数据,而且全图视野会导致带宽流量异常巨大,运营成本太高。假设一个地图有800个玩家,他们每秒钟移动5次,一个移动包假设1KB,一秒钟的下行的带宽消耗就是4MB,而游戏的特性又注定角色移动十分频繁,热门场景中实际每秒移动次数会多于每秒5次,消耗的流量会更大。更为重要的是,即使服务器采取了全图广播,客户端也很可能因屏幕限制或者性能原因无法展示全图场景,导致收到的绝大多数角色数据是没法呈现在界面上的,也就是说全图的广播是在做无用功。因此全图视野广播的策略并不可行,故引入了视野管理的方案。
视野管理的核心观点就是服务器只需要把客户端屏幕视野之内的数据发给客户端就好,视野之外的数据不需要下发,本质就是根据角色的可视距离来发送数据包。我们的游戏采取视野管理算法是九宫格算法,这里开始介绍这个经典的AOI算法。
我们把整张地图分割成N*M的小格子。玩家只能看到他周围的八个格子以及他所处的格子的角色,也就是他拥有9个格子的视野(九个格子加起来的视野比客户端屏幕要大一些)。每个格子都有一个玩家列表(数据结构我们可以使用链表或者切片),当有玩家走进该格子里,该这个的玩家列表就存储这个玩家的信息;当玩家走出这个格子时,格子玩家列表就删除这个玩家的数据。
当玩家走进一个新格子时,会看到一些新玩家(下图的new1,new2,new3),但同时势必会导致有一些格子不在位于自己的九宫格内,旧格子的玩家就不能再看到了(下图的old1,old2,old3)。这就是九宫格视野管理的策略。
我们从技术方案的角度再来捋一捋九宫格算法实现。
我们根据地图大小以及客户端屏幕视野的限制,把地图切割为 MAX_X/GRID_WIDTH, MAX_Y/GRID_HIGHT 这么多个格子。假设当前我的坐标为(x,y),那么我所处的格子索引为(x/GRID_WIDTH, y/GRID_HIGHT)。
GRID_WIDTH = 50
GRID_HIGHT = 30
MAX_X = 250
MAX_Y = 240
- 我们定义一个格子数组gridX[MAX_X]和gridY[MAX_Y],数组里存储的是一个切片
- 当玩家A进入格子(X,Y)时,我们在gridX[X]和gridY[Y],以及与其相邻的8个格子都append这个玩家数据。
- 服务器向这九个格子的所有玩家广播玩家信息,通知玩家A进入了格子(X,Y),客户端需要创建出玩家A的形象。
- 玩家A继续移动,移动到新格子了(X2,Y2),以及与其相邻的8个格子都append这个玩家数据。同时旧格子的玩家列表需要remove掉这个玩家信息。
- 服务器广播玩家A的移动结果,客户按视野列表创建玩家A形象或者删除A形象。