Wydaje się, że ruch jest zależny od liczby klatek na sekundę, pomimo użycia Time.deltaTime

13

Mam następujący kod do obliczenia tłumaczenia wymaganego do przeniesienia obiektu gry w Unity, który jest wywoływany LateUpdate. Z tego, co rozumiem, moje użycie Time.deltaTimepowinno uniezależnić ostateczną liczbę klatek na sekundę tłumaczenia (pamiętaj, CollisionDetection.Move()że wykonuję raycasty).

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);
}

Jeśli zablokuję klatkę na sekundę w grze do 60 FPS, moje obiekty poruszają się zgodnie z oczekiwaniami. Jednak jeśli go odblokuję ( Application.targetFrameRate = -1;), niektóre obiekty będą poruszać się znacznie wolniej niż oczekiwałbym po osiągnięciu ~ 200 klatek na sekundę na monitorze 144 Hz. Wydaje się, że dzieje się tak tylko w samodzielnej wersji, a nie w edytorze Unity.

GIF ruchu obiektu w edytorze, odblokowane FPS

http://gfycat.com/SmugAnnualFugu

GIF ruchu obiektu w ramach samodzielnej wersji, odblokowany FPS

http://gfycat.com/OldAmpleJuliabutterfly

Bednarz
źródło
2
Powinieneś przeczytać to. Ograniczanie czasu jest tym, czego chcesz, a stałe kroki czasowe! gafferongames.com/game-physics/fix-your-timestep
Alan Wolfe

Odpowiedzi:

30

W symulacjach opartych na ramkach wystąpią błędy, gdy aktualizacje nie zrekompensują nieliniowych szybkości zmian.

Na przykład rozważmy obiekt zaczynający się od wartości położenia i prędkości zerowej, doświadczający stałego przyspieszenia jednego.

Jeśli zastosujemy tę logikę aktualizacji:

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

Możemy spodziewać się tych wyników przy różnych częstotliwościach klatek: wprowadź opis zdjęcia tutaj

Błąd jest spowodowany traktowaniem prędkości końcowej tak, jakby dotyczyła całej klatki. Jest to podobne do prawej sumy Riemanna, a ilość błędów zależy od liczby klatek na sekundę (zilustrowane inną funkcją):

Jak zauważa MichaelS , ten błąd zostanie zmniejszony o połowę, gdy czas trwania ramki zmniejszy się o połowę, i może stać się nieistotny przy wysokich częstotliwościach klatek. Z drugiej strony gry, w których występują gwałtowne wzrosty wydajności lub długotrwałe ramki, mogą powodować nieprzewidywalne zachowanie.


Na szczęście kinematyka pozwala nam dokładnie obliczyć przesunięcie spowodowane przyspieszeniem liniowym:

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

Jeśli zastosujemy tę logikę aktualizacji:

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

Będziemy mieć następujące wyniki:

wprowadź opis zdjęcia tutaj

Kelly Thomas
źródło
2
Jest to przydatna informacja, ale jak faktycznie odnosi się do danego kodu? Po pierwsze, błąd dramatycznie spada wraz ze wzrostem liczby klatek na sekundę, więc różnica między 60 a 200 fps jest znikoma (8 fps w porównaniu z nieskończonością jest już tylko o 12,5% za duże). Po drugie, gdy duszek osiągnie pełną prędkość, największą różnicą jest wyprzedzenie o 0,5 jednostki. Nie powinno to wpływać na rzeczywistą prędkość marszu, jak pokazano w załączonych plikach .gif. Kiedy się odwracają, przyspieszenie jest pozornie natychmiastowe (być może kilka klatek przy 60+ fps, ale nie pełne sekundy).
MichaelS
2
Jest to zatem problem z jednością lub kodem, a nie matematyka. Szybki arkusz kalkulacyjny mówi, że jeśli użyjemy a = 1, vi = 0, di = 0, vmax = 1, powinniśmy osiągnąć vmax przy t = 1, przy d = 0,5. Robiąc to w ponad 5 klatkach (dt = 0,2), d (t = 1) = 0,6. Ponad 50 klatek (dt = 0,02), d (t = 1) = 0,51. Ponad 500 klatek (dt = 0,002), d (t = 1) = 0,501. Zatem 5 fps to 20%, 50 fps to 2%, a 500 fps to 0,2%. Ogólnie błąd jest zbyt wysoki o 100 / fps. 50 fps jest o około 1,8% wyższa niż 500 fps. I to tylko podczas przyspieszania. Gdy prędkość osiągnie maksimum, różnica powinna wynosić zero. Przy a = 100 i vmax = 5 różnica powinna być jeszcze mniejsza.
MichaelS
2
W rzeczywistości użyłem twojego kodu w aplikacji VB.net (symulując dt 1/60 i 1/200) i otrzymałem Bounce: 5 w klatce 626 (10.433) sekund vs. Bounce: 5 w klatce 2081 ( 10.405) sekund . 0.27% więcej czasu przy 60 fps.
MichaelS
2
To twoje podejście „kinematyczne” daje 10% różnicę. Tradycyjnym podejściem jest różnica 0,27%. Właśnie oznaczyłeś je nieprawidłowo. Myślę, że dzieje się tak, ponieważ niepoprawnie włączasz przyspieszenie, gdy prędkość jest maksymalna. Wyższe prędkości klatek dodają mniej błędów na klatkę, więc dają bardziej dokładny wynik. Trzeba if(velocity==vmax||velocity==-vmax){acceleration=0}. Następnie błąd znacznie spada, choć nie jest idealny, ponieważ nie wiemy dokładnie, która część przyspieszenia klatki zakończyła się.
MichaelS
6

To zależy od tego, skąd dzwonisz. Jeśli wywołujesz go z aktualizacji, twój ruch rzeczywiście będzie niezależny od klatek na sekundę, jeśli skalujesz za pomocą Time.deltaTime, ale jeśli dzwonisz z FixedUpdate, musisz skalować za pomocą Time.fixedDeltaTime. Wydaje mi się, że nazywasz swój krok z FixedUpdate, ale skalujesz za pomocą Time.deltaTime, co spowodowałoby zmniejszenie prędkości pozornej, gdy stały krok Unity jest wolniejszy niż główna pętla, co dzieje się w twojej samodzielnej wersji. Gdy ustalony krok jest wolny, parametr fixedDeltaTime jest duży.

Nox
źródło
1
Jest wywoływany z LateUpdate. Zaktualizuję moje pytanie, aby to wyjaśnić. Chociaż wierzę, Time.deltaTimeże nadal użyje poprawnej wartości, niezależnie od tego, gdzie się nazywa (jeśli zostanie użyta w FixedUpdate, użyje fixedDeltaTime).
Cooper