Закрытие вкладки браузера не должно стирать четыре часа прогресса. Это кажется очевидным, однако многие браузерные игры относятся к локальному хранилищу как к чему-то второстепенному. Игрок достигает рекорда, настраивает параметры, возвращается на следующий день и обнаруживает, что всё пропало. Хуже того, они возвращаются после патча, и игра выдает ошибку, потому что файл сохранения на их устройстве больше не соответствует коду, который вы только что выпустили. Создание шутера в стиле survivor на Phaser 4 означает борьбу с постоянными волнами врагов, но настоящая долгосрочная угроза — это ваши собственные будущие обновления.

Большинство разработчиков создают свою первую систему сохранений, беря объект, пропуская его через JSON.stringify и сбрасывая в localStorage. При загрузке они парсят его и отдают игре в сыром виде. Это работает в первый день. Но всё ломается, как только вы добавляете новую настройку, новый флаг разблокировки или третий уровень вложенной конфигурации. Если у вернувшегося игрока есть старый файл сохранения, в котором отсутствует свойство vignette, а ваш новый код ожидает его наличия, вы получите undefined там, где ожидали логическое значение. Умножьте это на дюжину новых функций, и вы получите кошмар при отладке, который в первую очередь ударит по вашим самым преданным игрокам.

Начинайте с контракта, а не с сырого объекта

Прежде чем прикасаться к localStorage, определите схему сохранения по умолчанию в вашем коде. Думайте об этом как о контракте, который должен соблюдать каждый файл сохранения, независимо от того, был ли он создан пять минут или пять месяцев назад. Четкая отправная точка может выглядеть так:

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

Этот объект живет в вашем исходном коде. Когда игра запускается, у вас всегда есть доступ к такой структуре. Это дает вам базовую линию. Это также заставляет вас задуматься о структуре перед тем, как что-либо сериализовать. Если вы пропустите этот шаг и будете просто сохранять любой удобный на данный момент объект состояния, вы столкнетесь с несогласованными ключами, отсутствующими полями и «тихими» ошибками, когда старые сохранения перестанут соответствовать вашим ожиданиям.

Защищенная загрузка с помощью Try/Catch

Локальное хранилище — это не база данных. Это своего рода «шкаф со строками» в браузере, и в нем может оказаться что угодно. Пользователь мог вручную изменить значение, операция записи могла прерваться на полпути, или расширение браузера могло забросить мусор в ваш ключ. Когда вы извлекаете эту строку и передаете её в JSON.parse, один единственный поврежденный символ вызывает критическое исключение. В игре на Phaser эта необработанная ошибка может заморозить процесс загрузки или выбросить игрока на пустой экран.

Всегда оборачивайте логику чтения и парсинга в блок try/catch. В случае неудачи откатывайтесь к вашей схеме по умолчанию. Цель проста: если файл сохранения не читается, считайте игрока новым пользователем, а не обрушивайте всю сессию. Эта привычка отличает любительские проекты от продуктов промышленного уровня. Она почти ничего не стоит в реализации, но спасает вас от загадочных отчетов об ошибках, которые невозможно воспроизвести.

Объединяйте старые данные с настройками по умолчанию

Успешный парсинг не означает, что вы в безопасности. Никогда не заменяйте объект по умолчанию полностью результатом парсинга. Старый файл сохранения может не содержать ваших последних настроек. В нем может быть screenShake, но не быть vignette. Если ваша игровая логика предполагает наличие vignette (потому что она появилась в последнем обновлении), вы снова вернетесь к погоне за ошибками undefined.

Вместо этого объединяйте загруженные данные с настройками по умолчанию. Используйте Object.assign, чтобы наложить сохраненные значения поверх базовой схемы. Значения по умолчанию автоматически заполнят все пробелы. Новые свойства, добавленные во второй версии, получат свои начальные значения из объекта по умолчанию. Существующие свойства, которые игрок действительно изменил, будут перезаписаны его сохраненными предпочтениями. В выигрыше все. Вернувшийся игрок сохраняет свой рекорд, а игра получает доступ к новому переключателю, который вы добавили вчера, без сбоев.

