Come interpolare tra due stati di gioco?


24

Qual è il modello migliore per creare un sistema che posiziona tutti gli oggetti da interpolare tra due stati di aggiornamento?

L'aggiornamento verrà sempre eseguito alla stessa frequenza, ma voglio essere in grado di eseguire il rendering su qualsiasi FPS. Quindi il rendering sarà il più fluido possibile, indipendentemente dai frame al secondo, sia inferiore che superiore alla frequenza di aggiornamento.

Vorrei aggiornare 1 frame nel futuro interpolato dal frame corrente al frame futuro. Questa risposta ha un link che parla di questo:

Timestep semi-fisso o completamente fisso?

Modifica: come posso usare anche l'ultima e la velocità attuale nell'interpolazione? Ad esempio, con una semplice interpolazione lineare, si sposta alla stessa velocità tra le posizioni. Ho bisogno di un modo per far interpolare la posizione tra i due punti, ma prendere in considerazione la velocità in ciascun punto per l'interpolazione. Sarebbe utile per simulazioni a basso tasso come effetti particellari.


2
le zecche sono zecche logiche? Quindi il tuo aggiornamento fps <rendering fps?
Il comunista Duck

Ho cambiato il termine. Ma sì tick logici. E no, desidero liberare completamente il rendering dall'aggiornamento, quindi il gioco può eseguire il rendering a 120 HZ o 22,8 HZ e l'aggiornamento continuerà a funzionare alla stessa velocità, a condizione che l'utente soddisfi i requisiti di sistema.
Attaccando Hobo

questo potrebbe essere davvero complicato poiché durante il rendering tutte le posizioni degli oggetti dovrebbero rimanere ferme (modificarle durante il processo di rendering potrebbe causare un comportamento indefinito)
Ali1S232,

L'interpolazione calcolerebbe lo stato in un momento tra 2 frame di aggiornamento già calcolati. Non è questa domanda sull'estrapolazione, il calcolo dello stato per un tempo dopo l'ultimo frame di aggiornamento? Dal momento che il prossimo aggiornamento non è nemmeno stato ancora capitalizzato.
Maik Semder,

Penso che se ha solo un thread di aggiornamento / rendering, non può succedere di ri-aggiornare solo la posizione di rendering. È sufficiente inviare posizioni alla GPU e quindi aggiornare nuovamente.
Zacharmarz,

Risposte:


22

Si desidera separare le frequenze di aggiornamento (tick di logica) e disegnare (render tick).

I tuoi aggiornamenti produrranno la posizione di tutti gli oggetti nel mondo da disegnare.

Tratterò due diverse possibilità qui, quella che hai richiesto, l'estrapolazione e anche un altro metodo, l'interpolazione.

1.

L'estrapolazione è il punto in cui calcoleremo la posizione (prevista) dell'oggetto nel fotogramma successivo, quindi interpoleremo tra la posizione corrente degli oggetti e la posizione in cui l'oggetto sarà nel fotogramma successivo.

Per fare ciò, ogni oggetto da disegnare deve avere un velocitye associato position. Per trovare la posizione in cui si troverà l'oggetto nel fotogramma successivo, aggiungiamo semplicemente velocity * draw_timestepalla posizione corrente dell'oggetto, per trovare la posizione prevista del fotogramma successivo. draw_timestepè la quantità di tempo trascorsa dal segno di spunta del rendering precedente (ovvero la precedente chiamata di disegno).

Se lo lasci a questo, troverai che gli oggetti "tremolano" quando la loro posizione prevista non corrispondeva alla posizione effettiva nel fotogramma successivo. Per eliminare lo sfarfallio, è possibile memorizzare la posizione prevista e scorrere tra la posizione prevista in precedenza e la nuova posizione prevista in ciascuna fase di disegno, utilizzando il tempo trascorso dal precedente aggiornamento come fattore di lerp. Ciò comporterà comunque un comportamento scadente quando gli oggetti in rapido movimento cambiano improvvisamente posizione e potresti voler gestire quel caso speciale. Tutto quanto detto in questo paragrafo sono i motivi per cui non si desidera utilizzare l'estrapolazione.

2.

L'interpolazione è il luogo in cui memorizziamo lo stato degli ultimi due aggiornamenti e li interpoliamo in base al tempo corrente trascorso dall'aggiornamento precedente all'ultimo. In questa configurazione, ogni oggetto deve avere un positione associato previous_position. In questo caso, il nostro disegno rappresenterà, nella peggiore delle ipotesi, un segno di spunta di aggiornamento dietro l'attuale gamestate e, nella migliore delle ipotesi, nello stesso stato esatto del segno di spunta di aggiornamento corrente.


