Fare debug di un software, ovvero trovare gli errori al suo interno, non è un'arte; o meglio, non me la sento di definirla così, però a volte può essere molto complicato. Inoltre, quando si ha a che fare con un videogioco, si passa decisamente a un livello di difficoltà superiore. Molti sviluppatori, come i due tesisti con cui ho discusso pochi giorni fa, a volte non se ne rendono conto. Oggi parliamo quindi di debug: perché è complesso e quali sono le difficoltà extra che bisogna affrontare quando lo si fa con un videogioco.
Due filosofie di pensiero
Partiamo dalle basi. Chi fa debugging si divide grossomodo in due categorie: quelli che usano uno strumento esterno per monitorare il codice (debugger) e quelli che, per pigrizia o per semplicità, aggiungono delle scritte in output riguardo a ciò che il programma sta facendo e da lì trovano l’errore.
Non vi nascondo che il secondo approccio (che usiamo tutti prima o poi) è nettamente più facile del primo, ma, paradossalmente, rischia anche di farci perdere molto più tempo alla fine della giornata di lavoro.
I debugger
Un debugger è un software che ha la capacità di prendere il controllo di un programma in esecuzione (un processo) e di sospenderlo, bloccandone l’esecuzione. Una volta fermato, siamo in grado, attraverso il debugger, di osservare tutto lo stato del processo: dalla sua memoria, ai file che ha aperto, fino al valore di ogni singola variabile. Quando siamo soddisfatti, possiamo far ripartire l’esecuzione per bloccarla nuovamente nel momento in cui lo riteniamo necessario.
Usare (e scegliere) un debugger non è la cosa più semplice del mondo, anche perché ne esistono moltissimi, specializzati in aspetti diversi del software che stiamo analizzando.
Ci sono debugger che ci mostrano direttamente il linguaggio assembly e i registri, come WinDbg (per Windows) e GDB (per Linux), ma vengono per lo più usati per carpire informazioni o fare reverse engineering (chiamiamolo così). Hanno relativamente poca utilità se il nostro codice si colloca a un alto livello di astrazione, ad esempio dietro strati e strati di librerie di un game engine. Alcuni, come GDB, se è disponibile il codice sorgente, riescono a includere anche quello nella visualizzazione.
Ci sono poi debugger che aggiungono un’interfaccia grafica e permettono di interagire con il codice, ad esempio inserendo dei punti di interruzione (breakpoint). A volte implementano un proprio sistema di debugging, altre volte estendono le funzionalità di un debugger più essenziale. Ad esempio, DDD (Data Display Debugger) è un’interfaccia grafica costruita sopra GDB ed è forse anche uno dei debugger più potenti e versatili che abbiamo disponibili oggi.

