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.

Це дає вам дві практичні переваги. По-перше, ви ніколи випадково не розпарсите blob v1 за допомогою логіки v2. По-друге, ви можете написати код міграції, якщо забажаєте. Під час запуску перевірте наявність v1. Якщо він існує, а v2 — ні, мігруйте старі дані в нову структуру, запишіть їх у новий ключ і рухайтеся далі. Якщо ви не хочете здійснювати міграцію, принаймні старий ключ безпечно лежатиме в сховищі, поки ваш новий код його ігнорує. У будь-якому разі, версіонування запобігає прихованому пошкодженню даних.

Зробіть збереження непомітним

Персистентність має відчуватися як дихання. Гравець ніколи не повинен про це думати. Не додавайте кнопку «Застосувати» (Apply) у меню налаштувань. Кнопки застосування створюють зайвий опір і привчають користувачів хвилюватися, чи справді їхній вибір зберігся. Вони також провокують втрату даних, коли гравець перемикає три параметри, пропускає кнопку «Застосувати» і закриває вкладку.

Зберігайте дані в момент взаємодії. Коли гравець натискає прапорець, щоб вимкнути трясіння екрана, негайно викликайте функцію запису. Коли забіг завершується і підбиваються підсумки рахунку, запишіть новий рекорд до того, як закінчиться анімація екрана завершення гри. Збереження, кероване подіями (event-driven saving), робить вашу архітектуру передбачуваною, оскільки збереження завжди знаходиться поруч із дією, що змінила дані. Вам ніколи не доведеться шукати центральну функцію пакетної обробки або хвилюватися про застарілий стан.

Цей підхід також спрощує вашу ментальну модель. Ви точно знаєте, де відбувається збереження: у колбеку (callback), який обробляє перемикач, і у функції, яка обробляє смерть. У коді немає ніяких загадкових записів, розкиданих по всьому проєкту.

Створіть кнопку скидання для себе

Під час розробки ви неминуче будете пошкоджувати власні збереження. Ви будете записувати некоректні дані, тестувати граничні випадки та потребуватимете швидкого повернення до чистого стану. Створіть кнопку скидання в меню налагодження або за допомогою прихованої комбінації клавіш. Зробіть так, щоб ця кнопка виконувала дві дії в точно такому порядку: скидала ваш стан у пам'яті до дефолтної схеми, а потім негайно викликала ту саму функцію збереження, яка записує дані в local storage.

Якщо ви лише очистите локальну змінну, пропустивши крок запису, ви нічого не досягнете. Наступне оновлення сторінки витягне старі дані з браузера і «воскресить» їх. Скидання, яке забуває про збереження — це саме той тип багів, що здатні змарнувати цілий день. Налаштуйте цю послідовність один раз, і ваш цикл тестування залишатиметься швидким до кінця проєкту.

Головний висновок

Збереження — це не функція, яку ви прикручуєте в самому кінці. Це інфраструктура, яка визначає, чи здається ваша гра надійною та чи поважає вона час гравця. Життя survivor-шутера на Phaser 4 залежить від можливості повторних забігів. Якщо вкладка браузера — це заряджена зброя, спрямована на прогрес гравця, він зрештою перестане повертатися. Створіть схему, захистіться від некоректних даних, використовуйте злиття замість заміни, версіонуйте ключі та зберігайте дані при кожній значущій події. Ваша майбутня версія себе та кожен гравець, який повернеться після вашого наступного оновлення, подякують вам.