Scopri il potenziale di WebGL con lo scenario infinito e procedurale di questo casual game di guida.
Slow Roads è un gioco di guida casual incentrato su scenografie infinitamente procedurali, il tutto ospitato nel browser come applicazione WebGL. Per molti, un'esperienza così intensa potrebbe sembrare fuori luogo nel contesto limitato del browser e, infatti, correggere questo atteggiamento è stato uno dei miei obiettivi per questo progetto. In questo articolo illustrerò alcune delle tecniche che ho usato per superare gli ostacoli prestazionali nella mia missione di evidenziare il potenziale spesso trascurato del 3D sul web.
Sviluppo 3D nel browser
Dopo aver rilasciato Slow Roads, ho visto un commento ricorrente nel feedback: "Non sapevo che questo fosse possibile nel browser". Se condividi questo sentiment, non sei di certo una minoranza; secondo il sondaggio 2022 State of JS, circa l'80% degli sviluppatori non ha ancora provato WebGL. È un peccato che possa trascurare tantissime potenzialità, soprattutto quando si tratta di giochi basati su browser. Con Slow Roads, spero di portare WebGL ulteriormente sotto i riflettori e forse di ridurre il numero di sviluppatori che si oppone alla frase "motore di giochi JavaScript ad alte prestazioni".
WebGL può sembrare misterioso e complesso per molti, ma negli ultimi anni i suoi ecosistemi di sviluppo si sono sviluppati molto in librerie e strumenti pratici e pratici. Per gli sviluppatori front-end ora è più facile che mai incorporare l'UX 3D nel loro lavoro, anche senza esperienza nella grafica computerizzata. Three.js, la libreria WebGL principale, è la base di molte espansioni, tra cui react-tre-fiber, che inserisce componenti 3D nel framework di React. Ora ci sono anche editor di giochi completi basati sul web, come Babylon.js o PlayCanvas, che offrono un'interfaccia familiare e toolchain integrate.
Nonostante la straordinaria utilità di queste librerie, tuttavia, i progetti ambiziosi sono in definitiva vincolati da limitazioni tecniche. Gli scettici sull'idea di giochi basati su browser potrebbero evidenziare che JavaScript è a thread unico e con risorse limitate. Tuttavia, l'esplorazione di queste limitazioni sblocca il valore nascosto: nessun'altra piattaforma offre le stesse funzioni di accessibilità istantanea e compatibilità di massa consentite dal browser. Gli utenti di qualsiasi sistema compatibile con il browser possono iniziare a giocare con un clic, senza dover installare applicazioni o accedere ai servizi. Per non parlare del fatto che gli sviluppatori apprezzano l'elegante comodità di disporre di solidi framework front-end per la creazione di UI o per la gestione del networking per le modalità multiplayer. Questi valori, a mio avviso, sono ciò che rende il browser una piattaforma eccellente sia per i giocatori che per gli sviluppatori e, come dimostrato da Slow Roads, i limiti tecnici potrebbero spesso essere riducibili a un problema di progettazione.
Ottenere prestazioni fluide nelle strade lente
Poiché tra gli elementi fondamentali di Slow Roads ci sono movimenti ad alta velocità e creazione di scenari costosi, la necessità di prestazioni fluide ha sottolineato ogni mia decisione di progettazione. La mia strategia principale era iniziare con un design del gameplay essenziale che consentisse di scegliere scorciatoie contestuali all'interno dell'architettura del motore. Lo svantaggio di questo approccio è rinunciare ad alcune utili funzionalità nella ricerca del minimalismo, ma si traduce in un sistema su misura e iper-ottimizzato che funziona bene su diversi browser e dispositivi.
Di seguito un'analisi dei componenti chiave che mantengono snelle le strade lente.
Modellare il motore ambientale intorno al gameplay
Il motore di generazione dell'ambiente, il componente chiave del gioco, è inevitabilmente costoso, in quanto prende giustificatamente la maggior parte dei budget per memoria e calcolo. Il trucco usato qui è la pianificazione e la distribuzione del calcolo intensivo in un determinato periodo di tempo, in modo da non interrompere la frequenza fotogrammi con picchi di prestazioni.
L'ambiente è composto da riquadri di geometria, che differiscono per dimensioni e risoluzione (classificati come "livelli di dettaglio" o LoD) a seconda di quanto saranno vicini alla videocamera. Nei giochi tipici con una videocamera in movimento, diversi LoD devono essere costantemente caricati e scaricati per descrivere l'ambiente circostante del giocatore ovunque si trovino. Si tratta di un'operazione costosa e dispendiosa, soprattutto quando l'ambiente stesso viene generato dinamicamente. Fortunatamente, questa convenzione può essere completamente sovvertita nelle strade lente grazie all'aspettativa contestuale che l'utente debba rimanere sulla strada. Invece, la geometria molto dettagliata può essere riservata al corridoio stretto che fiancheggia direttamente il percorso.
![Un diagramma che mostra come la generazione della strada con largo anticipo possa consentire una pianificazione proattiva e la memorizzazione nella cache della generazione dell'ambiente.](https://web.dev/static/case-studies/slow-roads/image/a-diagram-showing-genera-8bc6e65c0bba1.png?authuser=0000&hl=it)
La linea intermedia della strada stessa viene generata molto prima dell'arrivo del giocatore, consentendo una previsione accurata di quando e dove saranno necessari dettagli sull'ambiente. Il risultato è un sistema sottile che può pianificare in modo proattivo lavori costosi, generando solo il minimo necessario in ogni momento, senza sprechi di lavoro per dettagli che non saranno visibili. Questa tecnica è possibile solo perché la strada è un unico percorso senza rami, un buon esempio di compromessi del gameplay basati su scorciatoie architettoniche.
![Un diagramma che mostra come la generazione della strada con largo anticipo possa consentire una pianificazione proattiva e la memorizzazione nella cache della generazione dell'ambiente.](https://web.dev/static/case-studies/slow-roads/image/a-diagram-showing-genera-a22a99a5764f2.png?authuser=0000&hl=it)
Essere esigente con le leggi della fisica
In secondo luogo alla domanda computazionale del motore dell'ambiente c'è la simulazione fisica. Slow Roads usa un motore fisico personalizzato, minimalista, che sfrutta ogni scorciatoia disponibile.
Il maggiore risparmio in questo caso è evitare di simulare troppi oggetti, puntando sul contesto zen minimo, sminuendo elementi come le collisioni dinamiche e gli oggetti distruttili. L'ipotesi che il veicolo rimanga sulla strada significa che le collisioni con oggetti fuoristrada possono essere ragionevolmente ignorate. Inoltre, la codifica della strada come linea intermedia sparsa consente di ottenere trucchi eleganti per il rilevamento rapido delle collisioni con il manto stradale e i guard rail, il tutto basato su un controllo della distanza dal centro della strada. La guida fuoristrada diventa quindi più costosa, ma questo è un altro esempio di un compromesso equo adatto al contesto del gameplay.
Gestione dell'utilizzo della memoria
Essendo un'altra risorsa limitata dal browser, è importante gestire la memoria con attenzione, a prescindere dal fatto che JavaScript sia garbage collection. Può essere facilmente trascurato, ma dichiarare anche piccole quantità di nuova memoria all'interno di un ciclo di gioco può causare problemi significativi quando si esegue l'esecuzione a 60 Hz. Oltre a consumare risorse dell'utente in un contesto in cui è probabilmente multitasking, le garbage collection di grandi dimensioni possono richiedere diversi frame per il completamento, causando notevoli interruzioni. Per evitare che ciò accada, la memoria loop può essere preallocata nelle variabili di classe al momento dell'inizializzazione e riciclata in ogni frame.
![Una visualizzazione prima e dopo del profilo di memoria durante l'ottimizzazione del codebase Slow Roads, che indica risparmi significativi e una riduzione della percentuale di garbage collection.](https://web.dev/static/case-studies/slow-roads/image/a-before-after-view-the-f870ec7d3c94f.png?authuser=0000&hl=it)
È anche molto importante che le strutture di dati più pesanti, come le geometrie e i buffer di dati associati, siano gestite in modo economico. In un gioco infinitamente generato come Slow Roads, la maggior parte della geometria risiede su una sorta di tapis roulant: una volta che un vecchio pezzo cade indietro in lontananza, le sue strutture di dati possono essere memorizzate e riciclate di nuovo per un nuovo pezzo del mondo, un modello di progettazione noto come pooling di oggetti.
Queste pratiche aiutano a dare priorità all'esecuzione snella, sacrificando la semplicità del codice. In contesti con prestazioni elevate, è importante fare attenzione a come le funzionalità di convenienza possono a volte prendere in prestito dal cliente a vantaggio dello sviluppatore. Ad esempio, metodi
come Object.keys()
o Array.map()
sono incredibilmente utili, ma è facile trascurare che ognuno crea un nuovo array per il valore
restituito. Comprendere i meccanismi interni di queste black-box può aiutarti a perfezionare il tuo codice ed evitare i piccoli successi delle prestazioni.
Ridurre il tempo di caricamento con gli asset generati proceduralmente
Sebbene le prestazioni del runtime siano la principale preoccupazione per gli sviluppatori di giochi, i consueti assiomi relativi al tempo di caricamento iniziale delle pagine web rimangono validi. Gli utenti potrebbero essere più tolleranti quando accedono consapevolmente a contenuti di grandi dimensioni, ma tempi di caricamento lunghi possono comunque compromettere l'esperienza, se non la fidelizzazione degli utenti. I giochi richiedono spesso asset di grandi dimensioni, sotto forma di texture, suoni e modelli 3D, che devono essere quanto meno compressi ove si possano evitare i dettagli.
Al contrario, la generazione procedurale delle risorse sul cliente può evitare in primo luogo trasferimenti lunghi. Si tratta di un vantaggio enorme per gli utenti con connessioni lente e offre allo sviluppatore un maggiore controllo diretto su come è strutturato il gioco, non solo per la fase di caricamento iniziale, ma anche per adattare i livelli di dettagli a diverse impostazioni di qualità.
La maggior parte della geometria di Slow Roads è generata proceduralmente e semplicistica, con gli Shaker personalizzati che combinano più texture per esaltare i dettagli. Lo svantaggio è che queste texture possono essere asset pesanti, anche se in questo caso ci sono ulteriori opportunità di risparmio, con metodi come le texture stocastiche in grado di ottenere maggiori dettagli da piccole texture di origine. A un livello estremo, è anche possibile generare texture interamente sul client con strumenti come texgen.js. Lo stesso vale anche per l'audio, con l'API Web Audio che consente la generazione del suono con nodi audio.
Grazie al vantaggio delle risorse procedurali, la generazione dell'ambiente iniziale richiede in media solo 3,2 secondi. Per sfruttare al meglio le dimensioni di download iniziali ridotte, una semplice schermata iniziale accoglie i nuovi visitatori e posticipa la costosa inizializzazione della scena fino a dopo la pressione di un pulsante di conferma. Inoltre, funge da comodo buffer per le sessioni con rimbalzo, riducendo al minimo gli sprechi di trasferimento degli asset caricati dinamicamente.
Adozione di un approccio agile all'ottimizzazione tardiva
Ho sempre considerato sperimentale il codebase di Slow Roads e, come tale, ho adottato un approccio allo sviluppo estremamente agile. Quando si lavora con un'architettura di sistema complessa e in rapida evoluzione, può essere difficile prevedere dove potrebbero verificarsi i colli di bottiglia importanti. L'attenzione dovrebbe essere concentrata sull'implementazione rapida delle funzionalità desiderate, anziché sull'esecuzione pulita, e sul lavoro a ritroso per ottimizzare i sistemi dove sono più importanti. Il profiler delle prestazioni in Chrome DevTools è inestimabile per questo passaggio e mi ha aiutato a diagnosticare alcuni problemi importanti con le versioni precedenti del gioco. Il tuo tempo come sviluppatore è prezioso, quindi assicurati di non perdere tempo a pensare a problemi che possono rivelarsi insignificanti o ridondanti.
Monitorare l'esperienza utente
Quando implementi tutti questi trucchi, è importante assicurarsi che il gioco funzioni come previsto in natura. Offrire una vasta gamma di funzionalità hardware è un aspetto fondamentale di qualsiasi sviluppo di giochi, ma i giochi web possono avere come target uno spettro molto più ampio e comprendono sia computer di fascia alta che dispositivi mobili vecchi di dieci anni. Il modo più semplice per affrontare questo problema consiste nell'offrire le impostazioni per adattare i colli di bottiglia più probabili nel tuo codebase, per attività ad alta intensità di GPU e CPU.
La profilazione sulla tua macchina può coprire solo molto, tuttavia, è importante chiudere il ciclo di feedback con gli utenti in qualche modo. Per le strade lente eseguo semplici analisi che generano report sul rendimento insieme a fattori contestuali come la risoluzione dello schermo. Queste analisi vengono inviate al backend di base dei nodi utilizzando socket.io, insieme all'eventuale feedback scritto inviato dall'utente tramite il modulo in-game. All'inizio, queste analisi hanno individuato molti problemi importanti che potrebbero essere mitigati con semplici modifiche all'esperienza utente, ad esempio mettendo in evidenza il menu delle impostazioni quando viene rilevato un FPS costantemente basso o avvisando che un utente potrebbe dover abilitare l'accelerazione hardware se le prestazioni sono particolarmente scarse.
La lentezza delle strade più avanti
Anche dopo aver adottato tutte queste misure, rimane una parte significativa della base di giocatori che deve giocare con impostazioni inferiori, principalmente quelle che utilizzano dispositivi leggeri a cui manca una GPU. Sebbene la gamma di impostazioni di qualità disponibili porti a una distribuzione delle prestazioni abbastanza uniforme, solo il 52% dei giocatori supera i 55 FPS.
![Una matrice definita dall'impostazione della distanza di visualizzazione rispetto all'impostazione dei dettagli, che mostra la media di fotogrammi al secondo ottenuti con diverse accoppiamenti. La distribuzione è distribuita in modo abbastanza uniforme tra 45 e 60, dove 60 rappresenta l'obiettivo per una buona performance. Gli utenti con impostazioni basse tendono a vedere un FPS inferiore rispetto a quelli con impostazioni elevate, evidenziando le differenze nelle funzionalità hardware del client.](https://web.dev/static/case-studies/slow-roads/image/a-matrix-defined-view-di-5c73d8d72469c.png?authuser=0000&hl=it)
Fortunatamente, esistono ancora molte opportunità per risparmiare sul rendimento. Oltre ad aggiungere ulteriori trucchi di rendering per ridurre la domanda di GPU, spero di sperimentare con i web worker per caricare in contemporanea la generazione dell'ambiente a breve termine e alla fine potrebbe essere necessario incorporare WASM o WebGPU nel codebase. Qualsiasi margine di miglioramento mi permetterà di creare ambienti più elaborati e diversificati, che sarà l'obiettivo duraturo per il resto del progetto.
Con lo sviluppo di progetti hobby, Slow Roads si è rivelato un modo estremamente appagante per dimostrare quanto possano essere sorprendentemente elaborati, performanti e popolari i giochi per browser. Se sono riuscito a stuzzicare il tuo interesse per WebGL, sappi che le strade lente tecnologicamente sono un esempio piuttosto superficiale delle sue funzionalità complete. Consiglio vivamente ai lettori di esplorare la vetrina Three.js; chi è interessato in particolare allo sviluppo di giochi web è invitato nella community di webgamedev.com.