Come non perdersi nel Game Loop

UPS costante con FPS variabile interpolato

Il nome è già un programma e assomiglia più al nome di un’esibizione di tuffi che ad un algoritmo. Tuttavia è il meglio che ho trovato in circolazione (anche se è meglio padroneggiare il precedente prima di passare a questo). L’algoritmo è identico al precedente ad esclusione di due righe.

FRAMES_PER_SECOND = 50
SKIP_TICKS = 1000 / FRAMES_PER_SECOND
MAX_FRAMESKIP = 10

next_step_time = GetTicks()
loop = 0;

running = True;

while running  :
    loop = 0

    while (GetTicks() > next_step_time) and (loops < MAX_FRAMESKIP) :
        update()
        next_step_time += SKIP_TICKS
        loop += 1
   
    interpolation = (GetTick() + SKIP_TICKS - next_game_tick)/( SKIP_TICKS )
    rendering(interpolation)

La novità riguarda le ultime due righe. La variabile interpolation contiene un numero compreso fra 0 e 1 che rappresenta un punto intermedio fra due update consecutivi. La funzione di rendering invece è costruita in modo tale da usare questo valore per disegnare una scena che funga da interpolazione fra due update. Confusi? È normale, facciamo un esempio.

Supponiamo di avere un PC molto veloce che riesca ad eseguire due o più fasi di rendering prima di effettuare un nuovo update. Supponiamo inoltre che la palla che stiamo disegnando (inizialmente a zero) si sposta in linea retta di 1 metro ad ogni update. Lo stato del mondo attuale sarà dato dall’update precedente che chiamiamo U1. Adesso il ciclo richiede un nuovo rendering senza che sia ancora stato effettuato il nuovo update. La variabile interpolation calcola un valore di 0.3 indicando che ad un terzo del tempo che porta al nuovo stato U2. La funzione rendering quindi disegnerà un oggetto ad un terzo del percorso che lo porterà dal punto 0 al punto 1, ovvero a 0.3 metri dal punto di partenza.

A questo punto ci sono alcune chiarificazioni.

Primo. Innanzitutto non è possibile sapere qual’è lo stato U2 prima che sia calcolato dalla funzione di update. Questo significa che l’interpolazione disegnata dalla funzione di rendering sarà fatta fra l’ultimo stato calcolato e lo stato precedente. In pratica ciò che disegnamo sarà sempre in leggero ritardo rispetto allo stato effettivo, ma non preoccupatevi, parliamo di un ritardo nell’ordine dei centesimi di secondo, che lo rende di fatto impercettibile.

Per tornare all’esempio della palla supponiamo che l’ultimo stato calcolato (U2) veda la palla ad 1 metro. Adesso calcolo il nuovo stato (U3) con la palla a 2 metri, tuttavia poiché interpolation segna circa 0 la funzione di rendering disegnerà una scena molto più vicina allo stato U2 piuttosto che al nuovo stato U3.

Secondo. L’interpolazione deve essere semplice, non è un altro update. L’interpolazione riguarda solamente il rendering: non calcola la fisica, non percepisce le collisioni, non intercetta l’input. Quello che fa è una semplicissima interpolazione lineare fra l’ultimo stato calcolato e lo stato precedente. Ovviamente questo porta a cose “non giuste” ma vi ricordo che queste imprecisioni permangono per centesimi di secondo, quasi impercettibili.

Terzo. La complessità di questo game loop non sta nell’effettuare l’interpolazione bensì nel memorizzare gli ultimi due stati di tutti gli oggetti sullo schermo. È questa la vera complessità aggiunta quindi fateci attenzione durante lo sviluppo.

Conclusione

Insomma, abbiamo visto un bel gruzzolo di game loop e abbiamo imparato ad evitare i canti ammalianti dei più semplici. Ce ne sono altri? Si. Ci sono molte varianti e approcci differenti ma questi sono i più famosi e documentati e credo che siano sufficienti per iniziare ad avventurarsi nel semplice ma complesso mondo dei Game Loop.

Scusate per la valanga di parole ma non c’era modo di trattare l’argomento in modo più sintetico (già temo di aver tralasciato troppe cose). Spero vi sia utile. 🙂

Comments are closed.