想起来就写一点,不定期更新。

定义术语

先定义几个不算特别专业的术语,方便后面的讨论,平时工作中都是这样叫的:

  • 主角玩家
    • 将游戏客户端中玩家自己的角色称之为主角玩家,简称主角,主角由玩家来控制。
  • 第三方玩家
    • 将游戏客户端中玩家自己的角色以外的其他角色称之为第三方玩家,简称第三方,第三方由服务器发送的同步消息来控制。
  • 服务器玩家
    • 家在服务器中对应的对象,有时根据上下文简称为服务器。

客户端预测&外推

Client-side prediction ,客户端预测,也可称之为客户端先行。

玩家通过客户端操作主角,不用等待服务器的验证,立即让主角作出响应,避免因网络延迟而带来的迟滞感,操控上跟单机游戏无异。

Client-side extrapolation,同样是预测玩家的行为,但预测对象是第三方而不是主角,称之为客户端外推。

当第三方到达服务器同步过来的具体位置后,可以继续向前跑(外推)一小段距离。外推的过程中,如果收到新的同步消息,则取消外推,以最新的同步消息为准。如果外推一段距离后,还是没有新的同步消息到达,也停止外推,原地站立或原地踏步。

这样做的好处是,第三方表现平滑,减少因等待同步消息而产生的抖动。

关于prediction和extrapolation的相关资料有很多,随便列几个:

预测和外推的限制

客户端预测和外推不能是无限制的,除了有距离上限之外,还需要结合场景的实际情况来。

(为了凸显问题所在,以下示例均使用clumsy制造了比较极端的网络环境:300ms延迟,且有50%概率阻塞发包200ms。)

譬如下图中的情况,左侧为第三方,右侧为主角(0.3倍速播放):

可以看到主角走到岩石边缘就停止了,但第三方却往前多走了一步掉下去了,然后收到停止的消息之后才折返回来。

在地势比较平缓的地面,多往前预测几步可能无所谓,视觉上不会特别明显。但在地势变化大的地方预测或者外推时,就应该要保守一点了,判断方式也很简单,如果预测出来的位置与玩家当前位置高度差太多(比如0.5米)的话,就放弃预测,原地踏步,表现上会好很多。

采取以上限制之后,在同等网络环境下的表现(原速播放):

可视化同步信息

同步是个细致活,诸如前面谈到客户端预测和外推的限制时对高度值的判断,有太多类似的经验值,需要不断的去调优。比如多长时间同步一次?预测多少距离或多长时间合适?角色的当前位置与服务器位置相差多少时需要加速追赶?相差多少时需要拉扯过去?等等。

这些经验值大都可以通过对网络情况的预估给个初值,但到底表现如何,还得放到游戏里看具体效果,然后调整,再看效果,如此反复直到达到一个比较满意的表现。

除去这些经验值以外,同步的具体方式也很关键,当确定了一种同步算法之后,也是需要放到游戏里具体验证的。

那么,如果验证效果不理想,如何得知问题出在什么地方呢?该调整哪个经验值?如何判断哪个环节出了问题?

猜是一种办法,但效率低下,更可靠的还是将一些关键数据在屏幕上显示出来,包括发送的数据和接收的数据:

  • 服务器玩家的位置和朝向

这个是最基本的了,时刻关注小红点和红线,说不定什么时候就不同步了,早发现早治疗。

  • 玩家状态信息(服务器、客户端):
  • 发送给服务器的位置和朝向信息:

向服务器同步过去的信息对不对?有无冗余?频率合不合适?一目了然。

(浅蓝行走,深蓝疾跑,黄色急转向,红色停止)

  • 从服务器接受到的位置和朝向信息

颜色与发送相同,另外外推的点也是红色,正常情况下不该有太多。因此,如果发现第三方玩家脚下的红线过多,就说明同步有点问题了:可能是网络不稳定,可能是同步的点计算有问题,可能是两边速度不匹配…

  • 其他…

优化手段

坐标压缩

游戏中的世界坐标范围大都很小,比如说x、z在2048以内,而朝向角度更是在360度以内。因此,没必要用4个float来同步位置和朝向信息,可根据实际需要使用比较少的bit来同步信息,可参考下面这篇文章:

Snapshot Compression —— Advanced techniques for optimizing bandwidth

消息合并

在服务器的某一帧,某个玩家的坐标位置可能会发生多次变化(可能是因为战斗激烈,玩家中了多个可改变其位置的技能,也可能是网络环境较差,客户端在一段时间内同步上来的位置积压到了一起,这种可能性更大一些),每次变化都会生成一个需要广播的网络包(在Inception中是这样),在发包时可以将这些位置信息合并,只保留最后一条,这样既可以减少服务器的下行流量,也可降低客户端收包的压力。

不过,这样做也不是完全没有缺点,如果想在客户端尽可能还原第三方移动轨迹,就不能将这些位置信息丢掉。

固定公式

如果某个状态的位置变化过程可以通过公式精确计算出来,服务器和客户端就只需要同步公式的起始位置,然后按照同一个公式计算后续的位置,既准确又不耗流量。

但有一个问题需要注意,如果每一帧的位置计算依赖前一帧的位置,那么由于服务器和客户端帧率的差异,或者由于某一方卡顿,计算的结果可能是不一样的,而且这种差异会累加放大。

给定起点位置start_pos,任一时刻t对应的位置curr_pos都可以按照一个start_post的数学函数计算出来,这种才适合用公式来同步。而根据前一帧的位置prev_post,即便使用相同的算法,计算出来的结果也可能是不一样的。