浅谈物理引擎的网络同步方案

浅谈物理引擎的网络同步方案!

0x00 前言

本期文章的写作动机是最近碰到的一个问题:在一款在线对战FPS游戏中添加一个多人一起踢球的模式。本以为这个问题解决起来很简单,稍微研究了一会才发现坑还不小。最终花费了好一段时间才初步做出了还不错的效果,于是决定写这篇文章记录一下,抛砖引玉,供大家参考。

0x01 问题描述

我们要实现的效果可以抽象如下:一个场景中存在一个球形刚体(RigidBody)和多个在线玩家,每个玩家都能触碰到球体,并对球施加作用力。由于涉及核心玩法,必须保证每个玩家看到的球体位置与朝向(即Unity中的Transform)同步。

0x02 状态同步初探

说起网络同步,自然会想起经典的各种帧同步与状态同步。

我们先来说状态同步,状态同步的思想是客户端将输入上传到服务器,由服务器计算出结果,再将状态广播给所有客户端,客户端使用获得的状态在本地更新渲染数据。原始的状态同步可以保证每个客户端上获得的结果都是相同的,这能够满足我们的需求,但也存在一些问题:

服务器为了节省带宽,通常状态同步的频率不会很高(<每秒30次),造成物体的移动在视觉上不平滑

网络环境不稳定,出现丢包或卡顿时,物体的移动会不连续

输入延迟较大,玩家的操作需要等待一个来回才会反映到屏幕上

其中1可以通过内插值解决,2适用于外插值,3的经典解决方法则是客户端预测+状态矫正。

0x03 内插值(Interpolation)平滑

在收到的两个数据包间通过线性插值插入过渡数据,可以有效平滑物体移动的视觉效果。具体情况如下:

仅需要同步位置(12字节)和旋转(16字节)数据

不立刻应用收到的状态数据,而是多等待一个数据包

根据时间差在过去的两个状态数据间线性插值

对于四元数表示的旋转数据,使用球面插值(Slerp)而不是普通插值(Lerp)

客户端操作 -> 服务器计算 -> 回传状态之后才会开始运动,引入2倍ping值的延迟

对于高速反弹等大幅改变运动状态的情况的模拟会出现一些问题

表现效果与通信频率强相关,频率越高效果越高,但也会消耗很多网络带宽

如果出现网络波动连续丢包或者间隔太久,会停在半空中

可以根据收包间隔时间在内插和外插间切换,太久没收到新包,则用外插继续模拟

0x04 外插值(Extrapolation)推测

使用内插值同步时,物体的运动始终落后于服务器两个发包延迟,引入外插值推测,可以将延迟降低到一个发包延迟。外插值的基本思想是,每次收到服务器的状态包,立即应用状态,并使用该状态的速度数据直接预测下一步的轨迹。具体情况如下:

需要在每次同步的数据包中加入线速度(12字节)

立刻应用收到的状态数据包

使用线速度随时间推测位置

可以使用诸如航迹推算(Dead Reckoning)等算法优化推测结果

在不发生碰撞的情况下,外推效果很好

一旦发生碰撞,由于不知道碰撞另一方的信息,仅凭自身数据推测结果完全是错的

给静止物体施力,启动延迟依然存在

可以在本地也跑一个物理引擎,让物理引擎来预测位置,并用状态数据不断修正

看到这里,想必聪明的你又双叒发现问题了:既然本地也能跑物理引擎,那直接用物理引擎算不就完了吗,还同步个什么呢?诶,先别急,这个问题就是我们将要面对的第一个关键问题。

0x05 蝴蝶效应

为何需要对物理模拟进行网络同步呢?因为在多人游戏中,其他玩家的位置是延迟的。本地玩家的位置通常由本机计算,与玩家的操作保持同步;但本地看到的其他玩家皆是由服务器转发过来的2个ping值前的位置。考虑这么一种情况,有一个球在直线前进,本地玩家从下往上尝试碰球,由于本地位置领先,本地玩家触碰到了球,而在服务器上则还没碰到

本地触碰球后,球发生反弹改变方向,而服务器上待1P玩家到位时,球已经通过,没有发生碰撞,于是失去同步。

这便是第一个需要同步的理由:由于网络延迟的存在,各端的状态是不完全相同的,而刚体会碰撞、反弹,这会让任何细小的差异迅速放大,进而失去同步。

除此之外,物理引擎还存在一个特有的问题,那就是物理模拟具有不确定性。

0x05 物理引擎的不确定性

这里我用Unity做一个简单的实验,在一个碰撞场景中,记录所有刚体的位置、旋转、速度、角速度,给一个大球添加一个Impulse去碰撞许多小方块,随后重置场景并重复这一过程。

每次重放结果都不同

可以看出,虽然状态相同,但模拟的结果却差别很大,这是因为Unity中默认并未开启PhysX的增强确定性模式。在项目设置中打开Enable Enhanced Determinism选项,PhysX可以保证在同一平台、同一优化配置(Debug/Release)、同一编译器、同一时序、同样步进间隔下的确定性,但若是涉及跨平台,则需要另请高明了。

浅谈物理引擎的网络同步方案!

打开增强确定性的开关后,能够实现重放

原来,现行的浮点数标准是IEEE754,但该标准只规定了应该怎么存储,具体的运算规则(包括舍入、扩展、NaN的处理等)并不包括在标准内。因此,不同的指令集(Arm与x86)对浮点数运算的操作存在细微的差异。多次运算后,微小的差异也会不断累积,导致最后刚体的运行轨迹南辕北辙。

物理引擎的不确定性问题最直接的影响就是没法用lockstep锁帧同步了,因为状态同步可以不断用服务器数据对刚体位置进行矫正,锁帧同步下即使我们能够在多端同步完全相同的操作,不确定性也会让各端物理引擎的仿真结果天差地别。