Omar Game Developer
Programmazione Difensiva nei Videogiochi

Programmazione Difensiva nei Videogiochi

tips programming gamedev

“Programmazione difensiva” non è altro che un modo di progettare software in modo tale che vengano prese in considerazione quante più circostanze impreviste possibili, permettendo al software di comportarsi correttamente e al programma di non bloccarsi mai.

Tuttavia, qui iniziamo già con i problemi perché cos’è “comportarsi correttamente” nel contesto dei videogiochi?

Prima di scendere nella tana del coniglio, un breve disclaimer: non includerò la gestione delle eccezioni (alcuni la mettono come una forma separata di programmazione difensiva, altri la includono come un tutto) semplicemente perché nei videogiochi non usiamo quelle strutture di controllo, fondamentalmente per una questione di prestazioni (tutte quelle strutture sono buchi neri per le risorse) e non dobbiamo mai perdere di vista il fatto che in questo ambito non basta farlo bene… dobbiamo farlo con le migliori prestazioni possibili. Non mi riferirò nemmeno al caso particolare della programmazione difensiva nel contesto della sicurezza nei giochi multiplayer (perché dà luogo a un argomento separato e perché il concetto sottostante che voglio discutere è totalmente diverso).

Quindi torniamo a cos’è “comportarsi correttamente”? Vediamo 2 esempi con codice (in GDScript)

1° esempio:

#Enemy script
...
var target: Player
...
func _process(delta: float) -> void:
    look_at(target.global_position)

2° esempio:

#Player script
...
export var projectile_class: Projectile = null
...
func shoot() -> void:
    var new_projectile:Projectile = projectile_class.instance()
    add_child(new_projectile)

Nel primo script abbiamo un nemico che ruota costantemente (frame per frame) verso il giocatore (target). Nel secondo script abbiamo un metodo del giocatore che istanzia un oggetto della classe Projectile (quella del proiettile) lo memorizza in una variabile e lo aggiunge al mondo di gioco (add_child).

È chiaro che entrambi gli script hanno modi di fallire, inaspettatamente, e il programma (gioco) si ferma con un errore.

  • Nel primo script: se il giocatore viene distrutto (perdendo una vita, per esempio) il riferimento “target” sarà nullo finché non rinasce di nuovo. Questo darebbe un errore (null reference).

  • Nel secondo script: se per qualche motivo qualcuno ha dimenticato di assegnare il valore a “projectile_class” dall’editor (per quello, l’export, l’equivalente di un SerializeField in Unity o un TSubclassOf con una UPROPERTY in Unreal) quella variabile membro sarà nulla e, di nuovo, avremo un errore.


Qual è la soluzione? Lo so, la prima cosa che stai pensando è: le famose e amate “guard clauses”. Che non sono altro che un controllo delle precondizioni (funzionano molto bene anche con early return).

Proviamo questo: 1° esempio:

#Enemy script
...
func _process(delta: float) -> void:
    if target:
        look_at(target.global_position)

2° esempio:

#Player script
...
func shoot() -> void:
    if projectile_class:
        var new_projectile:Projectile = projectile_class.instance()
        add_child(new_projectile)

Perfetto, se target è nullo (per esempio quando il giocatore viene distrutto) il nemico smetterà di eseguire il codice che causerebbe un errore. Allo stesso modo, se qualcuno ha dimenticato di impostare un valore a projectile_class il giocatore non eseguirà la porzione di codice che causa un errore. Pronti allora? Il gioco si comporta “correttamente” ora? E no… davvero no.

Nel primo caso, , ma nel secondo, cosa succederà quando la persona che gioca preme il pulsante di sparo? Il gioco non si fermerà, ma non verranno sparati colpi. Immagina un FPS in cui non puoi sparare… ora immagina il povero programmatore che cerca di debuggare l’errore (che a prima vista può variare dal proiettile che collide impropriamente con chi lo istanzia, a un problema di binding con gli input, e un mucchio di “forse” nel mezzo). Quest’ultima situazione è CATTIVO codice difensivo, e lo vedi molto.

