text: 一文详解UE4物理同步
  在网络游戏中，如果场景中某个可以与玩家所操作的对象发生交互的物体需要借助物理引擎的模拟来得到正确的位置和朝向，那么需要确保在不同客户端上的模拟结果一致。如果可以保证物理引擎的每一次模拟结果是确定的，那么仅需要同步每次玩家操作，就能保证不同客户端的模拟结果一致。但大部分物理引擎（如UE4使用的PhysX物理引擎）都是不确定性物理引擎，其模拟结果受不同平台浮点数精度不同等问题影响，在不同客户端上的物理模拟结果常常不一致。之所以大部分物理引擎都是不确定性物理引擎，是因为确定性物理引擎有以下劣势：1. 需要使用定点数来代替浮点数进行物理运算，且需要确保每个客户端用于随机运算的随机数种子一致；2. 定点数的运算消耗大于浮点数，且能表示的数值范围远小于浮点数；3. 所有与物理相关的数据都需要用定点数表示（如碰撞体数据、地形数据等），需要编写专门的数据转换工具；如果仅仅让不同客户端分别独立进行物理模拟，而不进行物理同步。那么在网络延迟的作用下，不同客户端的状态差异会让物理模拟上的细微差异迅速放大，从而让不同客户端失去同步。（1）主控端的预表现会让玩家所控制的角色状态先于服务器；（2）如果不进行物理同步，那么主控端上玩家控制的角色踢到了球，但服务器的球则会因为没有接触玩家所控制的角色而继续向前运动，从而导致不同步的问题；使用非确定性物理引擎的网络游戏没办法使用帧同步作为网络同步方案，因此这里主要讨论状态同步下的物理同步策略。状态同步的思路是客户端将输入上传到服务器后，由服务器计算出结果，再将发生变化的状态广播给所有客户端，让客户端在本地更新渲染数据。如果按照这个思路，服务器将物理状态同步给客户端，而客户端只负责渲染本地数据，会有以下几个问题：1. 为了节省带宽会降低服务器的同步频率（每秒10帧左右），导致物体移动很不平滑（帧率过低）；2. 玩家的操作需要等待一个RTT（往返时延）之后才会在客户端上生效，再加上网络的不稳定性（丢包或者卡顿），输入延迟较大；如何让客户端上的物体能及时、平滑地正确运动，是物理同步需要解决的问题。内插值是一种常用的物理同步方案，其基本思路是仅同步物体的位置和朝向，在收到的两个数据包间通过线性插值（Lerp）插入过渡数据，从而让物体移动的视觉效果更加平滑。虽然内插值解决了物体移动不平滑的问题，但解决不了输入延迟的问题，且会引入新的问题：1. 在收到数据包时不能立刻应用，必须等下一个数据包到来才可以开始插值，延迟增加；2. 如果网络延迟比较高，那么会停住直到下一个数据包到来；3. 在运动状态发生剧烈变化时，会丢失部分的运动状态；外插值的基本思路是，通过额外同步线速度、角速度、加速度和网络延迟等数据，在收到数据包时直接推测出下一个数据包的状态，从而降低延迟。与内插值相比，外插值因为不需要等下一个数据包，所以会少一个RTT的延迟。但因为外插值需要等服务器发回来的数据包，所以并不能解决输入延迟的问题。此外，外插值会根据当前的运动状态预测出下一个数据包的状态，所以即便运动状态有变化，也可以很好地预测。但如果发生了碰撞，那么简单的预测无法模拟出正确的物理结果，此时则需要让客户端也进行物理模拟。当然，因为物理引擎的不确定性，即便让客户端也进行物理模拟，其最后的模拟结果也不一定正确。再加上在网络情况不稳定、运动方向频繁改变等情况下，外插值的预测结果非常容易出错，由此引入了新的问题：1. 如何判断外插值的预测结果是否正确；2. 如何从错误的预测结果平滑过渡到正确的运动状态；导航推测算法（Dead Reckoning Algorithm，简称DR）是一个利用物体位置及速度推定未来位置方向的航海技术，后被用于在网络游戏中预测物体未来的位置并计算物体的运动轨迹。Murphy在Believable Dead Reckoning for Networked Games（https://www.researchgate.net/publication/293809946_Believable_Dead_Reckoning_for_Networked_Games）一文中提出了一个可以用于预测物体未来位置的公式。 假设服务器告诉客户端，物体当前正处于位置P&#39;0，并以V&#39;0的速度和A&#39;0的加速度向前运动。那么可以预测时间T之后，服务器上的物体将会处于位置Qt。当然，如果服务器在通知客户端之后立即转向，那么客户端将会得到一个错误的预测结果。 P0是客户端的预测结果，而P&#39;0是物体在服务器上的位置。针对这种预测错误的情况，最简单的处理方法是直接将物体在客户端的位置切换到P&#39;0，然后开始新一轮的预测。显然，这样做的效果非常不好，让物体沿着平滑的运动轨迹逐渐切换到正确的运动状态是更加理想的做法。在预测的点P0到实际的点P&#39;0之间计算出一条曲线有很多种方法，比如三次贝塞尔曲线（Cubic Bezier Curve）。但出于CPU开销、曲线平滑度等因素的考虑，Murphy提出了一种名为Projective Velocity Blending的算法，它使用简单的线性插值去计算物体的运动轨迹，并能很好地消除速度变化对轨迹的影响。 整个算法的思路非常简单，那就是接受客户端预测失败的事实，然后想办法让客户端在每次预测的时候尽量多靠近服务器真实状态一点，从而确保客户端沿着一个平滑的轨迹不断修正预测结果。（1）先用线性插值纠正客户端的速度，得到速度Vb；（2）然后利用前面提到的公式，分别计算出T秒后客户端的位置Pt和服务器的位置P&#39;t；（3）最后再使用线性插值，进一步纠正客户端的位置，算出一个更接近服务器预测位置的Qt。尽管Projective Velocity Blending算法可以很好地纠正大部分预测错误的情况，但在预测结果偏差很大时，客户端的运动轨迹与服务器的运动轨迹的差异也会很大，其纠正所需的时间也会越长。期间出现碰撞等问题导致客户端的运动状态与服务器的运动状态差异变得更大的可能性也就越高。 红色线表示物体在客户端上的运动轨迹，而绿色线则表示物体在服务器的运动轨迹。客户端在沿着红色虚线这一计算出来的轨迹运动时，与蓝色物体发生了碰撞，导致与服务器的运动状态差异更大。因此，在差异过大时（尤其是地形比较复杂，障碍物比较多的场景），直接强行将客户端纠正到服务器下发的最新运动状态是很有必要的。一般来讲，可以通过以下几个指标来衡量客户端与服务器的差异程度；1. 位置距离大小；2. 朝向夹角大小；3. 速度方向的夹角大小；然后通过设定阈值的方式，在某一指标差异过大时，直接纠正客户端的运动状态（位置、朝向和速度等），再进行下一次的预测。UE4是在其网络同步框架的基础上实现物理同步，其主要流程如下所示。其中，服务器在每一帧的物理模拟结束之后，将物体的物理数据存放进类型为FRepMovement的成员变量ReplicatedMovement，并由UE4的网络同步框架将其同步至客户端。在被同步至客户端之后，ReplicatedMovement的数据会在AActor::PostNetReceivePhysicState函数里被拷贝至一个类型为FRigidBodyState的变量，最后将其添加到FPhysicsReplication的ComponentsToTargets队列中，并准备执行物理同步逻辑。 FRepMovement用于同步物体的物理数据，而FRigidBodyState则用于记录物体的物理状态。考虑到float的表示范围在大部分游戏场景下都是大大溢出的，可以在传输时对其进行压缩，减少数据量。但是float遵循IEEE754标准，在网络上传输float无法使用自适应Bit流来减小数据量，导致直接传输FVector就必须用12个Byte，有些浪费。因此，UE4在FRepMovement中定义了针对Location、Velocity和Roation这三个变量的压缩等级，在将其序列化前根据压缩等级对FVector进行有损压缩。至于FRigidBodyState中使用的FVector_NetQuantize100则是封装了压缩操作的数据结构。物理同步逻辑执行完之后，客户端再以同步后的物理状态继续物理模拟，从而得到同步之后最新的物理结果。UE4在FPhysicsReplication::ApplyRigidBodyState方法里使用导航预测算法实现了物理同步逻辑，这里主要讨论UE4是如何应用导航预测算法的。3.2.1 预测逻辑从2.3节可以知道，外插值可以很好地解决物理同步中的延迟问题。因此，在收到服务器同步的数据包之后，UE4首先通过外插值的方式，推测出此时物体在服务器上的物理状态。下图更加直观地展示了预测的过程。在知道服务器同步数据包时的物理状态和数据包的传输延迟的前提下，利用导航预测算法给出的公式，就能很好的预测在同一时刻下，物体在服务器上的物理状态。1. 圆形表示物理同步的目标；2. 三角形表示场景中的静态物体，用于体现圆形物体的运动状态变化；4. 虚线方框表示某个时刻下的场景状态；接下来，只需要消除客户端的实际物理状态与服务器的预测状态之间的差异，就可以消除网络延迟的影响，实现物理同步。3.2.2 纠错逻辑为了让客户端的运动轨迹更加平滑自然，UE4采用了一种名为错误累积（Error Accumulation）的纠错机制。首先计算客户端的实际物理状态与服务器的预测状态之间的距离差和夹角大小，若这两个数值的加权和小于预设的阈值，说明此时客户端与服务器是完全同步的，无须执行任何纠正逻辑，并通过设置bRestoredState为true来表示该物体在收到新的数据包之前无需再进行物理同步逻辑。 如果大于阈值，则会执行纠错逻辑。如果两者距离差不是很大，那么会尝试通过导航预测算法里的Projective Velocity Blending让客户端以平滑的曲线不断接近服务器的预测状态，直到这两者差距足够小，不需要再进行物理同步为止。虽然这里和2.5节里给出的计算方式不完全一样，但仔细推导，就会发现思路其实很像；1. 不难知道TargetPos是预测的位置P&#39;t，而NewLinVel则是修正后的速度Vb；2. 使用TargetPos(P&#39;t)和CurrentState.Position(P0)插值得到NewPos用于纠正物体的位置；3. 在网络同步处理完之后，使用修正后的速度Vb进行物理模拟，这中间的位移等效于(Pt - P0)；4. 因此，最终物体的位置Qt = P0 + (P&#39;t - P0) ΔT + (Pt - P0) = Pt + (P&#39;t - P0) ΔT。为了判断本次纠正是否有效，会计算PrevProgress和PrevSimilarity这两个指标并分别与设置的阈值比较。如下图所示，PrevProgress表示上一次修正的有效程度，而PrevSimilarity则表示上一次修正之后，客户端实际位置与当前预测位置的差异大小。如果纠正有效，客户端沿着正确的轨迹朝预测位置不断靠近：1. 在客户端每帧运动的距离不变的前提下（蓝色线长度不变），蓝色线与绿色线会不断趋于平行，也就是PrevProgress的值会不断变大；2. 客户端与预测位置的距离差会不断变小，也就是红色线的长度会不断变小，也就是PrevSimilarity会不断变小；值得注意的是，PrevSimilarity过大有可能不是纠正无效，而是收到了新的数据包，预测位置发生了变化。因此需要同时结合PrevProgress来判断客户端是否确实在沿着正确的轨迹不断靠近。确定了如果判断纠正是否有效的方法之后，再来考虑2.5节提到的问题：如果在纠正过程中，因为碰撞或者其他原因，导致客户端实际位置与预测位置的状态差异更大了，该如何处理？如果在发生错误时立刻将其拉扯到预测位置，那么表现效果会很差，卡顿感会很明显。但如果长时间不干涉，差异则有可能越来越大，直到失去同步。对此，UE4给出的结果方案是，每次在发现纠正逻辑无法让客户端靠近预测位置时，先暂时相信客户端的纠错能力，只累积错误时间而不做其他处理，然后继续纠正。直到错误时间累积足够久（在一定时间内客户端始终无法完成纠错），或者两者的距离差距足够大时，才触发Hard Snap操作，直接将客户端拉扯到服务器的预测状态上，从而一定程度上减少物理同步时的卡顿感。 触发HardSnap操作之后，客户端的位置显然与预测位置是同步的，后面自然不需要再执行物理同步逻辑。故这里直接将bRestoredState设置为true，并清空累积的错误时间。3.2.3 参数配置为了在客户端有更好的表现，UE4允许允许根据项目的实际情况，配置错误阈值等各种参数。如下图所示，在Project Setting里，可以配置物理同步的各个参数，以获得不同的物理同步效果。 虽然到这里，UE4物理同步的大致逻辑已被梳理清楚，但仍有一个十分重要的点还没讨论到，那就是如何处理物体角速度和朝向的同步？UE4的处理逻辑十分简单，依旧是通过&#34;预测+插值平滑&#34;的方式来出处理物体的朝向同步逻辑。尽管这个方法在处理物体的位置同步上效果非常好，但在处理物体的朝向同步时，表现则比较差。如下图所示，相同的方法，可以计算出一条平滑的位置同步曲线，但只能得到一条来回波动的朝向同步折线。 这个例子也许不是十分恰当，但能比较简单、直观地说明在处理物体的朝向同步时出现频繁抖动的原因。如果将每个帧的运动轨迹叠加起来，那么可以得到一个锯齿状的朝向同步折线。也就是说，物体在持续改变方向时，抖动感会非常明显。造成这一现象的主要原因，就是因为朝向同步的要求比位置同步的要求更为苛刻。位置同步只需要确保最终的轨迹平滑就可以了，而朝向同步则是在平滑的基础上尽可能避免来回波动。在位置上来回波动几厘米肉眼可能感知到不到，但如果在朝向上来回波动几度，那么玩家则可以明显感知到物体的抖动，尤其是玩家当前正在操作、有摄像机跟随的物体。修复这一问题的方法有很多，其中最简单的一个方法是通过修改参数减小朝向的预测幅度，从而尽可能减少朝向在修正过程中来回波动的情况。 在Project Setting里，将物理同步参数里的Angle Lerp从默认的0.4改成0.2，可以看到物体的抖动幅度明显降低。总体来说，UE4的物理同步逻辑能很好地满足大部分使用场景的需求，但并没有做到完美。玩家在操作高速运动、且频繁改变运动方向的物体时（如驾驶载具时），UE4的物理同步逻辑会导致物体持续抖动。尽管通过调整物理同步的参数可以缓解这一问题，但始终是治标不治本。最佳的方案是根据游戏的实际需求自定义物理同步逻辑。&gt;&gt;&gt;参考文章1. 浅谈物理引擎的网络同步方案（https://zhuanlan.zhihu.com/p/341447703）2. Believable Dead Reckoning for Networked Games（https://zhuanlan.zhihu.com/p/341447703）3. UE4移动的网络同步，&#34;关于FVector_NetQuantize一节&#34;（https://zhuanlan.zhihu.com/p/341447703）