0x00 前言
本期文章的写作动机是最近碰到的一个问题:在一款在线对战FPS游戏中添加一个多人一起踢球的模式。本以为这个问题解决起来很简单,稍微研究了一会才发现坑还不小。最终花费了好一段时间才初步做出了还不错的效果,于是决定写这篇文章记录一下,抛砖引玉,供大家参考。
0x01 问题描述
我们要实现的效果可以抽象如下:一个场景中存在一个球形刚体(RigidBody)和多个在线玩家,每个玩家都能触碰到球体,并对球施加作用力。由于涉及核心玩法,必须保证每个玩家看到的球体位置与朝向(即Unity中的Transform)同步。
0x02 状态同步初探
说起网络同步,自然会想起经典的各种帧同步与状态同步。
我们先来说状态同步,状态同步的思想是客户端将输入上传到服务器,由服务器计算出结果,再将状态广播给所有客户端,客户端使用获得的状态在本地更新渲染数据。原始的状态同步可以保证每个客户端上获得的结果都是相同的,这能够满足我们的需求,但也存在一些问题:
服务器为了节省带宽,通常状态同步的频率不会很高(
网络环境不稳定,出现丢包或卡顿时,物体的移动会不连续
输入延迟较大,玩家的操作需要等待一个来回才会反映到屏幕上
左:客户端 右:服务端 同步频率:每秒10次
其中1可以通过内插值解决,2适用于外插值,3的经典解决方法则是客户端预测+状态矫正。
0x03 内插值(Interpolation)平滑
在收到的两个数据包间通过线性插值插入过渡数据,可以有效平滑物体移动的视觉效果。具体情况如下:
仅需要同步位置(12字节)和旋转(16字节)数据
不立刻应用收到的状态数据,而是多等待一个数据包
根据时间差在过去的两个状态数据间线性插值
对于四元数表示的旋转数据,使用球面插值(Slerp)而不是普通插值(Lerp)
客户端操作 -> 服务器计算 -> 回传状态之后才会开始运动,引入2倍ping值的延迟
左:客户端 右:服务端 同步频率:每秒10次 线性内插
对于高速反弹等大幅改变运动状态的情况的模拟会出现一些问题
一次快速折返被忽略了
表现效果与通信频率强相关,频率越高效果越高,但也会消耗很多网络带宽
如果出现网络波动连续丢包或者间隔太久,会停在半空中
可以根据收包间隔时间在内插和外插间切换,太久没收到新包,则用外插继续模拟
0x04 外插值(Extrapolation)推测
使用内插值同步时