Come funziona effettivamente l'interpolazione per appianare il movimento di un oggetto?


10

Ho fatto alcune domande simili negli ultimi 8 mesi o giù di lì senza una vera gioia, quindi renderò la domanda più generale.

Ho un gioco Android che è OpenGL ES 2.0. al suo interno ho il seguente Game Loop:

Il mio loop funziona secondo un principio di step temporale fisso (dt = 1 / ticksPerSecond )

loops=0;

    while(System.currentTimeMillis() > nextGameTick && loops < maxFrameskip){

        updateLogic(dt);
        nextGameTick+=skipTicks;
        timeCorrection += (1000d/ticksPerSecond) % 1;
        nextGameTick+=timeCorrection;
        timeCorrection %=1;
        loops++;

    }

    render();   

La mia integrazione funziona così:

sprite.posX+=sprite.xVel*dt;
sprite.posXDrawAt=sprite.posX*width;

Ora, tutto funziona praticamente come vorrei. Posso specificare che vorrei che un oggetto si muovesse attraverso una certa distanza (diciamo la larghezza dello schermo) in 2,5 secondi e lo farà. Anche a causa del salto del frame che autorizzo nel mio loop di gioco, posso farlo praticamente su qualsiasi dispositivo e ci vorranno sempre 2,5 secondi.

Problema

Tuttavia, il problema è che quando una cornice di rendering salta, la balbuzie grafica. È estremamente fastidioso. Se rimuovo la possibilità di saltare i frame, allora tutto è liscio come preferisci, ma funzionerà a velocità diverse su dispositivi diversi. Quindi non è un'opzione.

Non sono ancora sicuro del motivo per cui il frame salta, ma vorrei sottolineare che questo non ha nulla a che fare con prestazioni scadenti , ho riportato il codice a 1 minuscolo sprite e nessuna logica (a parte la logica richiesta per spostare lo sprite) e continuo a saltare i frame. E questo è su un tablet Google Nexus 10 (e come menzionato sopra, ho bisogno di saltare il frame per mantenere la velocità costante su tutti i dispositivi).

Quindi, l'unica altra opzione che ho è usare l'interpolazione (o estrapolazione), ho letto tutti gli articoli che ci sono là fuori ma nessuno mi ha davvero aiutato a capire come funziona e tutte le mie tentate implementazioni hanno fallito.

Usando un metodo sono stato in grado di far muovere le cose senza intoppi, ma non è stato possibile perché ha incasinato la mia collisione. Posso prevedere lo stesso problema con qualsiasi metodo simile perché l'interpolazione viene passata (e attuata all'interno) al metodo di rendering - al momento del rendering. Quindi, se Collision corregge la posizione (il personaggio ora si trova proprio accanto al muro), il renderer può modificare la sua posizione e disegnarla nel muro.

Quindi sono davvero confuso. La gente ha detto che non dovresti mai modificare la posizione di un oggetto all'interno del metodo di rendering, ma tutti gli esempi online lo mostrano.

Quindi sto chiedendo una spinta nella giusta direzione, per favore non collegarti agli articoli popolari del loop di gioco (deWitters, Fix your timestep, ecc.) Come ho letto più volte . Sto Non chiedo a nessuno di scrivere il mio codice per me. Spiega per favore in termini semplici come l'interpolazione funziona effettivamente con alcuni esempi. Vado quindi a cercare di integrare qualsiasi idea nel mio codice e, se necessario, farò domande più specifiche. (Sono sicuro che questo è un problema con cui molte persone lottano).

modificare

Alcune informazioni aggiuntive: variabili utilizzate nel loop di gioco.

private long nextGameTick = System.currentTimeMillis();
//loop counter
private int loops;
//Amount of frames that we will allow app to skip before logic is affected
private final int maxFrameskip = 5;                         
//Game updates per second
final int ticksPerSecond = 60;
//Amount of time each update should take        
private final int skipTicks = (1000 / ticksPerSecond);
float dt = 1f/ticksPerSecond;
private double timeCorrection;

E il motivo del downvote è ...................?
BungleBonce,