Questo tipo di strumenti è molto versatile, perché può essere utilizzato con qualsiasi eseguibile e con decine di linguaggi, ma è anche piuttosto scomodo da usare. Innanzitutto, il programma va compilato includendo i simboli per il debugging dove il debugger si “aggrappa” per controllarlo; quindi, ciò che analizziamo non sarà il prodotto finale: equivalente, sì, ma non identico. In secondo luogo, sono strumenti esterni a ciò che usiamo per sviluppare. Questo non deve meravigliarci, perché prima che gli ambienti di sviluppo integrati (IDE) prendessero piede, il ciclo di sviluppo del software prevedeva l’uso di un editor (di testo, come Vi o Emacs), poi la compilazione separata (da linea di comando) e infine l’esecuzione (magari con un debugger). Ancora oggi esistono strumenti che non si sono evoluti, perché in alcune situazioni sono indispensabili, ad esempio per il debugging di un device driver o di una funzionalità del kernel di un sistema operativo.
Gli ambienti integrati (come Visual Studio, Visual Studio Code e Rider) sono soluzioni che oggi ci permettono di fare tutto all’interno di un solo strumento: scrivere il codice, compilarlo e fare debug. Attenzione però: l’esecuzione vera e propria del codice viene sempre demandata all’esterno, al sistema operativo o, nel nostro caso, a un game engine. È quindi necessario che il nostro IDE si integri (bene) con il game engine che stiamo utilizzando.
Due piccole note per chiudere il discorso. Innanzitutto, non confondete mai un debugger con interfaccia grafica con un IDE, perché al primo mancano l’editor e il compilatore. In secondo luogo, se non avete mai sentito parlare di Vi ed Emacs, è perché siete giovani, quindi siatene felici 😃.
Debugger e game engine
Per usare il debugger di un IDE in un game engine, occorre costruire una sorta di collegamento tra i due. Sono quindi necessari dei plugin da entrambe le parti per definire una lingua comune con cui comunicare.
In Unity, ad esempio, dobbiamo accedere al pannello del Package Manager e installare il plugin corretto rispetto all’IDE.
Viceversa, dentro l’IDE, dovremo assicurarci di aver installato i giusti moduli per parlare con il game engine che stiamo utilizzando.
Una volta fatto questo passo, possiamo dire che esistono i presupposti di base, ma è ancora necessario avviare il sistema. Nel senso che bisogna indicare all’IDE di agganciarsi all’editor del game engine, perché quello sarà il processo da controllare.
A questo punto, il gioco è fatto. Impostiamo i breakpoint che ci servono, avviamo il gioco dall’interno dell’engine e, quando il breakpoint viene raggiunto, il game engine va in pausa e possiamo chiedere al debugger il valore delle variabili, qual è lo stack delle chiamate e molte altre cose. Da questo momento in poi, è diverso dal fare il debug di un'applicazione standard.
Quale debugger scegliere
In realtà, a meno che voi non abbiate necessità strane dal punto di vista delle applicazioni, le funzionalità sono le stesse per tutti. Diciamo piuttosto che la domanda da farsi è quale IDE scegliere. Perché alla fine, tutto il vostro tempo lo passerete lì dentro e deve essere una buona esperienza per voi. Per cui, scegliete quello con cui vi trovate meglio.
Se chiedete a me, la mia scelta per il gamedev è Visual Studio. Microsoft non le fa tutte giuste (e lo dico spesso), ma sui linguaggi di programmazione e gli ambienti di sviluppo, secondo me, non ha rivali. Visual Studio Code, per mia esperienza, ha qualche problema con l’intellisense (l’autocompletamento) applicato alle librerie degli engine e non mi trovo bene con l’interfaccia di debug (questione di gusto personale). Infine, Rider di JetBrains è molto bello e completo come interfaccia; per alcuni aspetti anche più di Visual Studio; tuttavia, si prende troppe iniziative ed è molto intrusivo su come si scrive il codice. Va bene gli standard, ma se il codice deve per forza essere come vuole lui, allora che se lo scriva da solo! 😡
Il mio consiglio per voi: provateli tutti e tre (tanto prevedono tutti una forma di licenza gratuita per uso personale) per almeno una settimana e poi decidete quello che fa per voi.
Print debugging
Chiamato anche console logging, è, come dicevo all’inizio, un modo sbrigativo per sapere il valore delle variabili mentre il programma viene eseguito. Dopo quello che vi ho detto, credo sia chiaro che un debugger fa molto di più di questo. Tuttavia, ci sono delle situazioni in cui sapere il valore delle variabili può bastarci. Ad esempio, perché stiamo facendo un prototipo e abbiamo poco tempo. Io stesso lo faccio spesso quando sto scrivendo uno script di shell o sto facendo una prova con Python per capire che cosa sta succedendo.
La tecnica è molto semplice (basta un “print” o qualcosa di simile), per cui non starò a discuterla. Quello di cui però vorrei discutere con voi è il perché, secondo la mia esperienza, usarla in progetti complessi è una pessima idea.
Serve una console
Ecco, il primo punto è che serve qualcosa su cui visualizzare l’output. E questo, se stiamo sviluppando un videogioco, non è per nulla scontato. Quanti giochi moderni conoscete che funzionano all’interno di un terminale a caratteri? (a parte Stone Story RPG, che consiglio a tutti di mettere in wishlist)
Abbiamo quindi due opzioni: usare il sistema di logging dell’engine oppure inventarci una console.
Il sistema di logging dell’engine è facilmente accessibile ma, purtroppo, prevede uno scambio di messaggi con un servizio interno che, a seconda della complessità del gioco, potrebbe introdurre dei ritardi. Quindi, il momento in cui vediamo una scritta potrebbe non coincidere con l’istante in cui l’evento si è verificato. Inoltre, almeno su Unity, ci sono situazioni in cui un uso intensivo della console sovraccarica la CPU, causando un calo del framerate. Se stiamo facendo debugging proprio per capire come mai il framerate è basso… rischiamo di non andare molto lontano.
Se cerchiamo un meccanismo alternativo, possiamo utilizzare una libreria esterna che gestisca una finestra di testo, oppure diventare creativi e inserire degli elementi nell’interfaccia grafica per monitorare le variabili. La cosa funziona, ma spesso il risultato è simile a quello che si ottiene premendo F3 mentre si gioca a Minecraft.
Non è bellissimo, ma serve allo scopo. Tuttavia, ci obbliga a scrivere del codice aggiuntivo per gestire l’interfaccia, cosa di cui potremmo fare volentieri a meno.
Il codice va ricompilato
Diversamente da un debugger, dove abbiamo a disposizione tutte le informazioni possibili, se usiamo console logging vediamo solo ciò che pensavamo fosse utile. E se scopriamo che manca qualcosa? Dobbiamo modificare il codice, ricompilare tutto e ripartire da capo. Quanto tempo impiegheremo?
È facile immaginare che, se si tratta di uno script di un centinaio di righe di codice, l'impatto sulla produttività sia limitato; se invece stiamo lavorando a un FPS, l'impatto sulla nostra produttività è completamente diverso.
E poi chi lo spegne?
Un sistema di debugging è, per sua natura, uno strumento che vogliamo attivare e disattivare quando serve. Se dobbiamo fare una demo, forse non è il caso che si vedano anche tutte le nostre informazioni private di sviluppo. Se ci sono centinaia di print() sparsi per il codice sorgente, la situazione diventa problematica. Una possibile soluzione è scrivere una cosa del tipo:
…
if (debug) {
print(variabile)
}
…
Dove debug è una variabile booleana globale che, se impostata su falso, disabilita tutte le scritte.
Il problema, però, è che questa istruzione occupa spazio e viene comunque valutata durante l'esecuzione del gioco. Se non vi sembra avere un grande impatto, provate a eseguirla diecimila volte in un minuto e poi ne riparliamo.
Questo, a essere onesti, non è sempre vero: in alcuni linguaggi moderni come Java e C#, se debug è etichettata come costante ed è falsa, allora, in fase di ottimizzazione, il compilatore ignora l'intero statement. Tuttavia, dipende dal linguaggio e dal compilatore.
La cosa, tuttavia, non termina qui, perché quando il gioco è finito tutte le istruzioni funzionali al debug dovrebbero (il condizionale è d’obbligo) essere rimosse.
E se ne dimenticassimo qualcuna? Oppure, se togliendo le istruzioni commettessimo un errore e introducessimo un bug?
Se proprio lo vogliamo fare, anche in questo caso definire una variabile globale per l’attivazione del debug ci può venire un po’ in aiuto. Basta togliere la sua definizione e poi risolvere, uno per uno, tutti gli errori di compilazione che sono comparsi.
A cosa NON server il debugger
Il sistema di debugging, se usato bene, è il miglior coltellino svizzero che si possa desiderare. Tuttavia, non è la soluzione a tutti i nostri mali, perché è in grado di farci capire che cosa stia succedendo, ma il perché stia succedendo continua a essere un problema nostro.
Ad esempio, se osserviamo un basso valore di framerate, un debugger di solito non è in grado di dirci dove si verificano i rallentamenti, perché il codice è corretto ma lo abbiamo progettato male. Per questo tipo di valutazioni, dobbiamo invece usare un profiler. Un profiler misura quanto tempo l’engine impiega a svolgere ogni singola attività o a eseguire una determinata parte del codice. Questo strumento non analizza il codice, ma solo l’eseguibile; pertanto, non lo troveremo integrato nell’ambiente di sviluppo, ma solo all’interno dell’engine.
E con questo, anche questa settimana abbiamo concluso. L’appuntamento è per lunedì prossimo, per continuare a parlare di gamedev.
Nel frattempo, se anche voi avete deciso di diventare i migliori amici del vostro debugger, iscrivetevi alla nostra newsletter per scoprire se noi andremo d’accordo con il nostro. Perché, si sa, anche con gli amici si litiga.
Happy hacking!