Движение зависит от частоты кадров, несмотря на использование Time.deltaTime

13

У меня есть следующий код для расчета перевода, необходимого для перемещения игрового объекта в Unity, который вызывается LateUpdate. Из того, что я понимаю, мое использование Time.deltaTimeдолжно сделать окончательную частоту кадров перевода независимой (пожалуйста, обратите внимание, CollisionDetection.Move()что я просто выполняю raycast).

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;), некоторые объекты будут двигаться гораздо медленнее, чем я ожидал бы при достижении ~ 200FPS на мониторе 144 Гц. Кажется, это происходит только в автономной сборке, а не в редакторе Unity.

GIF движения объекта в редакторе, разблокированный FPS

http://gfycat.com/SmugAnnualFugu

GIF движения объекта в автономной сборке, разблокированный FPS

http://gfycat.com/OldAmpleJuliabutterfly

бондарь
источник
2
Вы должны прочитать это. Время - это то, что вы хотите, и фиксированные временные шаги! gafferongames.com/game-physics/fix-your-timestep
Алан Вулф

Ответы:

30

Моделирование на основе кадров будет вызывать ошибки, когда обновления не в состоянии компенсировать нелинейные скорости изменения.

Например, рассмотрим объект, начинающийся с нулевых значений положения и скорости, который испытывает постоянное ускорение, равное единице.

Если мы применим эту логику обновления:

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

Мы можем ожидать эти результаты при разных частотах кадров: введите описание изображения здесь

Ошибка вызвана обработкой конечной скорости, как если бы она применялась для всего кадра. Это похоже на сумму Правого Римана, и количество ошибок зависит от частоты кадров (показано на другой функции):

Как 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 и 200 кадрами в секунду незначительна (8 кадров в секунду против бесконечности уже только на 12,5% выше). Во-вторых, когда спрайт работает на полной скорости, самая большая разница в 0,5 единицы впереди. Это не должно влиять на фактическую скорость ходьбы, как показано на прилагаемом .gifs. Когда они поворачиваются, ускорение кажется мгновенным (возможно, несколько кадров при 60+ кадров в секунду, но не полные секунды).
MichaelS
2
Это проблема Unity или кода, а не математическая проблема. В быстрой электронной таблице сказано, что если мы используем a = 1, vi = 0, di = 0, vmax = 1, мы должны нажать vmax при t = 1 с 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 кадр / с - 20%, 50 кадр / с - 2%, а 500 кадр / с - 0,2%. В общем, ошибка слишком высока на 100% в секунду. 50 кадров в секунду примерно на 1,8% выше, чем 500 кадров в секунду. И это только во время ускорения. Как только скорость достигнет максимума, разница должна быть нулевой. При a = 100 и vmax = 5 разница должна быть еще меньше.
MichaelS
2
Фактически, я пошел дальше и использовал ваш код в приложении VB.net (имитируя dt 1/60 и 1/200), и получил Bounce: 5 в кадре 626 (10,433) секунд против Bounce: 5 в кадре 2081 ( 10,405) секунд . На 0,27% больше времени при 60 кадрах в секунду.
MichaelS
2
Ваш "кинематический" подход дает разницу в 10%. Традиционный подход - разница в 0,27%. Вы просто пометили их неправильно. Я думаю, это потому, что вы неправильно включаете ускорение, когда скорость максимальна. Более высокие частоты кадров добавляют меньше ошибок на кадр, поэтому дают более точный результат. Вам нужно if(velocity==vmax||velocity==-vmax){acceleration=0}. Затем ошибка существенно снижается, хотя она и не идеальна, поскольку мы точно не выясним, какая часть ускорения кадра закончилась.
MichaelS
6

Это зависит от того, откуда вы звоните. Если вы вызываете его из Update, ваше движение действительно будет независимым от частоты кадров, если вы масштабируете с помощью Time.deltaTime, но если вы вызываете его из FixedUpdate, вам нужно масштабировать с Time.fixedDeltaTime. Я полагаю, что вы вызываете свой шаг из FixedUpdate, но масштабируете его с помощью Time.deltaTime, что приведет к снижению видимой скорости, когда фиксированный шаг Unity медленнее, чем основной цикл, что и происходит в вашей автономной сборке. Когда фиксированный шаг медленный, fixedDeltaTime велик.

Nox
источник
1
Он вызывается из LateUpdate. Я уточню свой вопрос, чтобы прояснить это. Хотя я считаю, Time.deltaTimeчто все равно будет использовать правильное значение независимо от того, где оно вызывается (если используется в FixedUpdate, он будет использовать fixedDeltaTime).
Купер