#3 Costruisci una prigione con il mouse, e stanze che si trovano da sole
· Nick
La seconda tappa è conclusa e, per la prima volta, c’è qualcosa da guardare. Il client Godot ora poggia sul nucleo della simulazione, e con il mouse puoi costruire una prigione a un solo piano: posare pavimenti, tracciare muri, mettere porte, segnare stanze, demolire. Tutto è fatto di cubi semplici e colori piatti, e dentro non c’è ancora nessuno. Ma è un luogo, e puoi girarci intorno con la telecamera.
Che cosa si può fare ora
Avvii una nuova partita, scegli la dimensione della mappa (64, 128 o 256 tessere per lato) e un seed facoltativo, e ottieni una griglia vuota. La telecamera è ortografica con un’inclinazione fissa. Ruota a passi rigorosi di 90°, fa zoom verso il cursore, si sposta con i tasti o con il pulsante centrale e ha una modalità dall’alto per costruire. Con una telecamera libera di ruotare, i muri nasconderebbero le stanze; per questo non lo è.
Pavimenti e demolizioni prendono un rettangolo. I muri seguono un contorno (o una linea retta se tieni premuto Maiusc). Una porta è un clic, e sceglie la direzione dai muri vicini, con un tasto per girarla. Mentre trascini, l’area sotto il cursore appare come un fantasma: le tessere verdi verranno costruite, quelle rosse saltate, e un’etichetta dice quante e perché. Nulla viene inviato al mondo finché non rilasci, e se il nucleo dice che nemmeno una tessera cambierebbe, non viene inviato niente. Il clic destro o Esc annullano.
Nel modello del mondo ci sono più piani, quindi c’è un selettore di piano, e i piani sopra quello attuale sono nascosti. Le stanze hanno uno strumento proprio, trattato più sotto. Un overlay di debug mostra i bordi dei chunk, le tessere percorribili, la tessera sotto il cursore e un pannello dei tempi. Ogni parola dell’interfaccia è in inglese.
L’anteprima è quella vera
Un fantasma che non corrisponde al risultato è peggio di nessun fantasma. Per questo il client non indovina cosa farà un comando di costruzione. Il nucleo sa rispondere a “che cosa succederebbe se eseguissi questo” per la stessa area, tessera per tessera, con un motivo per ogni tessera che rifiuterebbe. Il controllo che riempie l’anteprima è la stessa regola che il comando usa quando viene eseguito davvero, quindi le due non possono divergere. Il client si limita a disegnare la risposta. Interroga di nuovo il nucleo solo quando cambia qualcosa che influisce sulla risposta: lo strumento, l’area trascinata, il piano o la mappa stessa.
Lo stesso principio plasma il resto del client: legge lo stato del mondo e invia comandi, e non ha una propria logica di simulazione. Per ora il nucleo esegue i tick sul thread principale, e un comando generato da un clic viene applicato all’inizio del frame successivo, prima che il mondo faccia il suo tick. Se i tempi dei tick dovessero cominciare a impennarsi quando il mondo sarà pieno di gente, il piano è spostare il nucleo su un thread proprio, con una soglia misurata per decidere quando farlo.
Stanze che si trovano da sole
Ho valutato due modi di creare le stanze. Prison Architect ti lascia disegnare una zona, che diventa un oggetto con le proprie tessere. È facile da capire, ma in una prigione va male: un intero blocco di celle sarebbe un’unica zona, e dopo aver ricostruito un muro bisogna ridipingerla. L’altro modo è trovare automaticamente le aree chiuse, ma allora non puoi segnare un cortile privo di muri intorno.
Ho scelto un misto. L’unica cosa memorizzata è un tipo di stanza su ogni tessera: cella, mensa, bagni, infermeria, sala colloqui, cortile per l’ora d’aria e così via. Tutto il resto si ricava dai muri. Uno spazio è un gruppo di tessere aperte unite per i lati, sullo stesso livello, ed è chiuso se non tocca il bordo della mappa né un vuoto. Una stanza è l’insieme delle tessere di un tipo all’interno di uno spazio. Muri e porte sono confini, e un tipo dipinto sotto un muro viene conservato ma ignorato, quindi ritorna se abbatti il muro.
Ne risulta un comportamento che sembra giusto senza bisogno di spiegazioni. Metti un muro in mezzo a una stanza e diventa due stanze dello stesso tipo. Togli il muro e si fondono. Metti una porta dove c’era un muro e non cambia nulla. Una stanza non valida non viene cancellata, perché sarebbe l’opposto di “senza dolore”; viene mostrata in rosso con i motivi: non chiusa, condivisa con un altro tipo, troppo piccola o senza porta. Puoi dipingere un rettangolo, riempire con un clic un intero spazio chiuso con un unico tipo, oppure cancellarlo.
L’elenco delle stanze è un dato derivato, quindi non viene salvato né entra nell’hash del mondo; ne fa parte solo il tipo su ogni tessera. Viene ricostruito dopo i comandi e dopo i sistemi, e controllare un mondo invariato non costa nulla. In questa tappa il livello modificato viene ricalcolato per intero, il che richiede circa mezzo millisecondo su una mappa 256×256; farlo chunk per chunk può aspettare finché le regioni del pathfinding non richiederanno lo stesso meccanismo. La regola a cui lo sottopongo è che una ricostruzione incrementale deve dare lo stesso risultato di una completa.
Ricostruire solo ciò che è cambiato, sezionare i piani gratis
La mappa è disegnata come una mesh per chunk per livello, non come un oggetto per muro. Ogni chunk ha un contatore di versione nel nucleo, e il renderer ricorda quale versione ha disegnato per ultima. Quando il mondo cambia, solo i chunk con una versione più recente entrano in una coda, e la coda viene smaltita con un budget di tempo per frame, così un cambiamento enorme si distribuisce su qualche frame invece di bloccarne uno solo. Dipingere un tipo di stanza non fa avanzare la versione della mappa, quindi non risveglia mai il costruttore di mesh.
La sezione per piano segue la stessa idea di fare meno. I piani sopra quello attuale sono semplicemente nascosti. I muri del piano attuale vengono schiacciati a un metro da uno shader, con un materiale condiviso e un unico valore di altezza, quindi cambiare piano o attivare i muri interi non ricostruisce nulla. Passare attraverso quattro livelli comporta zero ricostruzioni di mesh.
Uno spazio di dieci centimetri
Quando ho provato la prima prigione a un piano nel controllo finale, c’era un pavimento al livello di sopra, e di lato si vedeva una sottile linea chiara tra la cima di un muro e la lastra sopra. In un angolo si vedeva il pavimento sospeso nel vuoto. Ho sospettato la sezione per piano, poi lo z-fighting, poi lo smoothing.
Nessuno dei tre. Il pavimento era un singolo quad piatto senza spessore, disegnato all’altezza del suo livello più dieci centimetri. I muri del livello sotto arrivavano esattamente a tre metri. Il pavimento del livello successivo iniziava a tre metri e dieci. Lo spazio era largo dieci centimetri, e attraverso di esso si vedeva la faccia superiore illuminata del muro. In orizzontale tutto era allineato alla perfezione, ed ecco perché ci ho messo un po' ad accorgermene.
La correzione è che ora un pavimento è una lastra con uno spessore vero, che scende fino all’inizio del suo livello. La parte superiore resta alla stessa altezza, quindi la selezione delle tessere con il mouse e tutti gli overlay restano invariati. Le facce laterali vengono saltate dove un pavimento o un muro si toccano all’interno dello stesso chunk e disegnate ai bordi dei chunk, così la mesh di un chunk non dipende mai dai vicini. Questo lascia un bordo scuro di dieci centimetri all’estremità della lastra. Ho guardato gli screenshot di prima e dopo, e ho deciso di tenerlo. Si legge come il bordo di un pavimento, che è ciò che è.
Regge?
L’ultimo compito della tappa era costruire con il mouse una prigione a un piano e vedere se faceva male. Per lo più no. Quello che la prova ha trovato oltre allo spazio qui sopra era piccolo e fastidioso: i pulsanti degli strumenti non perdevano l’evidenziazione quando sceglievi un altro strumento con il mouse, e non c’era modo di uscire dalla finestra Nuova partita con Esc. Entrambi sono corretti. Un menu di pausa è nell’elenco delle idee per più avanti.
Prossimi passi
Ora tocca a M2, il movimento e la routine quotidiana. Il mondo riceve regioni e raggiungibilità, un pathfinding che funziona tra i piani e flow field per le folle. Il nucleo avrà finalmente un lavoro vero a ogni tick, quindi la soglia del benchmark smetterà di dormire. Vedi la roadmap.