Имейте в виду, что Object.assign выполняет поверхностное слияние (shallow merge). Если со временем ваш объект настроек станет глубоко вложенным, вам может потребоваться обрабатывать внутренние объекты с чуть большей осторожностью. Тем не менее, принцип остается прежним: данные игрока должны дополнять ваши настройки по умолчанию, а не заменять их полностью.

Версионируйте свои ключи

Браузеры не удаляют старые записи в локальном хранилище автоматически. Если вы радикально измените структуру данных, вам понадобится надежный способ отказаться от старого формата. Называйте ключ хранилища с суффиксом версии. bitSurvivorsSave_v1 — это наглядно. Он точно говорит о том, какая схема записала этот файл. Позже, когда вы переработаете систему прогрессии или добавите полноценную систему инвентаря, переходите на bitSurvivorsSave_v2.

Это дает два практических преимущества. Во-первых, вы никогда случайно не начнете парсить данные v1 с помощью логики v2. Во-вторых, при желании вы сможете написать код для миграции. При запуске проверьте наличие v1. Если он есть, а v2 — нет, перенесите старые данные в новую структуру, запишите их по новому ключу и продолжайте работу. Если вы не хотите проводить миграцию, по крайней мере, старый ключ будет безопасно лежать в хранилище, пока ваш новый код его игнорирует. В любом случае, версионирование предотвращает скрытое повреждение данных.

Сделайте сохранение незаметным

Сохранение должно ощущаться как дыхание. Игрок никогда не должен задумываться об этом. Не добавляйте кнопку «Применить» в меню настроек. Кнопки «Применить» создают лишнее трение и приучают пользователей беспокоиться о том, применились ли их изменения на самом деле. Они также провоцируют потерю данных: игрок может переключить три опции, пропустить кнопку «Применить» и закрыть вкладку.

Сохраняйте данные в момент взаимодействия. Когда игрок нажимает на чекбокс, чтобы отключить тряску экрана, немедленно вызывайте функцию записи. Когда забег заканчивается и подсчитывается итоговый счет, запишите новый рекорд до того, как закончится анимация экрана завершения игры. Сохранение, управляемое событиями, делает вашу архитектуру предсказуемой, так как сохранение всегда находится рядом с действием, изменившим данные. Вам никогда не придется искать центральную функцию пакетной записи или беспокоиться об устаревшем состоянии.

Такой подход также упрощает вашу ментальную модель. Вы точно знаете, где происходит сохранение: в колбэке, обрабатывающем переключатель, и в функции, обрабатывающей смерть. В кодовой базе нет никаких загадочных записей, разбросанных повсюду.

Создайте кнопку сброса для себя

В процессе разработки вы неизбежно будете повреждать собственные сохранения. Вы будете записывать некорректные данные, тестировать граничные случаи и захотите быстро вернуться в чистое состояние. Создайте кнопку сброса в меню отладки или через скрытую комбинацию клавиш. Сделайте так, чтобы эта кнопка выполняла два действия в строго определенном порядке: сбрасывала состояние в оперативной памяти к схеме по умолчанию, а затем немедленно вызывала ту же функцию сохранения, которая записывает данные в local storage.

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

Главный вывод

Сохранение — это не функция, которую вы прикручиваете в самом конце. Это инфраструктура, которая определяет, кажется ли ваша игра надежной и уважающей время игрока. Жизнь survivor-шутера на Phaser 4 зависит от возможности повторных забегов. Если вкладка браузера превращается в заряженное ружье, направленное на прогресс игрока, он в конечном итоге перестанет возвращаться. Напишите схему, защититесь от некорректных данных, используйте слияние вместо перезаписи, версионируйте ключи и сохраняйте данные при каждом значимом событии. Ваше будущее «я» и каждый игрок, который вернется после вашего следующего обновления, скажут вам спасибо.