Сохранение состояния в localStorage

Браузерный API localStorage предоставляет простой механизм долговременного хранения данных на стороне клиента. В контексте игр на Phaser он используется для:

  • сохранения прогресса игрока;
  • хранения рекордов;
  • настроек (звук, язык, сложность);
  • состояния открытых уровней;
  • пользовательских параметров;
  • кэширования вычисляемых данных.

Данные сохраняются в виде строк и остаются доступными после перезагрузки страницы и повторного запуска игры.

localStorage является частью Web Storage API и доступен глобально через объект window.localStorage.


Особенности хранения данных

Ключевые характеристики localStorage:

  • хранит данные в формате ключ–значение;
  • принимает только строки;
  • данные сохраняются бессрочно;
  • объём хранилища — обычно около 5–10 МБ (зависит от браузера);
  • доступ синхронный (операции блокируют поток выполнения).

Пример базовой записи:

localStorage.setItem('score', '1500');

Получение значения:

const score = localStorage.getItem('score');

Удаление:

localStorage.removeItem('score');

Полная очистка:

localStorage.clear();

Интеграция localStorage в структуру проекта Phaser

В Phaser чаще всего работа с хранилищем происходит:

  • в Scene (например, при создании сцены);
  • в менеджере данных;
  • в отдельном сервисе сохранений;
  • через Phaser.Data.DataManager.

Правильная архитектура предполагает изоляцию логики сохранения от игровой логики.


Сохранение простых значений

Пример: сохранение рекорда

class GameScene extends Phaser.Scene {
    constructor() {
        super('GameScene');
    }

    create() {
        this.bestScore = this.loadBestScore();
    }

    saveBestScore(score) {
        localStorage.setItem('bestScore', score.toString());
    }

    loadBestScore() {
        const saved = localStorage.getItem('bestScore');
        return saved ? parseInt(saved, 10) : 0;
    }
}

Здесь:

  • при создании сцены загружается сохранённый рекорд;
  • если значение отсутствует — возвращается 0.

Сериализация сложных структур

Так как localStorage хранит только строки, для работы с объектами используется JSON.

Сохранение объекта

const playerData = {
    level: 5,
    health: 80,
    inventory: ['sword', 'shield']
};

localStorage.setItem('playerData', JSON.stringify(playerData));

Загрузка объекта

const raw = localStorage.getItem('playerData');
const playerData = raw ? JSON.parse(raw) : null;

Важно учитывать возможные ошибки парсинга.


Организация структуры сохранений

Подход 1: отдельные ключи

bestScore
soundEnabled
unlockedLevels

Подходит для небольших проектов.

Подход 2: единый объект состояния

const saveData = {
    bestScore: 1200,
    settings: {
        sound: true,
        music: false
    },
    progress: {
        unlockedLevels: [1, 2, 3]
    }
};

localStorage.setItem('gameSave', JSON.stringify(saveData));

Преимущества:

  • централизованное хранение;
  • проще управлять версиями сохранений;
  • легче выполнять миграции структуры.

Версионирование сохранений

При развитии проекта структура данных может изменяться. Без версии сохранений возникают ошибки несовместимости.

Пример структуры:

const saveData = {
    version: 2,
    bestScore: 1200,
    settings: {
        sound: true
    }
};

При загрузке:

function loadGame() {
    const raw = localStorage.getItem('gameSave');
    if (!raw) return getDefaultSave();

    const data = JSON.parse(raw);

    if (!data.version || data.version < 2) {
        return migrateSave(data);
    }

    return data;
}

Такой подход позволяет безопасно изменять формат хранения.


Автоматическое сохранение через DataManager

В Phaser имеется встроенный DataManager, который можно связать с localStorage.

Пример:

class GameScene extends Phaser.Scene {
    create() {
        this.data.set('score', 0);

        this.data.events.on('changedata', (parent, key, value) => {
            this.saveData();
        });

        this.loadData();
    }

    saveData() {
        localStorage.setItem('sceneData', JSON.stringify(this.data.getAll()));
    }

    loadData() {
        const raw = localStorage.getItem('sceneData');
        if (!raw) return;

        const data = JSON.parse(raw);
        Object.keys(data).forEach(key => {
            this.data.set(key, data[key]);
        });
    }
}

В этом примере:

  • любое изменение данных автоматически инициирует сохранение;
  • данные сцены централизованно управляются через DataManager.

Обработка ошибок и защита от повреждённых данных

localStorage может содержать некорректные данные:

  • ручное изменение через DevTools;
  • устаревший формат;
  • частично повреждённая строка.

Рекомендуется использовать try/catch:

function safeLoad(key, defaultValue) {
    try {
        const raw = localStorage.getItem(key);
        return raw ? JSON.parse(raw) : defaultValue;
    } catch (e) {
        return defaultValue;
    }
}

Это предотвращает падение игры при ошибке парсинга.


Ограничения и производительность

Синхронность операций

Методы localStorage блокируют основной поток JavaScript. Частые вызовы в игровом цикле (update) могут вызывать лаги.

Неправильный пример:

upd ate() {
    localStorage.setItem('score', this.score);
}

Правильная стратегия:

  • сохранять при завершении уровня;
  • сохранять при паузе;
  • использовать дебаунс или таймер.

Ограничение объёма

При превышении допустимого объёма возникает исключение QuotaExceededError.

Пример защиты:

try {
    localStorage.setItem('gameSave', JSON.stringify(bigData));
} catch (e) {
    console.error('Storage limit exceeded');
}

Сохранение настроек игры

Частый сценарий — хранение пользовательских настроек.

const settings = {
    musicVolume: 0.5,
    sfxVolume: 0.7,
    fullscreen: false
};

localStorage.setItem('settings', JSON.stringify(settings));

Загрузка при старте игры:

class BootScene extends Phaser.Scene {
    create() {
        const settings = safeLoad('settings', {
            musicVolume: 1,
            sfxVolume: 1,
            fullscreen: false
        });

        this.registry.se t('settings', settings);
    }
}

Использование registry позволяет передавать данные между сценами.


Сброс прогресса

Для реализации кнопки “Сбросить прогресс”:

function resetGame() {
    localStorage.removeItem('gameSave');
}

Или выборочное удаление:

localStorage.removeItem('bestScore');

Локальные профили игроков

При необходимости поддержки нескольких профилей используется префиксация ключей:

function getProfileKey(profileId, key) {
    return `profile_${profileId}_${key}`;
}

localStorage.setItem(getProfileKey(1, 'save'), JSON.stringify(data));

Такой подход изолирует данные разных пользователей на одном устройстве.


Безопасность и защита данных

localStorage:

  • доступен через DevTools;
  • может быть изменён вручную;
  • не защищён от подмены.

Для минимальной защиты возможно:

  • использовать контрольные суммы;
  • шифровать данные (например, через сторонние библиотеки);
  • проверять допустимые диапазоны значений при загрузке.

Простейшая проверка:

if (data.bestScore < 0 || data.bestScore > 1000000) {
    data.bestScore = 0;
}

Практическая схема сохранения игры

Типичный поток:

  1. Загрузка сохранения в BootScene.
  2. Сохранение состояния в registry.
  3. Изменение данных в сценах.
  4. Сохранение при ключевых событиях.
  5. Проверка версии и целостности при старте.

Такая организация делает систему хранения:

  • предсказуемой;
  • масштабируемой;
  • устойчивой к изменениям структуры данных.

localStorage остаётся простым, но эффективным инструментом для большинства 2D-игр на Phaser, не требующих серверной синхронизации или облачных сохранений.