理解Unity的Timesteps(步长)和实现平滑移动
首先不知道有没有像我一样,一直不是很清楚Timesteps具体怎么解释的,稍微找了一下:
上面说的是两个物理检测帧之间的时间间隔
如果不是物理的话,可以直接理解为,两帧之间的时间间隔
-----
接下来开始翻译原文
原文:http://www.kinematicsoup.com/news/2016/8/9/rrypp5tkubynjwxhxjzd42s3o034o8
在Unity社区里,其中有一个辩论的最为激励的话题,就是何如去除游戏中生涩的动作,让它显得自然。这个问题不只是在Unity引擎才有,所有引擎都有这个问题,而且他的产生的原因来自于你的引擎用怎么样的timesteps。
并没有哪种解决方案能解决所有的这些情况,不过有一种直接的方法解决问题。许多开发者遇到这个问题是在移动的过程中,发现物体一抖一抖的,而且想要改善很难。令人惊讶的是,外面关于unity的timesteps有很多误传,许多unity论坛的回答者,即使他说对了,也没有综合的分析,并且留下了许多理解上的空白让人无法完全解决这些问题。本文旨在在更深层次的解释unity的timesteps来解决这个问题,解释为什么会产生抖动,并提供一个解决方案来解决这个问题。并且发布了一个AssetPackage来演示这个解决方案
额!这种类型的抖动是不是很熟悉?
上面的这个图片就是一种简单的运动抖动。很明显这并不是我们想要出现在游戏里的画面。如果你要重现这个非常简单:
创建一个新project
导入一个默认的第一人称角色控制器,放在一个新场景
放置一些物件,并选择一个朝着它画圆
删除掉头的摆动,放置一个游戏平板在地面使得观察效果更佳明显。一般情况下,你能注意到,当你绕着它转时,这里会有特别明显的抖动。
现在看看这种情况,当我使用了稍后会讨论的这些技术。和第一个例子对比,你能明显看到在顺滑上的重要的变化。
如丝般顺滑,好多了!
在解决这个问题之前,先去理解Unity(Mono)的生命周期是很重要的。特别是,我们必须去探索Update和FixedUpdate背后的逻辑。下面的链接是一遍来自Unity官方文档的,关于Unity生命周期的小的摘录:
https://docs.unity3d.com/Manual/ExecutionOrder.html
特别提醒一下,如果你以前没有注意过Update的执行顺序,那么你需要特别的研究一下,因为接下来的部分与这个密切相关。
Unity的各个Update的执行顺序,原文:https://docs.unity3d.com/Manual/ExecutionOrder.html
先了解这个流程图(生命周期)。
这个流程图概括了Unity的各个Update执行顺序中,一帧里面哪些函数会被调用。这部分我们需要注意的是FixedUpdate和Update函数,图上绿色和红色的部分。
Unity实现的是半固定的timestep。这意味着主游戏循环用一个可变的timestep跑在任何帧率上,这个在Unity里叫做deltaTime。并且用来控制一个使用固定timesteps的内部的循环。
这里有一些好处:
首先,在游戏中物理在固定帧率的检测下,可以进行游戏中画面上的高帧率的Update,只要硬件允许。
相反的,这里也有一些坏处,容易出现上面所说的抖动。
下面是简单的用代码来解释一下Unity的Update循环的结构:
float currentSimulationTime = 0;float lastUpdateTime = 0;while (!quit) // variable steps{ while (currentSimulationTime < Time.time) // fixed steps { FixedUpdate(); Physics.SimulationStep(currentState, Time.fixedDeltaTime); currentSimulationTime += Time.fixedDeltaTime; } Time.deltaTime = Time.time - lastUpdateTime; Update(); Render(); lastUpdateTime = Time.time;}
这个Update函数对你来说应该很熟悉了,它在Monobehaviours的每帧里,在输入已经执行后,在渲染前的中间这段时间里被调用。
如果垂直同步关闭了的话,当一帧结束以后,Unity会立刻执行下一帧,这样子去获得尽可能高的帧率。
当每帧的硬件和计算量在不停的变化时,帧率也会不停的变化,即使垂直同步打开了,由于Unity尝试去让每个帧率尽量相同,也不会真的固定不变。
由于以上这些,Update函数能在一秒内被调用任意次数。
FixedUpdate在每次Monobehaviors的物理检测中进行。即使多个物理检测中,他们的检测的时间间隔不是相同的时间,Unity也会把他们放到固定的时间间隔内调用。 这是因为在游戏的物理里,尤其是加速运动中,使用固定的时间差,是最准确和最稳定的。在Unity里,这个固定的时间差就叫做fixedDeltaTime。默认的时候,fixedDeltaTime的值为0.02,这意味着在游戏里每秒钟总有50次FixedUpdate的调用。用这种方法,你可以理解FixedUpdate是一个独立的帧率,它在每秒钟被调用的次数是固定的,即使这时你的渲染帧率非常低或者非常高。重点的提出FixedUpdate和物理的循环是相关关联的很有必要的,不是发现在不同线程的。
基于这点,一起来观察一下几个关于更新时机的例子。
上面是当每秒有50个FixedUpdate和60个Update的时候的更新时机。可是,在实际中由于帧率是不固定所以不会这么完美,下面这个图这个就是现实中的,一点点夸大的时间轴。注意到,有一些Update之间是没有FixedUpdate的,这种情况一般出现在渲染简单画面的时候。另一方面一些Update之间有还几次FixedUpdate,这个一般出现在加载资源的过程中。这意味着,即使你尽可能的让Update和FixedUpdate次数相同,这也很难做到对齐。
根据我们对Unity的timesteps的知识,我们可以理解下面的这个案例。这个场景里,一个球和照相机绕着同一个支点。这个球的transform的改变放在Update循环里,与此同时,左边照相机的transform改变放在FixedUpdate循环里,右边的照相机放在Update循环里。左边这个看起来就有很明显的抖动,右边的就很顺滑
左边的照相机在FixedUpdate里移动,右边的在Update里
由于Update和FixedUpdate的调用不在通过频率上,当球在移动的时候照相机依然还在原地。这就导致了球相对于照相机移动的不同步,产生了抖动。当在慢镜头下,这些行为就更为明显。
和上面一样的模型,5%的速度
所以,我们能看到导致抖动的缘由是因为移动不同的objects的时候,有的在Update里有的在FixedUpdate里。所以简单的修复方法就是必须把所有移动transform的地方,要不放在Update里,要不放在FixedUpdate里。
然而,这时事情开始变得棘手了,这个常见的回答会使很多Unity开发者把游戏中的许多游戏内的运动放在Update里,而FixedUpdate只放小部分物理逻辑。虽然这有它的优点,比如简化了你的操作,但是由于很多原因,它是有问题的。也许最大的问题是让你的游戏依靠画面的帧率进行。这就导致了有很多bug和连贯性的行为不一致的问题,打开了这些错误大门。此外,它会影响很多决策,包括几乎所有的实时策略类型。当你需要执行加速度运动时,比如玩家的重力,这也会出现问题。像这些物体就应该使用FixedUpdate,但由于其他的物体使用了Update,所以你会看到很多抖动。(请参考标准assets里面的第一人称控制器来管着这个问题)
因此,一个常见的且有时是必要的选择是,把所有的状态和游戏逻辑放在固定的timestep里像FixedUpdate,严格的把画面和输入信息放在Update里。
然而,这里面不是只有它自己产生的问题,首先,你可能希望你游戏的物理帧也不同于游戏逻辑帧的节奏出现。这个操作非常简单,Unity给了你很多操作的空间,允许你自己去选择和优化。接着,输入信息只在Update里也会有问题,当两个FixedUpdate直接出现了多个Update时,只有最后那一个Update里的输入信息会影响到后面的FixedUpdate。这个特然容易出现在上下按钮事件,因为它们支队单一帧有用。这个问题的解决方案是,在下一个FixedUpdate前,把输入信息用一个input buffer进行缓冲。将此行为集成到您使用的任何输入控制器中是一种相当无缝的方式来执行此操作,并将缓冲保留在一个位置。
然而,一个更大的问题是,一般来说FixedUpdate的调用会比Update少,所以移动物件的频率跟不上画面渲染的频率,导致虽然画面是流畅的,但是移动的时候断断续续的。
有很多种方法可以借鉴这种问题,比如用差值和推测的方法解决,使得可以平滑的移动。差值,可以很方便的流畅的使物体从一个状态移动到另一个状态,很容易使用。不过这会导致有一个fixedDeltaTime的延时。这个延时在大部分游戏内的一般情况下,是能被接受的,即使是射击游戏的射手也可以使用这种方法获得平滑的移动。推测,是预测物体的下个状态会在哪个位置,避免了延时,但是本质是更难做到无缝的滑动并且还带来了性能的压力。
摄像头和球都在FixedUpdate中移动,右边使用了差值计算
以上是演示差值的另一个比较案例。左边这个图,镜头和球的transform设置都在FixedUpdate里。右边也是,不过右边在FixedUpdate的两次调用之间用了差值计算是的移动的时候更顺滑。注意到两边的物件都基本保持了一致性,不过右边的移动更流畅更少抖动。
所以,怎么在Unity用差值计算呢?我做了一个assetPackage,链接在下面:
http://ksblogcontent.s3-website-us-east-1.amazonaws.com/TimeStepBlog/Timesteps.unitypackage
此外,您可以获取用于创建上述示例的构建:
http://ksblogcontent.s3-website-us-east-1.amazonaws.com/TimeStepBlog/TimestepsBuild.zip
具体就看上面的demo了,后面懒得翻了。。。