Здається, рух залежить від частоти кадрів, незважаючи на використання 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);
}

Якщо я заблокую рамку гри до 60 кадрів в секунду, мої об'єкти рухаються так, як очікувалося. Однак якщо я його розблокую ( Application.targetFrameRate = -1;), деякі об'єкти будуть рухатися значно повільніше, тоді, як я очікував, досягнувши ~ 200 кадрів в секунду на моніторі 144 Гц. Це, здається, відбувається лише в самостійній збірці, а не в редакторі Unity.

GIF переміщення об'єкта в редакторі, розблокований FPS

http://gfycat.com/SmugAn yearFugu

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
То це єдність чи проблема коду, а не математика. Швидка електронна таблиця говорить, що якщо ми використовуємо 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,550. Так 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), і отримав відмов: 5 у кадрі 626 (10.433) секунд проти відмов: 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 повільніше, ніж основний цикл, що відбувається у вашій самостійній збірці. Коли фіксований крок повільний, фіксованийDeltaTime великий.


1
Він викликається з LateUpdate. Я оновлю своє запитання, щоб зробити це зрозумілим. Хоча я вважаю Time.deltaTime, що все-таки використовуватиме правильне значення незалежно від місця його виклику (якщо він використовується у FixedUpdate, він використовуватиме fixDeltaTime).
Купер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.