Secondo me, probabilmente vorresti l'interpolazione come l'ho descritta, in quanto è la più facile da implementare delle due, e disegnare una piccola frazione di secondo (ad esempio 1/60 di secondo) dietro il tuo attuale stato aggiornato va bene.


Modificare:

Nel caso in cui quanto sopra non sia sufficiente per consentire l'esecuzione di un'implementazione, ecco un esempio di come eseguire il metodo di interpolazione che ho descritto. Non tratterò l'estrapolazione, perché non riesco a pensare a uno scenario del mondo reale in cui dovresti preferirlo.

Quando si crea un oggetto disegnabile, memorizzerà le proprietà necessarie per essere disegnate (ovvero, le informazioni sullo stato necessarie per disegnarlo).

Per questo esempio, memorizzeremo posizione e rotazione. Potresti anche voler memorizzare altre proprietà come il colore o la posizione delle coordinate della trama (cioè se una trama scorre).

Per impedire che i dati vengano modificati mentre il thread di rendering lo sta disegnando (ovvero la posizione di un oggetto viene modificata mentre il thread di rendering viene disegnato, ma tutti gli altri non sono ancora stati aggiornati), è necessario implementare un tipo di doppio buffering.

Un oggetto ne memorizza due copie previous_state. Li inserirò in un array e li chiamerò come previous_state[0]e previous_state[1]. Allo stesso modo ha bisogno di due copie di esso current_state.

Per tenere traccia di quale copia del doppio buffer viene utilizzata, memorizziamo una variabile state_index, disponibile sia per l'aggiornamento che per il thread di disegno.

Il thread di aggiornamento calcola innanzitutto tutte le proprietà di un oggetto utilizzando i propri dati (qualsiasi struttura di dati desiderata). Poi, le copie current_state[state_index]a previous_state[state_index], e copia i nuovi dati necessari per la compilazione, positione rotationin current_state[state_index]. Quindi lo fa state_index = 1 - state_index, per capovolgere la copia attualmente utilizzata del doppio buffer.

Tutto nel paragrafo precedente deve essere fatto con un lucchetto rimosso current_state. L'aggiornamento e il disegno dei thread eliminano entrambi questo blocco. Il blocco viene rimosso solo per la durata della copia delle informazioni sullo stato, che è veloce.

Nel thread di rendering, fai quindi un'interpolazione lineare su posizione e rotazione in questo modo:

current_position = Lerp(previous_state[state_index].position, current_state[state_index].position, elapsed/update_tick_length)

Dov'è elapsedil tempo che è trascorso nel thread di rendering, dall'ultimo tick di aggiornamento, ed update_tick_lengthè il tempo che impiega la frequenza di aggiornamento fissa per tick (ad es. Con aggiornamenti 20FPS update_tick_length = 0.05).

Se non sai quale sia la Lerpfunzione sopra, controlla l'articolo di Wikipedia sull'argomento: Interpolazione lineare . Tuttavia, se non sai cos'è il lerping, probabilmente non sei pronto per implementare l'aggiornamento / disegno disaccoppiato con il disegno interpolato.


1
+1 lo stesso deve essere fatto per gli orientamenti / rotazioni e tutti gli altri stati che cambiano nel tempo, ad esempio animazioni materiali nei sistemi di particelle, ecc.
Maik Semder,

1
Buon punto Maik, ho appena usato la posizione come esempio. È necessario memorizzare la "velocità" di qualsiasi proprietà che si desidera estrapolare (ovvero il tasso di variazione nel tempo di quella proprietà), se si desidera utilizzare l'estrapolazione. Alla fine, non riesco davvero a pensare a una situazione in cui l'estrapolazione è migliore dell'interpolazione, l'ho inclusa solo perché la domanda del richiedente lo ha richiesto. Uso l'interpolazione. Con l'interpolazione, dobbiamo memorizzare i risultati di aggiornamento attuali e precedenti di qualsiasi proprietà da interpolare, come hai detto tu.
Olhovsky,

Questa è una riaffermazione del problema e la differenza tra interpolazione ed estrapolazione; non è una risposta.

