✚ Sviluppo videogiochi

Prestazioni nei videogiochi: profiling, draw call e object pooling

Come migliorare le prestazioni di un gioco: budget del fotogramma, profiling, meno draw call, object pooling e allocazioni da evitare nel game loop.

Un gioco che scende di frame rate nei momenti più concitati, o che si blocca per un istante ogni tanto, dà subito una sensazione di scarsa qualità. Ottimizzare non significa riscrivere tutto in modo complicato fin dall'inizio, ma sapere dove il tempo viene speso e intervenire lì, con metodo.

Il budget del fotogramma

Il punto di partenza è un numero. A 60 fotogrammi al secondo ogni fotogramma ha a disposizione circa 16,7 millisecondi; a 30 circa 33,3; a 120 circa 8,3. In questo tempo bisogna leggere l'input, aggiornare la logica, la fisica, l'audio e disegnare la scena.

Ragionare in millisecondi è più utile che ragionare in fotogrammi al secondo. Passare da 60 a 50 fps sembra una piccola perdita, ma significa che il fotogramma è passato da 16,7 a 20 ms: 3,3 ms in più, una quantità precisa da cercare.

Conta anche la regolarità. Un gioco con una media di 60 fps ma con un fotogramma da 100 ms ogni pochi secondi è percepito come peggiore di uno stabile a 50. Questi picchi, spesso chiamati stutter, sono altrettanto importanti della media.

Misurare prima di ottimizzare

L'intuizione su dove sta il problema è spesso sbagliata. Lo strumento giusto è il profiler, che mostra quanto tempo richiede ciascuna funzione o sistema in ogni fotogramma. Tutti i motori principali ne includono uno, e se si usa un framework si possono aggiungere misure manuali:

inizio = tempo_preciso()
aggiorna_nemici()
registra("nemici", tempo_preciso() - inizio)

Il metodo:

  1. riprodurre una situazione lenta in modo ripetibile, per esempio un livello con molti nemici;
  2. misurare e individuare la voce più costosa;
  3. intervenire solo su quella;
  4. misurare di nuovo per verificare il miglioramento.

Bisogna anche capire se il collo di bottiglia è il processore o la scheda grafica. Se riducendo drasticamente la risoluzione il gioco diventa molto più veloce, il limite è probabilmente il lavoro sui pixel della GPU; se non cambia nulla, è il processore.

Draw call e batching

Ogni volta che il programma chiede alla scheda grafica di disegnare qualcosa effettua una draw call, che ha un costo fisso sul processore indipendente da quanto è piccolo l'oggetto. Mille sprite disegnati con mille chiamate separate possono essere molto più lenti di mille sprite disegnati in un'unica chiamata.

Il batching raggruppa gli oggetti che condividono texture e materiale in una sola chiamata. Per favorirlo:

  • raccogliere gli sprite in atlanti di texture comuni;
  • limitare il numero di materiali e shader diversi;
  • ordinare il disegno per texture quando l'ordine di profondità lo consente;
  • per molti oggetti identici, come erba o proiettili, usare il disegno instanziato.

Object pooling

Creare e distruggere oggetti continuamente, per esempio centinaia di proiettili al secondo, ha due costi: l'inizializzazione di ciascun oggetto e, nei linguaggi con garbage collector, le pause per liberare la memoria. Queste pause sono una causa tipica di stutter.

L'object pooling crea in anticipo un insieme di oggetti e li riutilizza:

pool = [nuovo Proiettile() per i da 1 a 200]

funzione spara():
    p = primo proiettile non attivo in pool
    se p esiste:
        p.attiva(posizione, direzione)

funzione al_termine(p):
    p.disattiva()   // torna disponibile

Il pool va dimensionato sul caso peggiore realistico. Se si esaurisce, si può scegliere se espanderlo, riutilizzare l'oggetto più vecchio o semplicemente non creare il nuovo.

Altre fonti comuni di lentezza

Problema Soluzione tipica
Allocazioni di memoria nel game loop Riutilizzare liste e buffer, evitare concatenazioni di stringhe a ogni fotogramma
Ricerche ripetute di oggetti per nome Memorizzare i riferimenti una volta sola
Collisioni tra tutte le coppie Griglie spaziali o altre strutture di partizione
IA e pathfinding aggiornati a ogni fotogramma Distribuire i calcoli su più fotogrammi
Logica su oggetti fuori schermo Sospendere o semplificare gli oggetti lontani
Caricamenti durante il gioco Caricare in anticipo o in background
Texture troppo grandi Dimensioni adeguate all'uso reale sullo schermo

Livelli di dettaglio

Ciò che è lontano o piccolo sullo schermo può essere più semplice. Nei giochi 3D si usano modelli con meno poligoni a distanza; in generale si possono ridurre la frequenza di aggiornamento delle animazioni lontane, il numero di particelle o la qualità delle ombre. Il giocatore non nota la differenza, il processore sì.

Ottimizzare con giudizio

L'ottimizzazione prematura può rendere il codice complicato senza un beneficio reale. Un buon equilibrio è scrivere codice chiaro, evitare gli errori evidenti come allocazioni continue nel ciclo principale, e ottimizzare in modo mirato quando le misure lo indicano. Conviene inoltre provare il gioco presto sull'hardware più modesto tra quelli che si vogliono supportare, non solo sulla macchina di sviluppo.

In sintesi

  • Il budget di un fotogramma si misura in millisecondi: circa 16,7 ms a 60 fps.
  • Regolarità e assenza di picchi contano quanto la media.
  • Il profiler indica dove intervenire; l'intuizione da sola spesso sbaglia.
  • Batching e atlanti riducono le draw call; l'object pooling elimina allocazioni e pause.
  • Livelli di dettaglio e calcoli distribuiti alleggeriscono il lavoro senza effetti visibili.
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