Chiudere una scheda del browser non dovrebbe cancellare quattro ore di progressi. Sembra ovvio, eppure molti giochi per browser trattano il local storage come un elemento secondario. Un giocatore sblocca un punteggio massimo, modifica le impostazioni, torna il giorno dopo e non trova nulla. Peggio ancora, tornano dopo una patch e il gioco restituisce un errore perché il file di salvataggio sulla loro macchina non corrisponde più al codice che hai appena rilasciato. Sviluppare uno shooter in stile survivor con Phaser 4 significa gestire ondate costanti di nemici, ma la vera minaccia a lungo termine sono i tuoi futuri aggiornamenti.
La maggior parte degli sviluppatori crea il proprio primo sistema di salvataggio prendendo un oggetto, passandolo attraverso JSON.stringify e scaricandolo in localStorage. Al caricamento, lo analizzano e lo restituiscono al gioco così com'è. Funziona il primo giorno. Si rompe nel momento in cui aggiungi una nuova impostazione, un nuovo flag di sblocco o un terzo livello di configurazione nidificata. Se un giocatore che ritorna ha un vecchio file di salvataggio privo della proprietà vignette, e il tuo nuovo codice si aspetta che esista, otterrai undefined dove ti aspettavi un booleano. Moltiplica questo per una dozzina di nuove funzionalità e avrai un incubo di debugging che colpirà per primi i tuoi giocatori più fedeli.
Inizia con un contratto, non con un oggetto grezzo
Prima di toccare localStorage, definisci uno schema di salvataggio predefinito nel tuo codebase. Pensalo come un contratto che ogni file di salvataggio deve rispettare, sia che sia stato creato cinque minuti fa o cinque mesi fa. Un punto di partenza chiaro potrebbe essere questo:
const defaultSave = {
highScore: 0,
settings: {
screenShake: true,
vignette: true
}
};
Questo oggetto risiede nel tuo codice sorgente. Quando il gioco si avvia, hai sempre questa struttura a disposizione. Ti fornisce una base di riferimento. Ti costringe anche a pensare alla struttura prima di serializzare qualsiasi cosa. Se salti questo passaggio e ti limiti a memorizzare qualsiasi oggetto di stato sia comodo in quel momento, finirai per avere chiavi incoerenti, campi mancanti e fallimenti silenziosi quando i vecchi salvataggi si sfasano rispetto alle tue aspettative.
Caricamento difensivo con Try/Catch
Il local storage non è un database. È un armadietto di stringhe nel browser, e qualsiasi cosa può finirci dentro. L'utente potrebbe aver modificato manualmente un valore, un'operazione di scrittura interrotta a metà, o un'estensione del browser potrebbe aver scaricato spazzatura nella chiave che hai dichiarato. Quando estrai quella stringa e la passi a JSON.parse, un singolo carattere corrotto genera un'eccezione critica. In un gioco Phaser, quell'errore non gestito può bloccare la sequenza di avvio o riportare il giocatore davanti a una schermata vuota.
Avvolgi sempre la logica di lettura e parsing in un blocco try/catch. In caso di errore, torna allo schema predefinito. L'obiettivo è semplice: se il file di salvataggio non è leggibile, tratta il giocatore come un nuovo utente invece di far crashare l'intera sessione. Questa singola abitudine separa i progetti amatoriali dalle build di livello professionale. Non costa quasi nulla implementarla e ti salva da misteriosi report di bug impossibili da riprodurre.
Unisci i vecchi dati con i valori predefiniti
Un parsing riuscito non significa che sei al sicuro. Non sostituire mai interamente il tuo oggetto predefinito con il risultato del parsing. Quel vecchio file di salvataggio potrebbe non contenere le tue impostazioni più recenti. Potrebbe memorizzare screenShake ma non vignette. Se la logica del tuo gioco assume che vignette esista perché è stata inclusa nell'ultimo aggiornamento, ti ritroverai di nuovo a dare la caccia agli errori undefined.
Invece, unisci i dati caricati con i tuoi valori predefiniti. Usa Object.assign per sovrapporre i valori salvati allo schema di base. I valori predefiniti colmano automaticamente ogni lacuna. Le nuove proprietà aggiunte nella versione due ricevono i loro valori iniziali dall'oggetto predefinito. Le proprietà esistenti che il giocatore ha effettivamente modificato vengono sovrascritte con le sue preferenze memorizzate. Tutti vincono. Il giocatore che ritorna mantiene il suo punteggio massimo e il gioco ottiene l'accesso al nuovo interruttore che hai aggiunto ieri senza andare in crash.
Tieni presente che Object.assign esegue un merge superficiale (shallow merge). Se l'oggetto delle impostazioni diventa profondamente nidificato nel tempo, potresti dover gestire quegli oggetti interni con un po' più di cura. Tuttavia, il principio rimane lo stesso: i dati del giocatore dovrebbero arricchire i tuoi valori predefiniti, non sostituirli completamente.
Versiona le tue chiavi
I browser non eliminano automaticamente le vecchie voci del local storage. Se modifichi drasticamente la struttura dei dati, hai bisogno di un modo pulito per abbandonare il vecchio formato. Dai alla tua chiave di archiviazione un suffisso di versione. bitSurvivorsSave_v1 è esplicito. Ti dice esattamente quale schema ha scritto quel file. In seguito, quando ristrutturerai la progressione o aggiungerai un sistema di inventario completo, passa a bitSurvivorsSave_v2.
Questo ti offre due vantaggi pratici. Primo, non analizzerai mai accidentalmente un blob v1 con la logica v2. Secondo, puoi scrivere del codice di migrazione se lo desideri. All'avvio, controlla la presenza di v1. Se esiste e v2 non c'è, migra i vecchi dati nella nuova struttura, scrivili nella nuova chiave e procedi. Se non vuoi migrare, almeno la vecchia chiave rimarrà innocua nello storage mentre il tuo nuovo codice la ignorerà. In ogni caso, il versionamento previene la corruzione silenziosa dei dati.
Rendi il salvataggio invisibile
La persistenza dovrebbe essere naturale come respirare. Il giocatore non dovrebbe mai doverci pensare. Non aggiungere un pulsante "Applica" nel menu delle impostazioni. I pulsanti "Applica" creano attrito e abituano gli utenti a preoccuparsi che le loro scelte siano state effettivamente salvate. Inoltre, favoriscono la perdita di dati quando un giocatore attiva tre opzioni, dimentica di cliccare su Applica e chiude la scheda.
Salva nel momento esatto in cui avviene l'interazione. Quando il giocatore clicca su una casella per disabilitare lo scuotimento dello schermo, chiama immediatamente la tua funzione di scrittura. Quando la partita finisce e il punteggio finale viene calcolato, scrivi il nuovo record prima che l'animazione della schermata di game over sia terminata. Il salvataggio guidato dagli eventi mantiene la tua architettura prevedibile, perché il salvataggio risiede sempre accanto all'azione che ha modificato i dati. Non dovrai mai dare la caccia a una funzione di batching centrale o preoccuparti di stati obsoleti.
Questo approccio semplifica anche il tuo modello mentale. Saprai esattamente dove avviene la persistenza: nel callback che gestisce il toggle e nella funzione che gestisce la morte. Non ci saranno scritture misteriose sparse per tutto il codice.
Crea un pulsante di reset per te stesso
Corromperai i tuoi stessi salvataggi durante lo sviluppo. Scriverai dati errati, testerai casi limite e avrai bisogno di tornare rapidamente a uno stato pulito. Inserisci un pulsante di reset in un menu di debug o in una combinazione di tasti nascosta. Fai in modo che quel pulsante di reset faccia due cose in questo ordine preciso: resetta lo stato in memoria allo schema predefinito, quindi chiama immediatamente la stessa funzione di salvataggio che scrive nello storage locale.
Se ti limiti a svuotare la variabile locale saltando il passaggio della scrittura, non avrai ottenuto nulla. Al prossimo aggiornamento della pagina, i vecchi dati verranno estratti nuovamente dal browser e risorgeranno. Un reset che dimentica di persistere è il tipo di bug che ti fa sprecare un intero pomeriggio. Definisci la sequenza una volta per tutte e il tuo ciclo di test rimarrà veloce per il resto del progetto.
La lezione fondamentale
Il salvataggio non è una funzionalità che aggiungi alla fine. È un'infrastruttura che definisce se il tuo gioco sembrerà solido e rispettoso del tempo del giocatore. Un survivor shooter in Phaser 4 vive o muore in base alle partite ripetute. Se la scheda del browser è una pistola carica puntata contro i progressi del giocatore, alla fine smetterà di tornare. Scrivi uno schema, difenditi dai dati errati, unisci invece di sostituire, crea versioni delle tue chiavi e salva ad ogni evento significativo. Il tuo "io" futuro, e ogni giocatore che tornerà dopo il tuo prossimo aggiornamento, ti ringrazieranno.