1
Impossibile dirlo a volte. Questo sembra avere tutto ciò che una buona domanda dovrebbe avere quando si cerca di risolvere un problema. Snippet di codice conciso, spiegazioni di ciò che hai provato, tentativi di ricerca e spiegazione chiara di quale sia il tuo problema e cosa devi sapere.
Jesse Dorsey

Non ero il tuo downvote, ma per favore chiarisci una parte. Dici la balbuzie grafica quando una cornice viene saltata. Sembra un'affermazione ovvia (manca un frame, sembra che un frame sia mancato). Quindi puoi spiegare meglio il salto? Succede qualcosa di più strano? Altrimenti, questo potrebbe essere un problema irrisolvibile, perché non è possibile ottenere un movimento regolare se il framerate scende.
Seth Battin,

Grazie, Noctrine, mi dà davvero fastidio quando le persone votano senza lasciare una spiegazione. @SethBattin, scusa, sì, certo, hai ragione, il salto del frame sta causando il jerkiness, tuttavia, l'interpolazione di qualche tipo dovrebbe risolvere questo problema, come ho detto sopra, ho avuto un successo (ma limitato). Se sbaglio, allora suppongo che la domanda sarebbe, come posso farlo funzionare senza problemi alla stessa velocità su vari dispositivi?
BungleBonce,

4
Rileggi attentamente quei documenti. In realtà non modificano la posizione dell'oggetto nel metodo di rendering. Modificano solo la posizione apparente del metodo in base alla sua ultima posizione e la sua posizione corrente in base al tempo trascorso.
AttackingHobo

Risposte:


5

Ci sono due cose cruciali per rendere il movimento più fluido, il primo è ovviamente quello che il rendering deve corrispondere allo stato previsto nel momento in cui il frame viene presentato all'utente, il secondo è che devi presentare i frame all'utente ad un intervallo relativamente fisso. Presentare un frame a T + 10ms, poi un altro a T + 30ms, quindi un altro a T + 40ms, sembrerà all'utente di giudicare, anche se ciò che è effettivamente mostrato per quei tempi è corretto secondo la simulazione.

Il tuo loop principale sembra non avere alcun meccanismo di gating per assicurarti di eseguire il rendering solo a intervalli regolari. Quindi a volte potresti fare 3 aggiornamenti tra i render, a volte potresti fare 4. Fondamentalmente il tuo loop verrà eseguito il rendering il più spesso possibile, non appena avrai simulato abbastanza tempo per spingere lo stato di simulazione davanti all'ora corrente, quindi rendere quello stato. Ma qualsiasi variabilità nel tempo necessario per l'aggiornamento o il rendering e anche l'intervallo tra i frame varierà. Hai un timestep fisso per la tua simulazione, ma un timestep variabile per il tuo rendering.

Ciò di cui probabilmente hai bisogno è un'attesa appena prima del rendering, che ti assicuri di iniziare il rendering solo all'inizio di un intervallo di rendering. Idealmente dovrebbe essere adattivo: se hai impiegato troppo tempo per aggiornare / renderizzare e l'inizio dell'intervallo è già passato, dovresti renderizzarlo immediatamente, ma anche aumentare la lunghezza dell'intervallo, fino a quando puoi renderizzare e aggiornare costantemente e ancora arrivare a il rendering successivo prima che l'intervallo sia terminato. Se hai un sacco di tempo da perdere, puoi ridurre lentamente l'intervallo (ovvero aumentare la frequenza dei fotogrammi) per eseguire nuovamente il rendering più velocemente.

Ma, ed ecco il kicker, se non si esegue il rendering del frame immediatamente dopo aver rilevato che lo stato della simulazione è stato aggiornato a "now", si introduce l'alias temporale. Il frame che viene presentato all'utente viene presentato al momento leggermente sbagliato, e questo di per sé sembrerà una balbuzie.

