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

0x00 前言

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

0x01 问题描述

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

0x02 状态同步初探

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

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

  • 服务器为了节省带宽,通常状态同步的频率不会很高(<每秒30次),造成物体的移动在视觉上不平滑
  • 网络环境不稳定,出现丢包或卡顿时,物体的移动会不连续
  • 输入延迟较大,玩家的操作需要等待一个来回才会反映到屏幕上

浅谈物理引擎的网络同步方案!_第1张图片


 

左:客户端 右:服务端 同步频率:每秒10次


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

0x03 内插值(Interpolation)平滑

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

  • 仅需要同步位置(12字节)和旋转(16字节)数据
  • 不立刻应用收到的状态数据,而是多等待一个数据包
  • 根据时间差在过去的两个状态数据间线性插值
  • 对于四元数表示的旋转数据,使用球面插值(Slerp)而不是普通插值(Lerp)
  • 客户端操作 -> 服务器计算 -> 回传状态之后才会开始运动,引入2倍ping值的延迟

浅谈物理引擎的网络同步方案!_第2张图片


 

左:客户端 右:服务端 同步频率:每秒10次 线性内插


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

浅谈物理引擎的网络同步方案!_第3张图片

一次快速折返被忽略了

  • 表现效果与通信频率强相关,频率越高效果越高,但也会消耗很多网络带宽
  • 如果出现网络波动连续丢包或者间隔太久,会停在半空中
  • 可以根据收包间隔时间在内插和外插间切换,太久没收到新包,则用外插继续模拟



0x04 外插值(Extrapolation)推测

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

  • 需要在每次同步的数据包中加入线速度(12字节)
  • 立刻应用收到的状态数据包
  • 使用线速度随时间推测位置
  • 可以使用诸如航迹推算(Dead Reckoning)等算法优化推测结果
  • 在不发生碰撞的情况下,外推效果很好
  • 一旦发生碰撞,由于不知道碰撞另一方的信息,仅凭自身数据推测结果完全是错的
  • 给静止物体施力,启动延迟依然存在


 

浅谈物理引擎的网络同步方案!_第4张图片

延迟降低了,但在碰撞时出现了穿墙


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

浅谈物理引擎的网络同步方案!_第5张图片


 

本地运行引擎+不断同步数据,效果不错


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

0x05 蝴蝶效应

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

浅谈物理引擎的网络同步方案!_第6张图片


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

浅谈物理引擎的网络同步方案!_第7张图片


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

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

0x05 物理引擎的不确定性

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

浅谈物理引擎的网络同步方案!_第8张图片

每次重放结果都不同


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

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


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

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

消除恐惧的最好办法就是面对恐惧,我们可以用下列方法强行解决不确定性问题:

使用基于定点数的确定性物理引擎,例如下面这个开源引擎

https://github.com/sam-vdp/bepuphysics1intgithub.com/sam-vdp/bepuphysics1int

保证仿真的时序和间隔,即保证对刚体的操作顺序,以及将步进仿真的操作放在FixedUpdate这样固定执行间隔的函数中
当然也需要付出一些代价:
 

  • 定点数引擎的性能消耗与速度要慢于原版
  • 修改引擎的工作量比较大,而且主流的开源引擎(PhysX、Bullet等)都找不到现成的定点数版本,得自己动手
  • 场景数据全部要重新导一遍,转为定点数
  • 使用定点数表示浮点数,数值范围有限,上面的BEPU引擎只能处理坐标在1000内的物体



0x06 站在G胖的肩膀上

光有理论知识还不够,参考现有的游戏能够帮助我们少走许多弯路。在网上分享这部分技术的游戏并不多,之前我玩过基于Source引擎制作的CS:Source和CS:GO,这两款游戏中都有实现多人踢球游戏模式,于是我在G胖的开发者社区中搜索了一下,居然还真找到了关于物理与同步的描述:

Physics Entities on Server & Client
developer.valvesoftware.com/wiki/Physics_Entities_on_Server_%26_Client
 

A major feature of the Source Engine is the physical simulation of rigid bodies. This simulation implements mostly mechanical and Newtonian physics like gravity, trajectory, friction, collisions, springs and damping. Models have to support this simulation by providing information about their collision model, material type, weight etc. In single player mode all physics entities are controlled and simulated by the server (server-side physics) and networked to the client. In multiplayer mode smaller objects like cans or bottles that don't affect gameplay are completely simulated client-side and are therefore not synchronized between clients. This is necessary because moving physics entities generate significant network traffic since they can change their position and orientation with every frame. Networking these updates would almost stall any connection as soon as lots of physics object start to move at the same time (explosions, etc). Client-side physics objects don't affect player movement, and they should always be significantly smaller than players so that a player can not hide behind the objects. When destroying server-side breakable objects, they break apart into smaller client-side simulated fragments.


