Сериализация и десериализация состояния игры

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

В контексте разработки на Phaser (версии 3) сериализация и десериализация позволяют:

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

Сериализация — это преобразование состояния в формат, пригодный для хранения или передачи (чаще всего JSON). Десериализация — обратный процесс восстановления игровых объектов из сохранённых данных.


Что необходимо сохранять в Phaser

Phaser не предоставляет встроенной универсальной системы сохранений. Разработчик самостоятельно определяет структуру сохраняемых данных.

Чаще всего сериализации подлежат:

1. Игровые сущности

  • координаты (x, y)
  • скорость (body.velocity)
  • здоровье
  • анимация
  • состояние (alive, stunned, attacking и т.д.)

2. Состояние сцены

  • текущий уровень
  • активные враги
  • таймеры
  • прогресс миссий

3. Глобальные данные

  • очки
  • инвентарь
  • разблокированные способности
  • настройки пользователя

Архитектура хранения состояния

Существует два основных подхода:

Централизованное хранилище

Создаётся единый объект состояния:

const GameState = {
    level: 1,
    score: 0,
    player: {
        x: 100,
        y: 200,
        health: 100
    },
    enemies: []
};

Этот объект используется всеми сценами.

Модульная сериализация

Каждая сцена или объект отвечает за собственное сохранение:

class Player extends Phaser.Physics.Arcade.Sprite {
    serialize() {
        return {
            x: this.x,
            y: this.y,
            health: this.health
        };
    }

    deserialize(data) {
        this.setPosition(data.x, data.y);
        this.health = data.health;
    }
}

Такой подход повышает масштабируемость и упрощает поддержку.


Сериализация состояния

Использование JSON

Основной способ сериализации — JSON.stringify():

const saveData = JSON.stringify(GameState);

Полученная строка может быть:

  • записана в localStorage
  • отправлена на сервер
  • сохранена в файл

Сохранение в localStorage

Браузерное хранилище — простой способ реализации системы сохранений.

localStorage.setItem('phaserSave', JSON.stringify(GameState));

Загрузка:

const savedData = localStorage.getItem('phaserSave');

if (savedData) {
    const parsed = JSON.parse(savedData);
    Object.assign(GameState, parsed);
}

Важно учитывать ограничения localStorage:

  • объём ~5 МБ
  • хранение только строк
  • отсутствие шифрования

Сериализация игровых объектов Phaser

Нельзя напрямую сериализовать объекты Phaser:

JSON.stringify(player); // некорректно

Причина — циклические ссылки и служебные поля движка.

Правильный способ

Извлекать только необходимые данные:

function serializePlayer(player) {
    return {
        x: player.x,
        y: player.y,
        velocityX: player.body.velocity.x,
        velocityY: player.body.velocity.y,
        health: player.health,
        currentAnim: player.anims.currentAnim?.key
    };
}

Сериализация групп (Phaser.GameObjects.Group)

Группы требуют обхода:

function serializeEnemies(group) {
    const data = [];

    group.children.iterate(enemy => {
        data.push({
            x: enemy.x,
            y: enemy.y,
            health: enemy.health
        });
    });

    return data;
}

Десериализация и восстановление сцены

Проблема порядка инициализации

Сначала необходимо:

  1. загрузить ресурсы,
  2. создать сцену,
  3. затем применить сохранённое состояние.

Пример восстановления:

function loadPlayer(scene, data) {
    const player = scene.physics.add.sprite(data.x, data.y, 'player');

    player.health = data.health;
    player.body.setVelocity(data.velocityX, data.velocityY);

    if (data.currentAnim) {
        player.anims.play(data.currentAnim);
    }

    return player;
}

Восстановление групп

function loadEnemies(scene, enemiesData) {
    const group = scene.physics.add.group();

    enemiesData.forEach(data => {
        const enemy = group.create(data.x, data.y, 'enemy');
        enemy.health = data.health;
    });

    return group;
}

Работа с Scene Data Manager

В Phaser каждая сцена имеет this.registry, позволяющий хранить данные между сценами.

this.registry.set('score', 100);

Получение:

const score = this.registry.get('score');

Для сериализации:

const data = this.registry.getAll();
localStorage.setItem('registrySave', JSON.stringify(data));

Использование Phaser.Data.DataManager

Объекты могут иметь собственный менеджер данных:

player.setData('health', 100);

Получение:

player.getData('health');

Сериализация:

const playerData = player.data.getAll();

Сериализация таймеров и событий

Таймеры (this.time.addEvent) нельзя напрямую сохранить. Необходимо сохранять:

  • оставшееся время
  • параметры события

Пример:

const timerData = {
    delay: timer.delay,
    remaining: timer.getRemaining(),
    repeat: timer.repeat
};

При восстановлении:

this.time.addEvent({
    delay: timerData.remaining,
    callback: someFunction,
    repeat: timerData.repeat
});

Чекпоинты

Чекпоинт — частичная сериализация состояния.

Стратегия:

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

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

{
    level: 2,
    player: {...},
    inventory: [...],
    checkpointId: 'room_3'
}

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

При изменении структуры данных старые сохранения могут стать несовместимыми.

Решение — хранить версию:

{
    version: 2,
    player: {...}
}

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

if (save.version === 1) {
    migrateFromV1(save);
}

Работа с сервером

Для онлайн-игр состояние отправляется через HTTP или WebSocket.

Пример отправки:

fetch('/save', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(GameState)
});

На сервере данные могут храниться в базе данных.


Оптимизация размера сохранений

Методы уменьшения объёма:

  • удаление временных полей;
  • использование коротких ключей;
  • хранение индексов вместо строк;
  • компрессия (например, LZ-String).

Пример минимизированной структуры:

{
    l: 3,
    s: 2500,
    p: { x: 120, y: 80, h: 90 }
}

Обработка ошибок

При загрузке возможны ошибки:

  • повреждённый JSON;
  • отсутствие данных;
  • несовпадение версии.

Безопасная загрузка:

function safeLoad(key) {
    try {
        const raw = localStorage.getItem(key);
        if (!raw) return null;
        return JSON.parse(raw);
    } catch (e) {
        console.error('Ошибка загрузки сохранения', e);
        return null;
    }
}

Полный пример системы сохранения

class SaveManager {
    static save(scene) {
        const state = {
            level: scene.level,
            score: scene.score,
            player: serializePlayer(scene.player),
            enemies: serializeEnemies(scene.enemies)
        };

        localStorage.setItem('save', JSON.stringify(state));
    }

    static load(scene) {
        const raw = localStorage.getItem('save');
        if (!raw) return;

        const state = JSON.parse(raw);

        scene.level = state.level;
        scene.score = state.score;

        scene.player = loadPlayer(scene, state.player);
        scene.enemies = loadEnemies(scene, state.enemies);
    }
}

Практические рекомендации

Не сохраняются:

  • текстуры;
  • ссылки на сцены;
  • функции;
  • объекты физического движка.

Сохраняются:

  • примитивные типы;
  • массивы;
  • plain-объекты;
  • идентификаторы ресурсов.

Система сериализации должна быть:

  • детерминированной;
  • независимой от внутренней реализации Phaser;
  • устойчивой к изменениям структуры;
  • легко расширяемой.

Грамотно реализованная сериализация превращает игру из временной сессии в полноценный продукт с устойчивым прогрессом, поддержкой паузы, перезапуска и масштабирования логики на сетевые сценарии.