Questo è il motivo del "timestep parziale" che vedrai menzionato negli articoli che hai letto. È lì per una buona ragione, e questo perché, a meno che non fissi il tuo timestep fisico a un multiplo integrale fisso del tuo timestep di rendering fisso, semplicemente non puoi presentare i frame al momento giusto. Finisci per presentarli troppo presto o troppo tardi. L'unico modo per ottenere una frequenza di rendering fissa e presentare ancora qualcosa che è fisicamente corretto, è accettare che nel momento in cui si verifica l'intervallo di rendering, molto probabilmente sarai a metà strada tra due dei tuoi timestep fisici fissi. Ciò non significa che gli oggetti vengano modificati durante il rendering, solo che il rendering deve stabilire temporaneamente dove si trovano gli oggetti in modo da renderli da qualche parte tra dove erano prima e dove sono dopo l'aggiornamento. Questo è importante: non cambiare mai lo stato mondiale per il rendering, solo gli aggiornamenti dovrebbero cambiare lo stato mondiale.

Quindi, per inserirlo in un ciclo pseudocodice, penso che tu abbia bisogno di qualcosa di più simile a:

InitialiseWorldState();

previousTime = currentTime = 0.0;
renderInterval = 1.0 / 60.0; //A nice high starting interval

subFrameProportion = 1.0; //100% currentFrame, 0% previousFrame

while (true)
{
    frameStart = ActualTime();

    //Render the world state as if it was some proportion 
    // between previousTime and currentTime
    // E.g. if subFrameProportion is 0.5, previousTime is 0.1 and 
    // currentTime is 0.2, then we actually want to render the state
    // as it would be at time 0.15. We'd do that by interpolating 
    // between movingObject.previousPosition and movingObject.currentPosition
    // with a lerp parameter of 0.5
    Render(subFrameProportion); 

    //Check we've not taken too long and missed our render interval
    frameTime = ActualTime() - frameStart;
    if (frameTime > renderInterval)
    {
        renderInterval = frameTime * 1.2f; //Give us a more reasonable render interval that we actually have a chance of hitting
    }

    expectedFrameEnd = frameStart + renderInterval;

    //Loop until it's time to render the next frame
    while (ActualTime() < expectedFrameEnd)
    {
        //step the simulation forward until it has moved just beyond the frame end
        if (previousTime < expectedFrameEnd) &&
            currentTime >= expectedFrameEnd)
        {
            previousTime = currentTime;

            Update();
            currentTime += fixedTimeStep;

            //After the update, all objects will be in the position they should be for
            // currentTime, **but** they also need to remember where they were before,
            // so that the rendering can draw them somewhere between previousTime and
            //  currentTime

            //Check again we've not taken too long and missed our render interval
            frameTime = ActualTime() - frameStart;
            if (frameTime > renderInterval)
            {
                renderInterval = frameTime * 1.2f; //Give us a more reasonable render interval that we actually have a chance of hitting
                expectedFrameEnd = frameStart + renderInterval
            }
        }
        else
        {
            //We've brought the simulation to just after the next time
            // we expect to render, so we just want to wait.
            // Ideally sleep or spin in a tight loop while waiting.
            timeTillFrameEnd = expectedFrameEnd - ActualTime();
            sleep(timeTillFrameEnd);
        }
    }

    //How far between update timesteps (i.e. previousTime and currentTime)
    // will we be at the end of the frame when we start the next render?
    subFrameProportion = (expectedFrameEnd - previousTime) / (currentTime - previousTime);
}

Affinché ciò funzioni, tutti gli oggetti in fase di aggiornamento devono preservare la conoscenza di dove si trovavano prima e dove si trovano ora, in modo che il rendering possa utilizzare la sua conoscenza di dove si trova l'oggetto.

class MovingObject
{
    Vector velocity;
    Vector previousPosition;
    Vector currentPosition;

    Initialise(startPosition, startVelocity)
    {
        currentPosition = startPosition; // position at time 0
        velocity = startVelocity;
        //ignore previousPosition because we should never render before time 0
    }

    Update()
    {
        previousPosition = currentPosition;
        currentPosition += velocity * fixedTimeStep;
    }

    Render(subFrameProportion)
    {
        Vector actualPosition = 
            Lerp(previousPosition, currentPosition, subFrameProportion);
        RenderAt(actualPosition);
    }
}