1
Nel mio esempio ho memorizzato posizione e rotazione nello stato. Puoi anche memorizzare la velocità (o la velocità) anche nello stato. Quindi si passa da una velocità all'altra allo stesso modo ( Lerp(previous_speed, current_speed, elapsed/update_tick_length)). Puoi farlo con qualsiasi numero che desideri memorizzare nello stato. Lerping ti dà solo un valore tra due valori, dato un fattore lerp.
Olhovsky,

1
Per l'interpolazione del movimento angolare si consiglia di utilizzare slerp anziché lerp. Il più semplice sarebbe immagazzinare i quaternioni di entrambi gli stati e farli passare da uno all'altro. Altrimenti valgono le stesse regole per la velocità angolare e l'accelerazione angolare. Hai un banco di prova per l'animazione scheletrica?
Maik Semder,

-2

Questo problema richiede di pensare alle definizioni di inizio e fine in modo leggermente diverso. I programmatori principianti pensano spesso al cambiamento di posizione per frame e questo è un ottimo modo per iniziare. Per motivi di risposta, consideriamo una risposta unidimensionale.

Supponiamo che tu abbia una scimmia in posizione x. Ora hai anche un "addX" al quale aggiungi la posizione della scimmia per fotogramma in base alla tastiera o ad altri controlli. Funzionerà finché avrai un frame rate garantito. Supponiamo che x sia 100 e addX sia 10. Dopo 10 fotogrammi, x + = addX dovrebbe accumularsi a 200.

Ora, invece di addX, quando hai un frame rate variabile, dovresti pensare in termini di velocità e accelerazione. Ti guiderò attraverso tutta questa aritmetica ma è super semplice. Quello che vogliamo sapere è quanto lontano vuoi viaggiare per millisecondo (1/1000 di secondo)

Se stai scattando per 30 FPS, allora il tuo velX dovrebbe essere 1/3 di secondo (10 fotogrammi dall'ultimo esempio a 30 FPS) e sai che vuoi viaggiare 100 'x' in quel momento, quindi imposta il tuo velX su 100 distanze / 10 FPS o 10 distanze per fotogramma. In millisecondi, ciò equivale a 1 distanza x per 3,3 millisecondi o 0,3 'x' per millisecondo.

Ora, ogni volta che aggiorni, tutto ciò che devi fare è capire il tempo trascorso. Sia che siano trascorsi 33 ms (1/30 di secondo) o qualsiasi altra cosa, è sufficiente moltiplicare la distanza 0,3 per il numero di millisecondi passati. Questo significa che hai bisogno di un timer che ti dia una precisione di ms (millisecondi) ma che la maggior parte dei timer ti dà. Fai semplicemente una cosa del genere:

var beginTime = getTimeInMillisecond ()

... dopo ...

var time = getTimeInMillisecond ()

var elapsedTime = time-beginTime

beginTime = time

... ora usa questo tempo trascorso per calcolare tutte le tue distanze.


1
Non ha una frequenza di aggiornamento variabile. Ha una frequenza di aggiornamento fissa. Ad essere sincero, non so davvero quale punto stai cercando di chiarire qui: /
Olhovsky,

1
??? -1. Questo è l'intero punto, sto avendo una frequenza di aggiornamento garantita, ma una velocità di rendering variabile e voglio che sia fluida senza balbuzie.
Attaccando Hobo

Le velocità di aggiornamento variabili non funzionano bene con i giochi in rete, i giochi competitivi, i sistemi di riproduzione o qualsiasi altra cosa che si basa sul fatto che il gioco sia deterministico.
Attaccando Hobo

1
L'aggiornamento fisso consente anche una facile integrazione dello pseudo-attrito. Ad esempio, se vuoi moltiplicare la tua velocità per 0,9 per ogni fotogramma, come fai a capire quanto moltiplicare se hai un fotogramma veloce o lento? L'aggiornamento fisso a volte è molto preferito: praticamente tutte le simulazioni fisiche utilizzano una frequenza di aggiornamento fissa.
Olhovsky,

2
Se ho usato un frame rate variabile e impostato uno stato iniziale complesso con molti oggetti che rimbalzano l'uno dall'altro, non vi è alcuna garanzia che simulerà esattamente lo stesso. In effetti, molto probabilmente simulerà in modo leggermente diverso ogni volta, con piccole differenze all'inizio, aggravando per un breve periodo in stati completamente diversi tra ogni serie di simulazioni.
Attaccando Hobo
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.