✚ Sviluppo videogiochi

Il game loop: aggiornamento, rendering e passo fisso

Come funziona il game loop di un videogioco: input, aggiornamento e rendering, passo fisso con accumulatore e interpolazione per un movimento fluido.

Ogni videogioco, dal più semplice clone di Pong al titolo tridimensionale più ambizioso, ruota attorno a un ciclo che si ripete decine di volte al secondo: il game loop. Capire come è costruito è il primo passo per scrivere giochi che si comportano allo stesso modo su un portatile modesto e su un PC da gioco.

Le tre fasi del ciclo

Nella sua forma più elementare il game loop esegue, in ordine, tre operazioni:

  1. Lettura dell'input: tastiera, mouse, gamepad, touch.
  2. Aggiornamento dello stato: posizioni, velocità, punteggi, timer, intelligenza dei nemici, collisioni.
  3. Rendering: disegno della scena sullo schermo a partire dallo stato appena calcolato.

In pseudocodice:

while (in_esecuzione) {
    leggi_input()
    aggiorna()
    disegna()
}

Il problema di questa versione è che la velocità del gioco dipende dalla velocità della macchina. Se ogni chiamata ad aggiorna() sposta il personaggio di 2 pixel, su un computer che esegue il ciclo 300 volte al secondo il personaggio corre cinque volte più veloce che su uno che arriva a 60.

Il tempo variabile e i suoi limiti

La prima correzione consiste nel misurare quanto tempo è passato dall'ultimo fotogramma (il cosiddetto delta time) e moltiplicare ogni movimento per quel valore. Così il personaggio percorre, per esempio, 120 pixel al secondo indipendentemente dal frame rate.

Funziona bene per animazioni e movimenti semplici, ma mostra i suoi limiti con la fisica. Se il passo temporale cambia continuamente, i risultati cambiano con lui:

  • un salto può raggiungere altezze leggermente diverse a seconda del frame rate;
  • un oggetto veloce, in un fotogramma lungo, può attraversare un muro sottile senza che la collisione venga rilevata;
  • le simulazioni con molle e vincoli possono diventare instabili ed «esplodere» quando il delta è grande.

Inoltre il gioco diventa non deterministico: la stessa sequenza di input non produce sempre lo stesso risultato, cosa che complica replay, multiplayer e test automatici.

Il passo fisso con accumulatore

La soluzione più diffusa separa il tempo della simulazione dal tempo del rendering. La logica viene aggiornata sempre con lo stesso passo, per esempio 1/60 di secondo, mentre il rendering avviene quando può. Il collegamento tra i due mondi è un accumulatore:

const PASSO = 1.0 / 60.0
accumulatore = 0
precedente = ora()

while (in_esecuzione) {
    adesso = ora()
    trascorso = adesso - precedente
    precedente = adesso
    accumulatore += trascorso

    leggi_input()
    while (accumulatore >= PASSO) {
        aggiorna(PASSO)
        accumulatore -= PASSO
    }
    disegna()
}

Se il rendering è lento e tra due fotogrammi passano 33 millisecondi, il ciclo interno esegue due aggiornamenti. Se è velocissimo, alcuni fotogrammi non eseguono nessun aggiornamento e ridisegnano lo stesso stato. La fisica vede sempre e solo passi identici, ed è quindi stabile e ripetibile.

Interpolare per eliminare gli scatti

Il passo fisso introduce un difetto visivo: se lo schermo va a 144 Hz e la logica a 60, molti fotogrammi mostrano lo stesso stato e il movimento appare leggermente a scatti. Il rimedio è l'interpolazione. Si conservano lo stato precedente e quello attuale di ogni oggetto e, al momento del disegno, si calcola una posizione intermedia:

alfa = accumulatore / PASSO
posizione_disegnata = precedente * (1 - alfa) + attuale * alfa

Il valore alfa, compreso tra 0 e 1, indica quanto siamo «avanti» rispetto all'ultimo aggiornamento. Il giocatore vede un movimento fluido mentre la simulazione resta a passo fisso. Il costo è un ritardo visivo di al massimo un passo, di solito impercettibile.

La spirale della morte

C'è un caso da gestire esplicitamente. Se un singolo aggiornamento richiede più tempo del passo stesso, l'accumulatore cresce a ogni giro, il ciclo interno esegue sempre più aggiornamenti e il gioco rallenta fino a bloccarsi. È la cosiddetta spiral of death. La difesa è semplice: limitare il tempo trascorso considerato in un singolo fotogramma, per esempio a un quarto di secondo. In caso di sovraccarico il gioco rallenta per un istante invece di crollare.

Come lo gestiscono i motori

I motori moderni offrono già questa separazione. In Unity esistono Update, chiamato una volta per fotogramma, e FixedUpdate, chiamato a passo fisso per la fisica. In Godot la distinzione è tra _process e _physics_process. Sapere cosa succede dietro le quinte aiuta a decidere dove mettere il codice:

Tipo di logica Dove collocarla
Movimento con fisica, collisioni Aggiornamento a passo fisso
Lettura dell'input Ogni fotogramma, memorizzando lo stato
Animazioni grafiche, effetti, interfaccia Ogni fotogramma con delta time
Timer di gioco che contano Passo fisso, per coerenza

Un errore frequente è leggere un tasto premuto «una tantum» dentro l'aggiornamento a passo fisso: se in quel fotogramma l'aggiornamento non viene eseguito, la pressione si perde. Conviene registrare l'evento nel fotogramma e consumarlo al primo passo utile.

Sincronizzazione verticale e consumo

Infine, un ciclo che gira senza freni consuma il cento per cento di un core anche per disegnare un menu fermo. Attivare la sincronizzazione verticale, o inserire una breve attesa quando si è in anticipo, riduce consumi, calore e rumore delle ventole.

In sintesi

  • Il game loop legge l'input, aggiorna lo stato e disegna, decine di volte al secondo.
  • Il delta time rende il movimento indipendente dal frame rate, ma non basta per una fisica stabile.
  • Il passo fisso con accumulatore rende la simulazione stabile e ripetibile.
  • L'interpolazione tra stato precedente e attuale elimina gli scatti visivi.
  • Limitare il tempo per fotogramma evita la spirale della morte.
MM
Marco Marzaduri

Marco Marzaduri si occupa di cybersecurity, informatica forense e sviluppo web. Laureato in informatica, unisce il rigore dell'analisi tecnica all'attenzione per le persone che usano la tecnologia.

Leggi anche