Perché oso dirlo così assertivamente? Perché per definizione il codice difensivo dice “…comportarsi correttamente…” e non è corretto che il gioco continui a funzionare in quelle circostanze, “se deve rompersi… lascialo rompere” mi piace dire. E se non vuoi credermi sulla parola, prendi quella di Ari Arnbjörnsson (che ne sa un po’ di questo) e in questo video parla di “fallire meglio”

Mi piace anche dire che se sei arrivato al lancio del gioco con un bug del genere, l’ultimo dei tuoi problemi è il codice difensivo (la parte QA di me viene fuori).


Quindi? Ci arrendiamo semplicemente a non difenderci? Certo che no, ci sono sempre modi per migliorare la qualità del nostro codice e del nostro software in generale. Il problema è che non c’è un “proiettile d’argento” perché a volte dipende dal nostro strumento, e capire come funziona. Qui arriva la parte che a molti programmatori non piace: devi leggere la documentazione.

Uno dei modi principali per difendersi correttamente da problemi come quello che ho menzionato è con le asserzioni. Ma queste, ovviamente, non hanno la stessa sintassi in tutti i linguaggi (o nell’API che un motore ci fornisce). Solo per fare un esempio nei 3 motori più popolari

Comunque, lo spirito delle asserzioni è sempre lo stesso, e con la documentazione alla mano possiamo capire esattamente cosa possiamo aspettarci quando le usiamo.


Dato che l’esempio problematico era con codice Godot, torniamo ad esso.

2° esempio:

#Player script
...
func shoot() -> void:
    if assert(projectile_class, "A projectile scene was not loaded"):
        var new_projectile:Projectile = projectile_class.instance()
        add_child(new_projectile)

Se leggiamo la documentazione sapremo che:

  • Se la condizione è falsa viene generato un errore.
  • Se accade quanto sopra avremo un messaggio di errore personalizzato.
  • Quando stiamo eseguendo il gioco dall’editor, si metterà in pausa, ci mostrerà l’errore, ma ci permette di continuare a testare il gioco (continue). Se stiamo eseguendo una versione Debug lancerà l’errore.
  • MOLTO IMPORTANTE: gli assert “costano” quindi, è codice che NON viene eseguito nelle versioni release. Se includiamo codice che ha effetti collaterali (per esempio, un’intera configurazione dipende dal successo dell’asserzione) possiamo avere comportamenti indesiderati. Questo è molto comune con le asserzioni (in Unreal è simile) ed è per questo che la soluzione non è mettere “assert” ovunque. Non c’è bisogno di spaventarsi se nei controlli di asserzione abbiamo “scatti” quando testiamo il gioco nell’editor, come ho detto, sono costosi e si perdono prestazioni, ma nella versione release quel codice non viene eseguito.

Quindi, di fronte alla stessa situazione di prima (qualcuno ha dimenticato di caricare la scena del proiettile) questa volta non avremo un gioco in cui premendo “spara” il personaggio semplicemente non sparerà o il gioco si romperà. Avremo la possibilità che il gioco si fermi automaticamente, mostrandoci un errore molto chiaro ed evidente, ma permettendoci di continuare l’esecuzione nel caso stessimo testando altre cose.

Quello che si chiama “win-win”.


Conclusione: la programmazione difensiva non è un design semplice come dire “metto controlli di precondizione ovunque”, deve essere fatto uno sforzo di design; Tuttavia, il codice risultante è di migliore qualità e (in gran parte) a prova di errore una volta in produzione/rilasciato. Se non abbiamo alcun tipo di programmazione difensiva siamo esposti, il nostro gioco funzionerà correttamente finché non troverà un errore e poi ci troveremo semplicemente di fronte alla situazione più indesiderabile: un errore critico che ferma tutto.

Dobbiamo guardare quali strumenti ci fornisce il linguaggio che stiamo usando per migliorare la nostra programmazione difensiva. Le asserzioni sono molto importanti, ma non sono un martello d’oro e non possiamo usarle indiscriminatamente.