Il movimento sembra dipendere dalla frequenza dei fotogrammi, nonostante l'uso di Time.deltaTime


13

Ho il seguente codice per calcolare la traduzione richiesta per spostare un oggetto di gioco in Unity, che viene chiamato LateUpdate. Da quanto ho capito, il mio uso di Time.deltaTimedovrebbe rendere indipendente la frequenza di fotogrammi della traduzione finale (si prega di notare che CollisionDetection.Move()sta solo eseguendo i 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);
}

Se blocco il framerate del gioco a 60FPS, i miei oggetti si muovono come previsto. Tuttavia, se lo sblocco ( Application.targetFrameRate = -1;), alcuni oggetti si sposteranno a un ritmo molto più lento di quanto mi aspetterei quando si raggiungono ~ 200FPS su un monitor da 144Hz. Questo sembra accadere solo in una build autonoma e non all'interno dell'editor Unity.

GIF del movimento degli oggetti all'interno dell'editor, FPS sbloccato

http://gfycat.com/SmugAnnualFugu

GIF di movimento degli oggetti all'interno della build autonoma, FPS sbloccato

http://gfycat.com/OldAmpleJuliabutterfly


2
Dovresti dare una lettura. L'orario è quello che vuoi e passi temporali fissi! gafferongames.com/game-physics/fix-your-timestep
Alan Wolfe

Risposte:


30

Le simulazioni basate su frame presenteranno errori quando gli aggiornamenti non riescono a compensare i tassi di variazione non lineari.

Ad esempio, si consideri un oggetto che inizia con valori di posizione e velocità pari a zero, con un'accelerazione costante di uno.

Se applichiamo questa logica di aggiornamento:

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

Possiamo aspettarci questi risultati con frame rate diversi: inserisci qui la descrizione dell'immagine

L'errore è causato trattando la velocità finale come se fosse applicata per l'intero fotogramma. Questo è simile alla somma di Riemann destra e la quantità di errore varia con la frequenza dei fotogrammi (illustrata con una funzione diversa):

Come sottolinea MichaelS , questo errore verrà dimezzato quando la durata del frame viene dimezzata e potrebbe diventare irrilevante con frame rate elevati. D'altra parte, tutti i giochi che presentano picchi di prestazioni o frame di lunga durata possono scoprire che questo produce comportamenti imprevedibili.


Fortunatamente la cinematica ci consente di calcolare accuratamente lo spostamento causato dall'accelerazione lineare:

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

Quindi, se applichiamo questa logica di aggiornamento:

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

Avremo i seguenti risultati:

inserisci qui la descrizione dell'immagine


2
Si tratta di informazioni utili, ma come risolve effettivamente il codice in questione? Innanzitutto, l'errore si riduce drasticamente all'aumentare del framerate, quindi la differenza tra 60 e 200 fps è trascurabile (8 fps contro l'infinito è già solo il 12,5% in più). In secondo luogo, una volta che lo sprite è alla massima velocità, la differenza più grande è di 0,5 unità davanti. Non dovrebbe influire sulla velocità effettiva di camminata, come mostrato nelle .gif allegate. Quando si girano, l'accelerazione è apparentemente istantanea (probabilmente diversi fotogrammi a 60+ fps, ma non secondi interi).
MichaelS,

2
Quindi è un problema di Unity o di codice, non un problema di matematica. Un foglio di calcolo rapido dice che se utilizziamo a = 1, vi = 0, di = 0, vmax = 1, dovremmo colpire vmax in t = 1, con d = 0,5. In questo modo su 5 fotogrammi (dt = 0,2), d (t = 1) = 0,6. Oltre 50 fotogrammi (dt = 0,02), d (t = 1) = 0,51. Oltre 500 fotogrammi (dt = 0,002), d (t = 1) = 0,501. Quindi 5 fps sono alti del 20%, 50 fps sono alti del 2% e 500 fps sono alti dello 0,2%. In generale, l'errore è del 100 / fps percento troppo alto. 50 fps è circa l'1,8% superiore a 500 fps. E questo è solo durante l'accelerazione. Una volta che la velocità raggiunge il massimo, non ci dovrebbe essere differenza zero. Con a = 100 e vmax = 5, dovrebbe esserci ancora meno differenza.
MichaelS,

2
In effetti, sono andato avanti e ho usato il tuo codice in un'app VB.net (simulando dt di 1/60 e 1/200), e ho ottenuto Bounce: 5 nel frame 626 (10.433) secondi vs. Bounce: 5 nel frame 2081 ( 10.405) secondi . 0,27% di tempo in più a 60 fps.
MichaelS,

2
È il tuo approccio "cinematico" che dà una differenza del 10%. L'approccio tradizionale è la differenza dello 0,27%. Li hai appena etichettati in modo errato. Penso che sia perché stai includendo erroneamente l'accelerazione quando la velocità è massima. Framerate più alte aggiungono meno errori per frame, quindi danno un risultato più accurato. Hai bisogno if(velocity==vmax||velocity==-vmax){acceleration=0}. Quindi l'errore diminuisce sostanzialmente, anche se non è perfetto poiché non capiamo esattamente quale parte dell'accelerazione del frame sia terminata.
MichaelS,

6

Dipende da dove stai chiamando il tuo passo. Se lo chiami da Update, il tuo movimento sarà effettivamente indipendente dal framerate se ridimensioni con Time.deltaTime, ma se lo chiami da FixedUpdate, devi ridimensionare con Time.fixedDeltaTime. Immagino che stai chiamando il tuo passaggio da FixedUpdate, ma ridimensionando con Time.deltaTime, il che comporterebbe una riduzione della velocità apparente quando il passaggio fisso di Unity è più lento del ciclo principale, che è ciò che sta accadendo nella tua build autonoma. Quando il passaggio fisso è lento, fixedDeltaTime è grande.


1
Viene chiamato da LateUpdate. Aggiornerò la mia domanda per chiarirlo. Anche se credo Time.deltaTimeche continuerà a utilizzare il valore corretto indipendentemente da dove viene chiamato (se utilizzato in FixedUpdate, utilizzerà fixedDeltaTime).
Cooper
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.