网络物理模拟(三):具有确定性的帧同步(上篇)
大家好,我是格伦·菲德勒。欢迎大家阅读系列教程《网络物理仿真》,这个系列教程的目的是将物理仿真的状态通过网络进行广播。
在之前的文章中,我们讨论了物理仿真需要在网络上进行广播的各种属性。在这篇文章中,我们将使用具有确定性的帧同步技术来将物理仿真通过网络进行传递和广播。
具有确定性的帧同步是一种用来在一台电脑和其他电脑之间进行同步的方法,这种方法发送的是控制仿真状态变化的输入,而不是像其他方法那样发送的是仿真过程中物体的状态变化。这种方法的背后思想是给定一个初始状态,不妨设为S(n),我们通过使用输入信息I(n)来运行仿真就能得到S(n+1)这个状态。然后我们可以通过S(n+1)这个状态和输入信息I(n+1)来运行仿真就能得到S(n+2)这个状态,我们可以一直重复这个过程得到S(n+3)、S(n+4)以及其后的各个状态。这看上去有点像是数学归纳法,我们可以只通过输入信息和之前的仿真状态就能得到后面的仿真状态-而且得到的仿真状态是高度一致,并且也不需要发送任何状态方面的同步。
这个网络模型的主要优点是所需的带宽仅仅用来传递输入信息,而输入信息所占的带宽其实是与仿真中物体的数目是完全无关的。你可以通过网络来对一百万个物体进行物理仿真,它所需的带宽会跟只对一个物体进行物理仿真所需的带宽完全相同。可以很容易的看到物理物体的状态通常是包含位置、方向、线性速度和角速度(如果是未压缩的话,这些状态一共需要52字节,在这里面假设方向使用的是四元数而其他所有的变量都是用vec3来表示),所以当你有大量的物体需要进行物理仿真的时候,这是一个非常具有吸引力的方案。
如果要采用具有确定性的帧同步这个方案来将物理仿真网络化,首先要做的第一件事就是要确保你的仿真具有确定性。在这个上下文中,确定性其实和自由意志之类的没有关系。它只是意味着给定相同的初始条件和相同的一组输入,仿真能够给出完全相同的结果。而且我在这里要着重强调下是完全相同的结果。而不是说的什么在在浮点数容忍度内足够接近。这种精确是精确到比特位的。所以这种精确性使得你可以在每帧的末尾对整个物理状态做一个校验和,不同机器上面同一帧得到的校验和是完全一致的。
https://v.qq.com/txp/iframe/player.html?vid=a1309ogz48x&width=500&height=375&auto=0
从上面的图中可以看到,这里面的仿真几乎是具有确定性的,但是不完全具有确定性。左边的仿真由玩家进行控制,而右边的仿真有完全一致的初始状态,输入信息也和左边完全相同,但是要有2秒钟的延迟。这两个仿真使用相同的间隔时间进行更新(使用相同的间隔时间进行更新也是确保得到完全一致结果的一个必要前提条件),并且在每一帧前对相同的输入信息进行相应。你可以注意到随着仿真的进行,那些一开始很微小的差异是如何一点点被扩大,最后导致两个仿真完全不同步。所以说这个仿真其实不具有确定性。
上面到底发生了什么?
最后会导致两个仿真的结果差的这么大?
这是因为我使用的物理引擎(ODE)在它的内部使用了一个随机数生成器来对约束处理的顺序进行随机化来提高稳定性。这个物理引擎是完全开源的,所以可以看看它的内部实现!不幸的是,由于左边的仿真处理约束的顺序和右边的仿真处理约束的顺序不同,这导致有一些轻微不同的结果。
要在同一台机器上、使用同一个编译好的二进制文件、并且在完全相同的操作系统上运行(这是必要的限制条件么?),还有就是在运行仿真之前通过dSetRandomSeed把随机数的种子设为当前帧的帧数。一旦满足这些条件的话,ODE这个物理引擎能够给出完全相同的结果,并且左边和右边的仿真能够保持高度一致的同步。
https://v.qq.com/txp/iframe/player.html?vid=x1309fe3ngp&width=500&height=375&auto=0
现在让我们针对上面这个情况给出一个警告。即使ODE这个物理引擎能够在相同的机器上得到确定性的结果,但是这并不一定意味着在不同编译器、不同的操作系统甚至不同的机器架构上(比如说在PowerPC架构上和在Intel架构上)它能够得到确定性的结果。事实上,由于浮点数的优化,在程序的debug版本和release版本之间可能都没有办法得到确定性的结果。浮点数的确定性是一个非常复杂的问题,而且这个问题没有银弹(意味着这个问题没有什么简单可行的解决办法)。要了解更多这方面的信息,请参考。
你可能想知道在我们这个示例仿真中输入信息到底是啥,以及我们该如何吧这些输入信息进行网络化。我们这个示例仿真是由键盘输入进行驱动的:方向键会给代表玩家的立方体施加一个力让他进行移动、按下空格键会把代表玩家的立方体提起来并把碰到的立方体四处滚落、按下‘z’键会启动katamari模式。
但是我们该如何对这些输入信息进行网络化呢?
我们需要把整个键盘的状态在网络上进行传输么?
在这些键被按下和释放的时候我们要发送这些事件么?
不,整个键盘的状态不需要在网络上进行传输,我们只需要传输那些会影响仿真的按键。那么被按下和释放的键的事件需要在网络上进行传输么?不,这也不是一个好的策略。我们需要确保的是在仿真第n帧的时候右边的仿真能够应用完全相同的输入信息,所以我们不能仅仅是通过TCP来发送“按键按下”和“按键释放”的事件,因为这些事件到达网络的另外一侧的时间如果早于或者晚于第n帧的时候都会给仿真造成偏差。
相反我们做的事情是用一个结构来表示整个输入信息,并且在左边一侧仿真开始的时候,通过键盘的访问来填充这个结构并把填充好的结构放到一个滑动窗口中,我们在后面可以根据帧号来对这个输入进行访问。
现在我们就可以通过上面的方法来把左边仿真的输入信息发送到右边仿真中去,这样右边的仿真就知道属于第n帧的输入信息到底是怎么样的。举个简单的例子来说,如果你在通过TCP进行发送的话,你可以简单的只发送输入信息而不发送其他的内容,而发送的输入信息的顺序隐含着帧号N。而在网络的另外一侧,你可以读取传送过来的数据包,并且对输入信息进行处理并把输入信息应用到仿真中去。我不推荐这种方法,但我们可以从这里开始,然后再向你展示如何把这种方法变得更好。
在进一步对这个方法进行优化之前,让我们先统一下使用的网络环境,让我们假设下我们是通过TCP进行数据传输,已经禁止了Nagle算法并且每帧都会从左边的仿真向右边的仿真发送一次输入信息(频率是每秒60次)。
这里面有一个问题会变得比较复杂。把左边仿真发生的输入信息通过网络进行传输,然后右边仿真并没有足够的时间来从网络上收到输入信息并利用这些到达的输入信息来模拟仿真,因为这个过程需要一定的时间。你不能按照某个频率在网络上发送信息并且期望它们能够按照完全相同一致的频率到达网络的另外一侧(比如说,每六十分之一秒到达一个数据包)。互联网并不是按照这个方式工作的。根本就没有这样的保证。
如果你想要做到这一点的话,你必须实现一个叫做播放延迟缓冲区的东西。不幸的是,播放延迟缓冲区收到了专利保护,也就是一个专利雷区。我不建议读者在实际使用确定性的帧同步模型的时候搜索“播放延迟缓冲区”或者是“自适应性延迟缓冲区”。但简而言之,你所需要做的事情是缓存收到的数据包一小段时间以便让这些数据包表现的像是以一个稳定的速度到达那样,即使实际上它们的到达时间是充满抖动的。
你现在所做的事情就跟你在看一个视频流的时候,Netflix所做的事情是很类似的。你在最初开始的时候停顿了一下以便你可以拥有一个缓冲区,这样即使一些数据包的到达时间有点晚,但是这种延迟不会对视频帧按正确时间间距的表现有什么影响,视频帧仍然会按照正确的时间间隔一帧帧的播放。当然如果你的缓冲区没有足够大的话,那么这些视频帧的播放可能还是会充满一些抖动。有了确定性的帧同步机制,你的模拟仿真将会以完全相同的方式执行。我建议在播放的时候最好在一开始有100毫秒-250毫秒的延迟。在下面的例子中,我使用的是100毫秒的延迟,这是因为我让延迟最小化来增强响应性。
我的播放延迟缓冲区的实现非常的简单。是将输入信息按照帧序号进行添加,当收到第一个输入信息的时候,它保存了接收方机器上的当前本地时间,并且从那一个时刻起假设所有到达的数据包都会带上100毫秒的延迟。你可能需要一些更加复杂的机制来适应真实世界的情况,比如说可能需要处理时钟漂移、检测在什么时候应该适当的加速或者减慢模拟的速度来让缓冲区的大小在能够保证整体延迟最小的情况下保持在一个适度的情形(这就是所谓的“自适应”),但是这些内容可能会相当的复杂并且可能需要一整篇文章来专门对这些情况进行专门论述。而且如前所述,这些内容还涉及到了专利保护方面的内容,所以这些内容我就不详细展开了,把如何处理这些东西全部托付给你自己实现。
在平均情况下,播放延迟缓冲区给帧n、n+1、n+2以及后续的帧提供了一个稳定的输入信息流,非常完美的以六十分之一秒的间隔依次到达。在最坏的情况下,就是已经该执行第N帧的模拟仿真了,但是这一帧的输入信息还没有到达,那么它就会返回一个空指针,这样整个模拟仿真就必须在那里进行等待了。如果数据包被集中起来发送并且到达接收方的时候已经比预期时间延迟了,这可能会导致多个帧的输入信息同时准备好等待出列进行计算。如果是这种情况的话,我会限制在一个渲染帧的时间最多只能进行4次模拟仿真,这样给模拟仿真一个追上来的机会。如果你把这个值设置的更高的话,那么可能会引起更多其他的问题,比如卡顿,因为你可能需要超过六十分之一秒的时间来运行这些帧(这可能会造成一个非常不好的反馈体验)。总而言之,重要的是确保你的模拟仿真在使用确定性的帧同步这个方案的时候性能不是在中央处理器这一端受限的,否则的话,你在运行更多的模拟帧来追上正常的模拟速度的时候会遇到很多麻烦。
通过使用这种延迟缓冲区的策略以及通过TCP协议来发送输入信息,我们可以很轻松的确保所有的输入信息会有序的到达并且传输是可信赖的。这就是一开始TCP协议在设计的时候希望达到的目标。
实际上,下面这些东西就是互联网的专家常说的一些东西:
如果你需要一个可以信赖的有序的发送信息的方法,你不可能找到一种比通过TCP协议进行传输更好的方法!
你的游戏根本就不会需要UDP协议。
我在这里将告诉你上面这些想法都是大错特错的。
https://v.qq.com/txp/iframe/player.html?vid=x1309fe3ngp&width=500&height=375&auto=0
在上面的讨论中,你可以看到如果网络同步模型使用基于TCP协议的确定性的帧同步模型的话,模拟仿真的网络延迟大概是100毫秒,并且有百分之一的丢包率。如果你仔细看下右边所提供的数据的话,你可以每隔几秒就会出现一些抖动。如果你在两边都出现这种的情况,那么很抱歉这意味着你的电脑的性能对于播放这些视频而言可能有些艰难。如果是这种情况的话,我建议下载这个视频然后离线观看。无论如何,这里所发生的事情是当一个数据包丢失的时候,TCP协议需要等待至少2个往返时延才会重新发送这个数据包(实际上这里面的等待时间可能会更糟,但是我很慷慨的设定了一个非常理想的情况。。。)。所以上面发生的抖动原因是确定性的帧同步模型要求右边的模拟仿真在没有第n帧的输入信息的时候不能执行第N帧的模拟仿真计算,所以整个模拟仿真就停下来等待对应帧的输入信息的到达!
未完待续。。。。。。
近期热文