Das Schließen eines Browser-Tabs sollte nicht vier Stunden Fortschritt auslöschen. Das scheint offensichtlich, doch viele Browser-Spiele behandeln den Local Storage nur als Nebensache. Ein Spieler erreicht einen Highscore, passt seine Einstellungen an, kommt am nächsten Tag zurück und findet nichts vor. Schlimmer noch: Er kehrt nach einem Patch zurück, und das Spiel wirft einen Fehler aus, weil die Save-Datei auf seinem Rechner nicht mehr mit dem Code übereinstimmt, den Sie gerade veröffentlicht haben. Einen Shooter im Survivor-Stil mit Phaser 4 zu entwickeln, bedeutet, mit ständigen Wellen von Gegnern fertig zu werden, aber die eigentliche langfristige Bedrohung sind Ihre eigenen zukünftigen Updates.
Die meisten Entwickler bauen ihr erstes Speichersystem, indem sie ein Objekt nehmen, es durch JSON.stringify jagen und in den localStorage werfen. Beim Laden parsen sie es und geben es unverändert an das Spiel zurück. Das funktioniert am ersten Tag. Es bricht jedoch in dem Moment zusammen, in dem Sie eine neue Einstellung, ein neues Unlock-Flag oder eine dritte Ebene einer verschachtelten Konfiguration hinzufügen. Wenn ein wiederkehrender Spieler eine alte Save-Datei hat, der die Eigenschaft vignette fehlt, und Ihr neuer Code deren Existenz erwartet, erhalten Sie undefined, wo Sie einen Boolean erwartet hätten. Multiplizieren Sie das mit einem Dutzend neuer Funktionen, und Sie haben einen Debugging-Albtraum, der zuerst Ihre treuesten Spieler trifft.
Beginnen Sie mit einem Contract, nicht mit einem rohen Objekt
Bevor Sie jemals den localStorage anfassen, definieren Sie ein Standard-Save-Schema in Ihrem Codebase. Betrachten Sie es als einen Contract, den jede Save-Datei einhalten muss, egal ob sie vor fünf Minuten oder vor fünf Monaten erstellt wurde. Ein klarer Ausgangspunkt könnte so aussehen:
const defaultSave = {
highScore: 0,
settings: {
screenShake: true,
vignette: true
}
};
Dieses Objekt existiert in Ihrem Quellcode. Wenn das Spiel startet, haben Sie diese Struktur immer zur Verfügung. Sie gibt Ihnen eine Basis vor. Zudem zwingt sie Sie dazu, über die Struktur nachzudenken, bevor Sie irgendetwas serialisieren. Wenn Sie diesen Schritt überspringen und einfach das State-Objekt speichern, das gerade gerade passt, enden Sie mit inkonsistenten Keys, fehlenden Feldern und lautlosen Fehlern, wenn ältere Saves aus dem Takt mit Ihren Erwartungen geraten.
Defensives Laden mit Try/Catch
Local Storage ist keine Datenbank. Es ist ein Schrank voller Strings im Browser, und alles kann dort landen. Der Benutzer könnte manuell einen Wert bearbeitet haben, eine halb geschriebene Schreiboperation wurde unterbrochen oder eine Browser-Erweiterung hat Müll in den Key geworfen, den Sie beansprucht haben. Wenn Sie diesen String wieder herausziehen und an JSON.parse übergeben, wirft ein einziger beschädigter Charakter eine harte Exception. In einem Phaser-Spiel kann dieser unbehandelte Fehler Ihre Boot-Sequenz einfrieren oder den Spieler vor einen leeren Bildschirm zurückwerfen.
Umschließen Sie Ihre Lese- und Parsing-Logik immer mit einem try/catch-Block. Greifen Sie im Fehlerfall auf Ihr Standard-Schema zurück. Das Ziel ist einfach: Wenn die Save-Datei nicht lesbar ist, behandeln Sie den Spieler wie einen neuen Benutzer, anstatt die gesamte Sitzung zum Absturz zu bringen. Diese eine Gewohnheit unterscheidet Hobby-Projekte von produktionsreifen Builds. Es kostet fast nichts, sie zu implementieren, und sie bewahrt Sie vor mysteriösen Bug-Reports, die unmöglich zu reproduzieren sind.
Alte Daten mit Defaults zusammenführen
Ein erfolgreiches Parsen bedeutet nicht, dass Sie sicher sind. Ersetzen Sie Ihr Standard-Objekt niemals vollständig durch das geparste Ergebnis. Die alte Save-Datei enthält möglicherweise nicht Ihre neuesten Einstellungen. Sie speichert vielleicht screenShake, aber nicht vignette. Wenn Ihre Spiellogik davon ausgeht, dass vignette existiert, weil es mit dem neuesten Update ausgeliefert wurde, sind Sie sofort wieder auf der Jagd nach undefined-Fehlern.
Stattdessen führen Sie die geladenen Daten mit Ihren Defaults zusammen. Verwenden Sie Object.assign, um die gespeicherten Werte über das Basis-Schema zu legen. Die Defaults füllen jede fehlende Lücke automatisch auf. Neue Eigenschaften, die Sie in Version zwei hinzugefügt haben, erhalten ihre Initialwerte aus dem Standard-Objekt. Bestehende Eigenschaften, die der Spieler tatsächlich geändert hat, werden mit seinen gespeicherten Präferenzen überschrieben. So profitieren alle: Der wiederkehrende Spieler behält seinen Highscore, und das Spiel erhält Zugriff auf den neuen Toggle, den Sie gestern hinzugefügt haben, ohne abzustürzen.
Beachten Sie, dass Object.assign ein Shallow Merge durchführt. Wenn Ihr Einstellungs-Objekt im Laufe der Zeit tief verschachtelt wird, müssen Sie diese inneren Objekte möglicherweise mit etwas mehr Sorgfalt behandeln. Dennoch gilt das Prinzip: Die Daten des Spielers sollten Ihre Defaults ergänzen, nicht einfach ersetzen.
Versionieren Sie Ihre Keys
Browser löschen alte Local-Storage-Einträge nicht automatisch. Wenn Sie Ihre Datenstruktur drastisch ändern, benötigen Sie einen sauberen Weg, das alte Format aufzugeben. Benennen Sie Ihren Storage-Key mit einem Versions-Suffix. bitSurvivorsSave_v1 ist explizit. Es sagt Ihnen genau, welches Schema diese Datei geschrieben hat. Wenn Sie später den Fortschritt überarbeiten oder ein vollständiges Inventarsystem hinzufügen, wechseln Sie zu bitSurvivorsSave_v2.
Dies bietet Ihnen zwei praktische Vorteile. Erstens parsen Sie niemals versehentlich einen v1-Blob mit v2-Logik. Zweitens können Sie bei Bedarf Migrationscode schreiben. Prüfen Sie beim Start auf v1. Wenn diese existiert und v2 nicht, migrieren Sie die alten Daten in die neue Struktur, schreiben Sie sie unter den neuen Key und machen Sie weiter. Wenn Sie nicht migrieren möchten, liegt der alte Key zumindest harmlos im Speicher, während Ihr neuer Code ihn ignoriert. So oder so verhindert Versionierung stille Datenkorruption.
Machen Sie das Speichern unsichtbar
Persistenz sollte sich wie das Atmen anfühlen. Der Spieler sollte niemals darüber nachdenken müssen. Fügen Sie keinen „Anwenden“-Button in Ihrem Einstellungsmenü hinzu. „Anwenden“-Buttons erzeugen Reibung und gewöhnen Nutzer daran, sich Sorgen zu machen, ob ihre Entscheidungen tatsächlich übernommen wurden. Sie laden zudem zu Datenverlust ein, wenn ein Spieler drei Optionen umschaltet, „Anwenden“ vergisst und den Tab schließt.
Speichern Sie in dem Moment, in dem die Interaktion stattfindet. Wenn der Spieler ein Kontrollkästchen anklickt, um das Bildschirmwackeln zu deaktivieren, rufen Sie sofort Ihre Write-Funktion auf. Wenn der Run endet und der Endstand berechnet wird, schreiben Sie den neuen Highscore, bevor die Game-Over-Animation abgeschlossen ist. Eventgesteuertes Speichern hält Ihre Architektur vorhersehbar, da das Speichern immer direkt neben der Aktion liegt, die die Daten geändert hat. Sie müssen nie nach einer zentralen Batching-Funktion suchen oder sich um veraltete Zustände sorgen.
Dieser Ansatz vereinfacht auch Ihr mentales Modell. Sie wissen genau, wo die Persistenz stattfindet: im Callback, der das Umschalten verarbeitet, und in der Funktion, die den Tod verarbeitet. Es gibt keine rätselhaften Schreibvorgänge, die über die gesamte Codebasis verstreut sind.
Bauen Sie einen Reset-Button für sich selbst
Während der Entwicklung werden Sie Ihre eigenen Spielstände korrumpieren. Sie werden fehlerhafte Daten schreiben, Edge-Cases testen und schnell zu einem sauberen Zustand zurückkehren müssen. Bauen Sie einen Reset-Button in ein Debug-Menü oder eine versteckte Tastenkombination ein. Lassen Sie diesen Reset-Button zwei Dinge in genau dieser Reihenfolge tun: Setzen Sie Ihren In-Memory-Zustand auf das Standard-Schema zurück und rufen Sie dann sofort dieselbe Save-Funktion auf, die in den Local Storage schreibt.
Wenn Sie nur die lokale Variable löschen und den Schreibschritt überspringen, haben Sie nichts erreicht. Das nächste Neuladen der Seite zieht die alten Daten wieder aus dem Browser und erweckt sie zum Leben. Ein Reset, der das Speichern vergisst, ist die Art von Bug, die einen ganzen Nachmittag verschwendet. Wenn Sie die Sequenz einmal richtig hinbekommen, bleibt Ihre Testschleife für den Rest des Projekts schnell.
Das eigentliche Fazit
Speichern ist kein Feature, das man am Ende einfach dranhängt. Es ist eine Infrastruktur, die darüber entscheidet, ob sich Ihr Spiel beständig anfühlt und die Zeit des Spielers respektiert. Ein Phaser 4 Survivor-Shooter lebt oder stirbt durch wiederholte Durchläufe. Wenn der Browser-Tab wie eine geladene Waffe auf den Fortschritt des Spielers gerichtet ist, werden sie irgendwann nicht mehr zurückkehren. Schreiben Sie ein Schema, schützen Sie sich vor fehlerhaften Daten, führen Sie Merges statt Ersetzungen durch, versionieren Sie Ihre Keys und speichern Sie bei jedem bedeutsamen Ereignis. Ihr zukünftiges Ich und jeder Spieler, der nach Ihrem nächsten Update zurückkehrt, wird es Ihnen danken.
