尽管使用了Time.deltaTime,但移动似乎与帧速率有关。


13

我有以下代码来计算在Unity中移动游戏对象所需的转换LateUpdate。据我了解,我对的使用Time.deltaTime应使最终的翻译帧速率独立(请注意CollisionDetection.Move(),仅用于进行射线广播)。

public IMovementModel Move(IMovementModel model) {    
    this.model = model;

    targetSpeed = (model.HorizontalInput + model.VerticalInput) * model.Speed;

    model.CurrentSpeed = accelerateSpeed(model.CurrentSpeed, targetSpeed,
        model.Accel);

    if (model.IsJumping) {
        model.AmountToMove = new Vector3(model.AmountToMove.x,
            model.AmountToMove.y);
    } else if (CollisionDetection.OnGround) {
        model.AmountToMove = new Vector3(model.AmountToMove.x, 0);
    }

    model.FlipAnim = flipAnimation(targetSpeed);
    // If we're ignoring gravity, then just use the vertical input.
    // if it's 0, then we'll just float.
    gravity = model.IgnoreGravity ? model.VerticalInput : 40f;

    model.AmountToMove = new Vector3(model.CurrentSpeed, model.AmountToMove.y - gravity * Time.deltaTime);

    model.FinalTransform =
        CollisionDetection.Move(model.AmountToMove * Time.deltaTime,
            model.BoxCollider.gameObject, model.IgnorePlayerLayer);
    // Prevent the entity from moving too fast on the y-axis.
    model.FinalTransform = new Vector3(model.FinalTransform.x,
        Mathf.Clamp(model.FinalTransform.y, -1.0f, 1.0f),
        model.FinalTransform.z);

    return model;
}

private float accelerateSpeed(float currSpeed, float target, float accel) {
    if (currSpeed == target) {
        return currSpeed;
    }
    // Must currSpeed be increased or decreased to get closer to target
    float dir = Mathf.Sign(target - currSpeed);
    currSpeed += accel * Time.deltaTime * dir;
    // If currSpeed has now passed Target then return Target, otherwise return currSpeed
    return (dir == Mathf.Sign(target - currSpeed)) ? currSpeed : target;
}

private void OnMovementCalculated(IMovementModel model) {
    transform.Translate(model.FinalTransform);
}

如果我将游戏的帧速率锁定为60FPS,我的对象将按预期移动。但是,如果我将其解锁(Application.targetFrameRate = -1;),则某些对象的移动速度将比在144hz显示器上达到〜200FPS时的速度慢得多。这似乎仅在独立版本中发生,而不在Unity编辑器中发生。

编辑器内对象移动的GIF,解锁FPS

http://gfycat.com/SmugAnnualFugu

独立构建内对象移动的GIF,解锁FPS

http://gfycat.com/OldAmpleJuliabutterfly


2
您应该阅读一下。您需要时间分段和固定时间步长! gafferongames.com/game-physics/fix-your-timestep
艾伦·沃尔夫

Answers:


30

当更新无法补偿非线性变化率时,基于帧的模拟将遇到错误。

例如,考虑一个对象,该对象以零的位置和速度值开始,并经历一个恒定的加速度。

如果我们应用此更新逻辑:

velocity += acceleration * elapsedTime
position += velocity * elapsedTime

我们可以在不同的帧频下获得这些结果: 在此处输入图片说明

该错误是由于将最终速度视为适用于整个帧而引起的。这类似于Right Riemann Sum,并且误差量随帧速率而变化(在不同的功能上进行了说明):

正如MichaelS指出的那样,当帧持续时间减半时,此错误将减半,并且在高帧频下可能变得无关紧要。另一方面,任何遇到性能峰值或长时间运行的游戏都可能会产生不可预测的行为。


幸运的是,运动学使我们能够准确计算线性加速度引起的位移:

d =  vᵢ*t + (a*t²)/2

where:
  d  = displacement
  v = initial velocity
  a  = acceleration
  t  = elapsed time

breakdown:
  vᵢ*t     = movement due to the initial velocity
  (a*t²)/2 = change in movement due to acceleration throughout the frame

因此,如果我们应用此更新逻辑:

position += (velocity * elapsedTime) + (acceleration * elapsedTime * elapsedTime / 2)
velocity += acceleration * elapsedTime

我们将得到以下结果:

在此处输入图片说明


2
这是有用的信息,但是它实际上如何解决有问题的代码?首先,误差随着帧速率的增加而急剧下降,因此60 fps和200 fps之间的差异可以忽略不计(8 fps vs无限远已经太高了12.5%)。其次,一旦精灵全速前进,最大的区别就是提前0.5个单位。如所附的.gifs所示,它不应影响实际的步行速度。当他们转身时,加速似乎是瞬间的(可能以60+ fps的速度拍摄了几帧,但不是整秒)。
MichaelS

2
那是一个Unity或代码问题,而不是数学问题。快速的电子表格显示,如果我们使用a = 1,vi = 0,di = 0,vmax = 1,则应该在t = 1处达到vmax,而d = 0.5。这样做超过5帧(dt = 0.2),d(t = 1)= 0.6。超过50帧(dt = 0.02),d(t = 1)= 0.51。超过500帧(dt = 0.002),d(t = 1)= 0.501。因此5 fps高20%,50 fps高2%,500 fps高0.2%。通常,错误是100 / fps百分比过高。50 fps比500 fps高约1.8%。那只是在加速过程中。一旦速度达到最大值,差应为零。在a = 100和vmax = 5的情况下,差异应该更少。
MichaelS 2016年

2
实际上,我继续在VB.net应用程序中使用了您的代码(模拟dt为1/60和1/200),并在第626帧(10.433)秒处获得了跳动:5,在第2081 帧中获得了跳动:5 ( 10.405)秒。60 fps时多出0.27%的时间。
MichaelS 2016年

2
是您的“运动”方法产生了10%的差异。传统方法是0.27%的差异。您只是错误地标记了它们。我认为这是因为当速度达到最大值时,您不正确地包含了加速度。较高的帧速率会增加每帧错误,从而提供更准确的结果。你需要if(velocity==vmax||velocity==-vmax){acceleration=0}。然后误差会大幅下降,尽管它并不是完美的,因为我们无法确切地知道帧加速的哪一部分结束了。
MichaelS 2016年

6

这取决于您从何处呼叫。如果从Update调用它,则使用Time.deltaTime进行缩放确实可以独立于帧速率,但是如果从FixedUpdate进行调用,则需要使用Time.fixedDeltaTime进行缩放。我认为您是从FixedUpdate调用您的步骤,但是使用Time.deltaTime进行缩放,当Unity的固定步骤比主循环慢时,这将导致视在速度降低,这就是您的独立构建中发生的情况。当固定步长很慢时,fixedDeltaTime很大。


1
从LateUpdate调用它。我将更新我的问题以使其清楚。尽管我相信Time.deltaTime无论在何处调用它都将使用正确的值(如果在FixedUpdate中使用,它将使用fixedDeltaTime)。
库珀
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.