E definiamo una linea temporale in millisecondi, dicendo che il rendering richiede 3ms per essere completato, l'aggiornamento richiede 1ms, il tuo time-step di aggiornamento è fissato a 5ms e il tuo timestep di rendering inizia (e rimane) a 16ms [60Hz].

0   1   2   3   4   5   6   7   8   9   10  11  12  13  14  15  16  17  18  19  20  21  22  23  24  25  26  27  28  29  30  31  32  33
R0          U5  U10 U15 U20 W16                                 R16         U25 U30 U35 W32                                 R32
  1. Prima inizializziamo al tempo 0 (quindi currentTime = 0)
  2. Facciamo il rendering con una proporzione di 1.0 (100% currentTime), che disegnerà il mondo al tempo 0
  3. Al termine, il tempo effettivo è 3 e non prevediamo che il frame termini fino alle 16, quindi è necessario eseguire alcuni aggiornamenti
  4. T + 3: aggiorniamo da 0 a 5 (quindi in seguito currentTime = 5, previousTime = 0)
  5. T + 4: ancora prima della fine del frame, quindi aggiorniamo da 5 a 10
  6. T + 5: ancora prima della fine del frame, quindi aggiorniamo da 10 a 15
  7. T + 6: ancora prima della fine del frame, quindi aggiorniamo da 15 a 20
  8. T + 7: ancora prima della fine del frame, ma currentTime è appena oltre la fine del frame. Non vogliamo simulare ulteriormente perché farlo ci spingerebbe oltre il tempo che vorremmo renderizzare. Invece aspettiamo piano il prossimo intervallo di rendering (16)
  9. T + 16: è tempo di eseguire nuovamente il rendering. previousTime è 15, currentTime è 20. Quindi, se vogliamo renderizzare a T + 16, siamo 1ms di distanza attraverso il timestep lungo 5ms. Quindi siamo al 20% del modo attraverso il frame (proporzione = 0,2). Quando eseguiamo il rendering, disegniamo il 20% degli oggetti tra la loro posizione precedente e la loro posizione corrente.
  10. Torna al 3. e continua indefinitamente.

C'è un'altra sfumatura qui sulla simulazione troppo in anticipo, il che significa che gli input dell'utente potrebbero essere ignorati anche se sono avvenuti prima che il frame fosse effettivamente riprodotto, ma non preoccuparti finché non sei sicuro che il loop stia simulando senza problemi.


NB: lo pseudocodice è debole in due modi. In primo luogo non rileva il caso della spirale della morte (impiega più tempo di fixedTimeStep ad aggiornare, il che significa che la simulazione è sempre più indietro, in effetti un ciclo infinito), in secondo luogo il renderingInterval non viene mai più abbreviato. In pratica si desidera aumentare immediatamente renderInterval, ma nel tempo accorciarlo gradualmente nel miglior modo possibile entro una certa tolleranza del tempo di frame effettivo. Altrimenti un aggiornamento errato / lungo ti sosterrà per sempre con un framerate basso.
MrCranky,

Grazie per questo @MrCranky, in effetti, ho lottato per anni su come "limitare" il rendering nel mio loop! Non riuscivo a capire come farlo e mi chiedevo se quello potesse essere uno dei problemi. Avrò una lettura corretta di questo e darò una prova ai tuoi suggerimenti, riporterò indietro! Grazie ancora :-)
BungleBonce