大概意思是,Source引擎提供了两种物理对象,一种是由服务端完全控制,一种则仅本地客户端运行。服务端物理实体所有的模拟和操作都由服务器控制,客户端只做渲染表现,在多人游戏中会受延迟影响;仅本地物体实体通常用于细小的物体,例如瓶子、花盆等不会影响游戏的物体,这些物体的物理计算仅由本地模拟,而且不会与其他玩家进行同步。此类物体不会与玩家碰撞,玩家也无法站在它们上面,因此不会影响引擎中基于预测、插值、延迟补偿的状态同步。

这个方案跟前面设想的差不多,看来的确是一种可行的方案。我在CSGO中测试了一下效果,de_dust2地图中T家后面就有一个足球形状prop_physics实体,在高延迟下对球进行攻击,的确存在较高的启动延迟,与开发者文档描述相符。

此方案的效果比较依赖客户端与服务端的通信速率,达10次/秒时就基本可用,30次/秒时效果就很不错了。虽然在高延迟下,物体状态改变的延迟肉眼可见,不过这种情况下玩家也有心理准备,加上物理玩法并不是核心玩法,这样的效果可以接受。

0x07 火箭联盟的全预测方案

最终我们的项目采用了上面的服务器模拟+客户端插值渲染的方案,不过找到了一些别的资料,在此也分享出来。首先是GDC2018上火箭联盟做的演讲:

火箭联盟物理系统、网络同步讲解(含字幕)】It IS Rocket Science!The Physics of Rocket League Detailed_
 

  • 火箭联盟是一个以多人载具踢球为核心玩法的游戏,因此物理同步自然是他们要攻破的关键技术问题。
  • 采用了开源的Bullet物理引擎,这样可以自己修改和定制整个流程,而不像Unity中的PhysX是个黑箱。
  • 同步方案上,采用了类似守望先锋的关键帧同步方案,客户端和服务端各自运行物理引擎,客户端不等待服务器数据而是一路向前运行。
  • 客户端维护一个输入缓冲区,保存过去一段时间的每帧的输入数据。待收到服务器数据包后,按照帧号与缓冲区中对应的本地数据进行对比,判断本地在该帧的计算是否正确
  • 如果对比失败,说明预测失败,客户端状态回滚到收到数据包的帧号,应用服务器数据,然后连续运行多次物理引擎仿真,直到追到当前时间,完成一次矫正
  • 物理仿真部分,可以单独拿一个新引擎专门做需要同步物体的仿真,融入现有项目不会太难



这种方案的效果也是非常不错的,付出的代价仅仅是在预测错误时需要在一帧内回滚并消耗CPU连续运行多次仿真,对于PC/主机游戏完全可以接受。不过使用该方案也意味着需要重新部署一套支持确定性仿真的物理引擎,工程量上相对会大一些。

0x08 分布式授权方案

除了客户端预测方案外,笔者还找到了一篇应用在VR多人游戏上的物理同步资料:

https://gafferongames.com/post/networked_physics_in_virtual_reality/gafferongames.com/post/networked_physics_in_virtual_reality/

这篇文章设计了一个针对VR游戏特化的物理同步方案,具有以下特点:
 

  • VR游戏对帧数要求很高,通常在90Hz左右,并且由玩家手动操作,对延迟敏感,不适用单一服务端主控客户端表现的方案
  • 使用预测/回滚方案,对CPU压力较大,因为帧数高,每次回滚时追帧数量多
  • 不是强PVP游戏,所以可以容忍作弊,也没什么人作弊
  • 采用分布式授权方案,哪个玩家拿到球,哪个玩家就获得球的所有权变成物理主控端
  • 其他玩家像服务器主控方案一样渲染球
  • 需要处理好多个玩家竞争球权的情况
  • 可以实现本地几乎零操作延迟



该方案以容忍作弊+消耗更多的客户端带宽为代价换取了超低的本地互动延迟,对VR游戏这种在线人数少、客户端配置高、延迟敏感的专用场景来说自然是非常适合的,而且也不依赖于确定性物理引擎。但若是要考虑反作弊,或是手游等场景,可能该方案就不太合身了。

0x09 总结

由于浮点数精度问题,凡是涉及到跨平台同步的时候,浮点数就都会变得不可靠,再叠加上物理引擎的时序等问题,看上去很简单的物理同步就变得异常难处理。Unity似乎也知道有这个问题存在,计划在未来加入跨平台确定性物理引擎的官方支持,不过目前仍然是“在做了”的状态,正式发布遥遥无期。因此,目前我们还是处在需要自力更生的状态,希望这篇文章能够带给大家一些启发和帮助吧,也欢迎大家在评论区提出意见和交流,我们下期再见。

你可能感兴趣的:(https,xml,c#)