|
五:物理同步
1.概念与理解
- 什么是物理同步
2.问题与解决方案
- 物理引擎的确定性问题
- 同步误差如何被物理模拟放大
- 物理同步Demo案例介绍
- 《看门狗》的载具同步
- 《火箭联盟》的物理同步策略
网络同步是游戏开发中比较重要且复杂的一项技术,但是由于网上资料参差不齐导致很多朋友对部分概念和原理理解有误。作者耗费近一年的时间,参考了大量的论文与资料(不下100篇),最终整理出来“网络同步在游戏历史中的发展变化”系列文章,希望能够帮到大家(求关注与分享)。上一篇文章我们分析了状态同步的发展历史以及相关优化手段,这篇主要是针对物理同步技术做分析和总结。
前段时间,@知乎韦易笑老师 阐述了有关“帧同步”一词在国内的发展历史也解释清了该名词被乱用的原因。总的来说,国内“帧同步”与国外“Lockstep”的核心思想是一致的,为了方便描述,这篇文章会使用“帧同步”替代“LockStep”。
五、物理同步
1.概念与理解
所谓“物理同步”,字面上讲就是“带有物理状态对象的网络同步”,严格上来说它并不是一个标准的技术名词,而是大家约定俗成的一个概念。按照我的个人理解,可以进一步解释为“在较为复杂的物理模拟环境或有物理引擎参与计算的游戏里,如何对持有物理状态信息的对象做网络同步”。在英文中,我们可以使用Replicate physics simulated objects 或者Networked physics来表示类似的概念。
网络游戏《火箭联盟》
不过,考虑到并不是所有物理现象都交给物理引擎处理,而且有物理引擎参与的网游也并不一定需要对同步做任何处理,所以我们常说的物理同步更多的是指“在网络游戏中,如果玩家的位置或者与玩家交互对象的位置需要经过物理引擎的模拟处理来得到结果,那么其中涉及到网络同步技术就可以称为物理同步”。(这里的物理模拟一般指整个对象完全交给物理引擎去计算碰撞、位置、约束等,很多情况下可以等价为对Ragdoll的模拟)
备注:物理一词涉及的范围非常广,在游戏里面应用的场景也很多,但是并不一定需要进行网络同步,比如简单的抛物线运动,射线检测,与玩法无关的场景破碎等。
早在上世纪70年代,就诞生了许多围绕物理特性产生玩法的游戏,不过由于当时计算机系统算力有限,涉及到的物理计算都非常简单(比如乒乓球游戏中小球的移动模拟[1])。随着计算机性能的飞速提升,开发者们考虑将环境中的所有对象都交由统一的物理模块驱动,由此慢慢的催生出了通用的物理引擎[2]。很快的,各个游戏开发商逐渐将物理引擎集成进来,将更多更复杂的物理模拟过程应用到游戏中,制作出了诸如极品飞车、FIFA、NBA、愤怒的小鸟等围绕物理特性进行玩法设计的游戏。另一方面,随着计算机网络的发展,游戏中的网络同步技术愈加成熟,网络游戏的品质也不断向单机游戏靠拢,我们也得以将传统的单机游戏拓展成多人游戏。物理模拟作为提升游戏趣味性的一大技术也自然逐渐被纳入其中,物理同步变得重要起来。
游戏《Pong》
2.面临的问题与解决方案
正如所前面解释的那样,物理同步并不是一种特殊的同步方式,而是在物理引擎和网络同步技术共同发展的条件下而诞生的一种综合行性解决方案,其核心手段还然是我们熟悉的帧同步或者状态同步。使用帧同步技术我们需要每帧把玩家的Input信息发送出去,然后让另一端的物理引擎根据输入去模拟结果。如果使用状态同步我们则需要本地模拟好数据并把物理位置、旋转等关键信息发送到其他的客户端,然后其他客户端可以根据情况决定是否再执行本地的物理模拟(如果是快照同步,由于拿到的就是最终的结果,那么就不需要本地再进行模拟了)。
这样看来,物理同步好像与常规的同步也没什么本质上的区别,那么为什么他却是一个难题呢?我认为原因有以下两点:
- 物理引擎的不确定性
- 在物理引擎参与模拟的条件下,网络同步的微小误差很容易被迅速放大
首先,我们谈谈物理引擎的确定性问题。很不幸,目前所有的物理引擎严格来说都不是确定性的,因为想保证不同平台、编译器、操作系统、编译版本的指令顺序以及浮点数精度完全一致几乎是不可能的。关于物理确定性的讨论有很多[3],核心问题大致可以归类为以下几点:
- 1.编译器优化后的指令顺序
- 2.约束计算的顺序
- 3.不同版本、不同平台浮点数精度问题[4][5]
(问题1与问题3其实是密切相关的)
这里摘选一段PhysX物理引擎的描述[6]:
The PhysX SDK can be described as offering limited determinism(注:提供了有限程度的确定性). Results can vary between platforms due to differences in hardware maths precision and differences in how the compiler reoders instructions during optimization. This means that behavior can be different between different platforms, different compilers operating on the same platform or between optimized and unoptimized builds using the same compiler on the same platform(注:不同平台、编译器、优化版本都会影响确定性). However, on a given platform, given the exact same sequence of events operating on the exact scene using a consistent time-stepping scheme, PhysX is expected to produce deterministic results. In order to achieve this determinism, the application must recreate the scene in the exact same order each time and insert the actors into a newly-created PxScene. There are several other factors that can affect determinism so if an inconsistent (e.g. variable) time-stepping scheme is used or if the application does not perform the same sequence of API calls on the same frames, the PhysX simulation can diverge.
如果游戏只是单个平台上发行,市面上常见的物理引擎(Havok,PhysX,Bullet)基本上都可以保证结果的一致性。因为我们可以通过使用同一个编译好的二进制文件、在完全相同的操作系统上运行来保证指令顺序并解决浮点数精度问题,同时打开引擎的确定性开关来保证约束的计算顺序(不过会影响性能),这也是很多测试者在使用Unity等商业引擎时发现物理同步可以完美进行的原因。当然,这并不是说我们就完全放弃了跨平台确定性的目标,比如Unity新推出的DOTS架构[7][8]正在尝试解决这个问题(虽然注释里面仍然鲜明的写着“Reserved for future”)。
常见物理引擎
考虑到物理引擎的确定性问题,我们可以得出一个初步的结论——完全使用帧同步做物理同步是不合适的(或者说做跨平台游戏是行不通的)。而对于状态同步,我们可以定时地去纠正位置信息来避免误差被放大。如果一定要使用帧同步去做跨平台同步,那么只能选择放弃物理引擎自己模拟或者用定点数来改造物理引擎,这可能是得不偿失的。
下面不妨先排除掉一致性的问题,来看看如何实现所谓的“物理同步”。实际上,无论是优化手段还是实现方式与前两篇提到的方案是几乎一致的,帧同步、快照同步、状态同步都可以采用,增量压缩、Inputbuffer等优化手段也一样可以用于物理同步的开发中。Network Next的创始人Glenn Fiedler在2014年撰写了一系列的物理同步相关的文章[9],使用一个同步的Demo非常详细地阐述了同步技术是如何应用以及优化的。涉及到的技术点大致如下,涵盖了网络同步的大部分的知识细节:
- 如何确保物理引擎的确定性
- 如何实现物理帧同步
- Inputbuffer如何改善帧同步
- 为什么用UDP替代TCP
- 如何实现快照同步
- 怎样用插值解决网络抖动
- 如何通过快照压缩减少网络流量
- 如何实现增量压缩
- 如何实现状态同步
另外,在2018年的GDC上,Glenn也对物理同步进行一次演讲分享[10],具体的细节建议大家移步到Glenn Fiedler的网站以及GitHub[11]去看。
Glenn Fiedler的物理同步Demo
接下来,我们再来谈谈第二个难点,即网络同步的误差是如何被物理模拟迅速放大的(尤其在多人交互的游戏中)。我们在前面的章节里也谈过,为了保证本地客户端的快速响应,通常会采取预测回滚的机制(Client prediction,即本地客户端立刻相应玩家操作,服务器后续校验决定是否合法)。这样我们就牺牲了事件顺序的严格一致来换取主控端玩家及时响应的体验,在一般角色的非物理移动同步时,预测以及回滚都是相对容易的,延迟比较小的情况位置的误差也可以几乎忽略。然而在物理模拟参与的时候,情况就会变得复杂起来。
主控(Autonomous/Master)以及模拟(Simulate/Replica)都是针对某个对象而言的。
假如有两个客户端,玩家A控制小车1,玩家B控制小车2。小车1在玩家A的客户端上就是主控的,小车2在玩家A的客户端上就是模拟的。同理,小车2在B客户端上就是主控的,小车1在B客户端上就是模拟的。
假如在一个游戏中(带有预测,也就是你本地的对象一定快于远端)你和其他玩家分别控制一个物理模拟的小车朝向对方冲去,他们相互之间可能发生碰撞而彼此影响运动状态,就会面临下面的问题。
1.由于网络同步的误差无法避免,那么你客户端上的发生碰撞的位置一定与其他客户端的不同。
本地客户端的模拟小车(对手小车)一定落后其控制端
2.其次,对于本地上的其他模拟小车,要考虑是否在碰撞时完全开启物理模拟(Ragdoll)。如果不开启物理,那么模拟小车就会完全按照其主控端同步的位置进行移动,即使已经在本地发生了碰撞他可能还是会向前移动。如果开启碰撞,两个客户端的发生碰撞的位置会完全不同。无论是哪种情况,网络同步的误差都会在物理引擎的“加持”下迅速被放大进而导致两端的结果相差甚远。
不开启物理模拟条件下,模拟端不会在碰撞时立刻停下
其实对于一般角色的非物理移动同步,二者只要相撞就会迅速停止移动,即使发生穿透只要做简单的位置“回滚”即可。然而在物理模拟参与的时候,直接作位置回滚的效果会显得非常突兀并出现很强的拉扯感,因为我们几乎没办法在本地准确的预测一个对象的物理模拟路径。如果你仔细阅读了前面Glenn Fiedler的文章(或者上面总结的技术点),你会发现里面并没有提到常见的预测回滚技术,因为他只有一个主控端和一个用于观察结果的模拟端,并不需要回滚。
在2017年的GDC上,来自育碧的技术负责人Matt Delbosc就《看门狗2》中的载具同步进行了演讲[12],详细的分析了多个主控端控制不同对象发生碰撞时应该如何处理。
《看门狗2》的网络模型是基于状态同步的P2P,主控角色预测先行而模拟对象会根据快照(snapshot,即模拟对象在其主控端的真实位置)使用Projective Velocity Blengding做内插值,他们在制作时也面临和上面描述一样的问题。假如两个客户端各控制一个小车撞向对方,由于延迟问题,敌人在本地的位置一定是落后其主控端的。那么就可能发生你开车去撞他时,你本地撞到了他的车尾,而他的客户端什么都没有发生。
收到快照再进行内插值,会有延迟造成两边不一致
所以,首先要做的就是尽量减少不同客户端由于延迟造成的位置偏差,Matt Delbosc引入了一个TimeOffset的概念,根据当前时间与TimeOffset的差值来决定对模拟对象做内插值还是外插值,有了合适的外插值后本地的模拟对象就可以做到尽量靠近敌方的真实位置。
使用外插值减少延迟
而关于碰撞后位置的误差问题,他们采用了Physics Simulation Blending技术,即发生碰撞前开启模拟对象的RigidBody并设置位置权重为1(快照位置的权重为0),然后在碰撞发生后的一小段时间内,不断减小物理模拟的权重增大快照位置的权重使模拟对象的运动状态逐渐趋于与其主控端,最终消除不一致性,腾讯的吃鸡手游就采用了相似的解决方案[13]。
根据碰撞时间调整权重
不过实际上, Matt团队遇到的问题远不止这些,还有诸如如何用插值解决旋转抖动问题,人物与载具相撞时不同步怎么办等等,知乎上有一篇译文可以参考[14]。
可能有些朋友会问,如果我不使用预测回滚技术是不是就没有这个问题呢?答案依然是否定的,假如你在运行一个车辆的中间突然变向,而这个操作被丢包或延迟,只要服务器不暂停整个游戏来等待你的消息,那么你本地的结果依然与其他客户端不同进而产生误差。也就是说除非你使用最最原始的“完全帧同步”(即客户端每次行动都要等到其他客户端的消息全部就绪才行),否则由于网络同步的延迟无法避免,误差也必定会被物理模拟所放大。
同样在2017年,另一款风靡全球的竞技游戏——《火箭联盟》悄然上线,可谓是将物理玩法发挥到了极致。次年,《火箭联盟》的开发者Jared Cone也来到了GDC,分享了他们团队是如何解决物理同步问题的[14]。
《火箭联盟》的核心玩法是“用车踢球”,每个玩家控制一个汽车,通过撞击足球来将其“踢”进敌方的球门。由于是多人竞技游戏,所以一定要有一个权威服务器来避免作弊,最终的结果必须由服务器来决定。相比于《看门狗》,他们遇到的情况明显更复杂,除了不同玩家控制不同的小车,还有一个完全由服务器操控的小球。按照常规的同步方式,本地的主控玩家预测先行,其他角色的数据由服务器同步下发做插值模拟。但是在这样一个延迟敏感且带有物理模拟的竞技游戏中,玩家的Input信息的丢失、本地对象与服务器的位置不统一都会频繁的带来表现不一致的问题,而且FPS中常见的延迟补偿策略并不适合当前的游戏类型(简单来说就是延迟大的玩家会影响其他玩家的体验,具体原因我们在上一篇延迟补偿的章节也有讨论)。
客户端有延迟,导致服务器实际位置不同
为了解决这些问题,Jared Cone团队采用了“InputBuffer”以及“客户端全预测”两个核心方案。InputBuffer,即服务器缓存客户端的Input信息,然后定时的去buffer里面获取(buffer大小可以动态调整),这样可以减少网络延迟和抖动带来的卡顿问题。
Inputbuffer的作用
客户端全预测,即客户端上所有可能产生移动的对象(不仅仅是主控对象)全部会在本地预测先行,这样本地在预测成功时所有对象的位置都是准确的,客户端与服务器的表现也会高度一致,当然预测失败的时候自然会也要处理位置回滚。
采用全预测后的效果
仔细分析这两款游戏,你会发现他们采用都是“状态同步+插值+预测回滚”的基本框架,这也是目前业内上比较合适的物理同步方案。
除了同步问题,物理引擎本身对系统资源(CPU/GPU)的消耗也很大。比如在UE4引擎里面,玩家每一帧的移动都会触发物理引擎的射线检测来判断位置是否合法,一旦场景内的角色数量增多,物理引擎的计算量也会随之增大,进而改变Tick的步长,帧率降低。而帧率降低除了导致卡顿问题外,还会进一步影响到物理模拟,造成更严重的结果不一致、模型穿透等问题,所以我们需要尽量减少不必要的物理模拟并适当简化我们的计算模型。
文/Jerish
来源:游戏开发那些事
原文:https://mp.weixin.qq.com/s/XQwIcOEJUGBuNFDWWd_-aA
|
|