Closing a browser tab should not erase four hours of progress. That seems obvious, yet plenty of browser games treat local storage as an afterthought. A player unlocks a high score, tweaks their settings, comes back tomorrow, and finds nothing. Worse, they return after a patch and the game throws an error because the save file on their machine no longer matches the code you just shipped. Building a survivor-style shooter in Phaser 4 means dealing with constant waves of enemies, but the real long-term threat is your own future updates.

Most developers build their first save system by grabbing an object, running it through JSON.stringify, and dumping it into localStorage. On load, they parse it and hand it back to the game raw. That works on day one. It breaks the moment you add a new setting, a new unlock flag, or a third layer of nested configuration. If a returning player has an old save file that lacks a vignette property, and your new code expects it to exist, you get undefined where you expected a boolean. Multiply that across a dozen new features and you have a debugging nightmare that hits your most loyal players first.

Start with a Contract, Not a Raw Object

Before you ever touch localStorage, define a default save schema in your codebase. Think of it as a contract that every save file must honor, whether it was created five minutes ago or five months ago. A clear starting point might look like this:

const defaultSave = {
  highScore: 0,
  settings: {
    screenShake: true,
    vignette: true
  }
};

This object lives in your source code. When the game boots, you always have this shape available. It gives you a baseline. It also forces you to think about structure before you serialize anything. If you skip this step and simply store whatever state object is convenient at the time, you end up with inconsistent keys, missing fields, and silent failures when older saves drift out of sync with your expectations.

Defensive Loading with Try/Catch

Local storage is not a database. It is a string closet in the browser, and anything can end up in there. The user might have manually edited a value, a half-written write operation got interrupted, or a browser extension dumped garbage into the key you claimed. When you pull that string back out and feed it to JSON.parse, a single corrupted character throws a hard exception. In a Phaser game, that unhandled error can freeze your boot sequence or dump the player back to a blank screen.

Always wrap your read and parse logic in a try/catch block. On failure, fall back to your default schema. The goal is simple: if the save file is unreadable, treat the player as a new user rather than crashing the entire session. This one habit separates hobby projects from production-grade builds. It costs almost nothing to implement, and it saves you from mysterious bug reports that are impossible to reproduce.

Merge Old Data with Defaults

A successful parse does not mean you are safe. Never replace your default object entirely with the parsed result. That old save file might not contain your newest settings. It might store screenShake but not vignette. If your game logic assumes vignette exists because it shipped with the latest update, you are right back to chasing undefined errors.

Instead, merge the loaded data with your defaults. Use Object.assign to layer the saved values on top of the baseline schema. The defaults fill every missing gap automatically. New properties you added in version two get their initial values from the default object. Existing properties the player actually changed get overwritten with their stored preferences. Everyone wins. The returning player keeps their high score, and the game gains access to the fresh toggle you added yesterday without blowing up.

Keep in mind that Object.assign performs a shallow merge. If your settings object becomes deeply nested over time, you may need to handle those inner objects with slightly more care. Still, the principle holds: the player’s data should decorate your defaults, not replace them outright.

Version Your Keys

Browsers do not delete old local storage entries automatically. If you change your data structure dramatically, you need a clean way to abandon the old format. Name your storage key with a version suffix. bitSurvivorsSave_v1 is explicit. It tells you exactly which schema wrote that file. Later, when you overhaul progression or add a full inventory system, move to bitSurvivorsSave_v2.

Cela vous offre deux avantages pratiques. Premièrement, vous ne risquez jamais d'analyser accidentellement un blob v1 avec la logique v2. Deuxièmement, vous pouvez écrire un code de migration si vous le souhaitez. Au démarrage, vérifiez la présence de la v1. Si elle existe et que la v2 est absente, migrez les anciennes données vers la nouvelle structure, écrivez-les dans la nouvelle clé, et continuez. Si vous ne souhaitez pas migrer, au moins l'ancienne clé reste sans danger dans le stockage pendant que votre nouveau code l'ignore. Dans tous les cas, le versionnage empêche la corruption silencieuse.

Rendez la sauvegarde invisible

La persistance devrait être aussi naturelle que la respiration. Le joueur ne devrait jamais avoir à y penser. N'ajoutez pas de bouton « Appliquer » dans votre menu de paramètres. Les boutons « Appliquer » créent de la friction et habituent les utilisateurs à se demander si leurs choix ont réellement été pris en compte. Ils favorisent également la perte de données lorsqu'un joueur modifie trois options, oublie de cliquer sur « Appliquer » et ferme l'onglet.

Sauvegardez dès que l'interaction se produit. Lorsque le joueur coche une case pour désactiver le tremblement de l'écran, appelez votre fonction d'écriture immédiatement. Lorsque la partie se termine et que le score final est calculé, enregistrez le nouveau record avant que l'écran de fin de partie ne finisse de s'animer. La sauvegarde pilotée par les événements rend votre architecture prévisible, car la sauvegarde se trouve toujours juste à côté de l'action qui a modifié les données. Vous n'aurez jamais à traquer une fonction de traitement par lots centrale ou à vous soucier d'un état obsolète.

Cette approche simplifie également votre modèle mental. Vous savez exactement où la persistance intervient : dans le callback qui gère la bascule, et dans la fonction qui gère la mort. Il n'y a pas d'écritures mystérieuses éparpillées dans toute la base de code.

Créez un bouton de réinitialisation pour vous-même

Vous corromprez vos propres sauvegardes pendant le développement. Vous écrirez des données erronées, testerez des cas limites et devrez revenir rapidement à un état propre. Intégrez un bouton de réinitialisation dans un menu de débogage ou via une combinaison de touches cachée. Faites en sorte que ce bouton de réinitialisation effectue deux actions dans cet ordre précis : réinitialisez votre état en mémoire selon le schéma par défaut, puis appelez immédiatement la même fonction de sauvegarde qui écrit dans le stockage local.

Si vous vous contentez d'effacer la variable locale en sautant l'étape d'écriture, vous n'aurez rien accompli. Le prochain rafraîchissement de la page récupérera les anciennes données du navigateur et les ressuscitera. Une réinitialisation qui oublie de persister est le genre de bug qui fait perdre une après-midi. Maîtrisez cette séquence une fois pour toutes, et votre boucle de test restera rapide pour le reste du projet.

L'essentiel à retenir

La sauvegarde n'est pas une fonctionnalité que l'on ajoute à la fin. C'est une infrastructure qui définit si votre jeu semble robuste et respectueux du temps du joueur. Un shooter de survie sous Phaser 4 vit ou meurt grâce aux parties répétées. Si l'onglet du navigateur est une arme chargée pointée sur la progression du joueur, il finira par ne plus revenir. Écrivez un schéma, protégez-vous contre les données erronées, fusionnez plutôt que de remplacer, versionnez vos clés et sauvegardez à chaque événement significatif. Votre futur vous-même, ainsi que chaque joueur qui reviendra après votre prochaine mise à jour, vous en remercieront.