Grazie @MrCranky, OK, ho letto e riletto la tua risposta ma non riesco a capirla :-( Ho provato a implementarlo ma mi ha dato solo uno schermo vuoto. Davvero alle prese con questo. PreviousFrame e currentFrame presumo riguarda le posizioni precedenti e attuali dei miei oggetti in movimento? Inoltre, che dire della linea "currentFrame = Update ();" - Non capisco questa linea, questo significa call update (); come non riesco a vedere dove altrimenti sto chiamando update? O significa semplicemente impostare currentFrame (posizione) sul suo nuovo valore? Grazie ancora per il vostro aiuto !!
BungleBonce

Sì, efficacemente. Il motivo per cui ho inserito in previousFrame e currentFrame come valori di ritorno da Update e InitialiseWorldState è perché per consentire al rendering di disegnare il mondo in quanto è a metà strada tra due passaggi di aggiornamento fissi, è necessario disporre non solo della posizione corrente di ogni oggetto che vuoi disegnare, ma anche le loro posizioni precedenti. Si potrebbe avere ogni oggetto salvare entrambi i valori internamente, il che diventa ingombrante.
MrCranky,

Ma è anche possibile (ma molto più difficile) progettare le cose in modo che tutte le informazioni sullo stato necessarie per rappresentare lo stato attuale del mondo al momento T siano conservate sotto un singolo oggetto. Concettualmente è molto più chiaro quando si spiegano quali informazioni sono presenti nel sistema in quanto è possibile trattare lo stato del frame come qualcosa prodotto da un passaggio di aggiornamento, e mantenere il frame precedente intorno significa semplicemente conservare un altro di quegli oggetti stato del frame. Tuttavia, potrei riscrivere la risposta in modo che sia un po 'più simile a quella che probabilmente la implementeresti.
MrCranky,

3

Ciò che tutti ti hanno detto è corretto. Non aggiornare mai la posizione di simulazione dello sprite nella logica di rendering.

Pensala in questo modo, il tuo sprite ha 2 posizioni; dove la simulazione dice che è all'ultimo aggiornamento della simulazione e dove viene eseguito lo sprite. Sono due coordinate completamente diverse.

Lo sprite è reso nella sua posizione estrapolata. La posizione estrapolata viene calcolata per ogni fotogramma di rendering, utilizzata per il rendering dello sprite, quindi eliminata. Questo è tutto quello che c'è da fare.

A parte questo, sembra che tu abbia una buona comprensione. Spero che sia di aiuto.


Eccellente @WilliamMorrison - grazie per averlo confermato, non ero mai sicuro al 100% che fosse così, ora penso di essere sulla buona strada per farlo funzionare in qualche modo - evviva!
BungleBonce,

Semplicemente curioso @WilliamMorrison, usando queste coordinate usa e getta, come mitigare il problema degli sprite che sono disegnati "incorporati" o "appena sopra" altri oggetti - l'esempio ovvio, essendo oggetti solidi in un gioco 2d. Dovresti eseguire anche il tuo codice di collisione al momento del rendering?
BungleBonce,

Nei miei giochi sì, è quello che faccio. Per favore, sii migliore di me, non farlo, non è la soluzione migliore. Ciò complica il codice di rendering con la logica che non dovrebbe utilizzare e sprecherà la CPU nel rilevamento di collisioni ridondante. Sarebbe meglio interpolare tra la penultima posizione e la posizione corrente. Questo risolve il problema in quanto non si sta estrapolando in una cattiva posizione, ma complica le cose mentre si sta facendo un passo indietro rispetto alla simulazione. Mi piacerebbe sentire la tua opinione, l'approccio che segui e le tue esperienze.
William Morrison,

Sì, è un problema difficile da risolvere. Ho fatto una domanda separata riguardo a questo qui gamedev.stackexchange.com/questions/83230/… se vuoi tenerlo d'occhio o contribuire con qualcosa. Ora, cosa hai suggerito nel tuo commento, non lo sto già facendo? (Interpolazione tra frame precedente e corrente)?
BungleBonce,

Non proprio. Al momento stai estrapolando. Prendi i dati più recenti dalla simulazione ed estrapoli l'aspetto di quei dati dopo intervalli di tempo frazionari. Sto suggerendo di interpolare tra l'ultima posizione di simulazione e l'attuale posizione di simulazione con timestep frazionari per il rendering. Il rendering sarà dietro la simulazione di 1 timestep. Ciò garantisce che non verrà mai eseguito il rendering di un oggetto in uno stato che la simulazione non ha convalidato (ad es. Un proiettile non apparirà in un muro a meno che la simulazione non fallisca).
William Morrison,
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.