О чем этот пример

Разделение игровой логики на независимые модули — ключ к поддержке сложных проектов. В этой статье мы разберем паттерн SubScene (или SubGame), который позволяет встроить целую мини-игру в основную сцену Phaser. Этот подход полезен для создания изолированных механик, таких как битвы в RPG, головоломки или мини-игры в открытом мире, без монолитного кода. Вы научитесь управлять несколькими игровыми контекстами, переключать их и поддерживать чистую архитектуру.

Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.

Живой запуск

Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.

Исходный код


const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    parent: 'phaser-example',
    physics: {
        default: "arcade",
        arcade: {
            debug: false,
            gravity: { y: 0 }
        }
    },
    scene: [ Preloader, WorldMap, SubGame ]
};

const game = new Phaser.Game(config);

Что такое SubGame?

SubGame — это не официальный термин Phaser, а распространенный паттерн проектирования. По сути, это отдельная сцена Phaser (например, SubGame в примере), которая загружается и управляется из другой, родительской сцены (в данном случае, WorldMap).

Основная идея — инкапсуляция. Вся логика, ресурсы, объекты и обработчики событий для мини-игры содержатся внутри этой под-сцены. Это позволяет: - Четко разделять ответственность между разными частями игры. - Легко включать и выключать целые игровые режимы. - Избегать конфликтов систем, например, физики или ввода.

В предоставленном примере конфигурации игры видно, что движок Phaser будет использовать три сцены, перечисленные в массиве scene.

Анализ конфигурации игры

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

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    parent: 'phaser-example',
    physics: {
        default: "arcade",
        arcade: {
            debug: false,
            gravity: { y: 0 }
        }
    },
    scene: [ Preloader, WorldMap, SubGame ]
};

Давайте разберем ключевые моменты: - scene: [ Preloader, WorldMap, SubGame ]: Это массив классов сцен. Phaser создаст их экземпляры в этом порядке. Первой запустится сцена Preloader, затем управление перейдет к WorldMap. Сцена SubGame будет создана, но не запущена, до явного вызова. - physics.default: "arcade": Включает простой и быстрый физический движок Arcade. Обратите внимание, что gravity: { y: 0 } отключает гравитацию по умолчанию, что типично для топ-даун игр или игр с управляемым движением. - Важно: эта физическая конфигурация будет глобальной для всех сцен, если они не переопределят ее локально.

Как запустить SubGame из основной сцены

Запуск мини-игры из основной сцены (например, WorldMap) осуществляется с помощью менеджера сцен Phaser. Предположим, в WorldMap игрок взаимодействует с объектом (сундуком, NPC), которое должно запустить мини-игру.

В методе обработки взаимодействия в классе WorldMap вам нужно будет вызвать метод this.scene.launch() или this.scene.start().

// Пример кода внутри класса WorldMap
openMiniGame() {
    // Метод launch() запускает сцену SubGame параллельно с текущей.
    // Текущая сцена (WorldMap) остается активной, но может быть приостановлена.
    this.scene.launch('SubGame');

    // Часто основную сцену приостанавливают, чтобы ее логика не работала фоном.
    this.scene.pause();
}

Ключевое отличие между launch() и start(): - launch(): Запускает сцену параллельно. Вы можете управлять несколькими сценами одновременно (например, для эффекта наложения). - start(): Останавливает текущую сцену и запускает новую. Это полное переключение контекста.

Для паттерна SubGame чаще используют launch(), чтобы позже легко вернуться в основную сцену.

Как вернуться из SubGame обратно

Завершение мини-игры и возврат в основной мир также происходит через менеджер сцен. Логика закрытия должна быть внутри класса SubGame.

Например, при достижении цели в мини-игре или по нажатию кнопки "Выход" нужно остановить текущую SubGame и возобновить основную сцену.

// Пример кода внутри класса SubGame
completeMiniGame() {
    // 1. Останавливаем (и уничтожаем) текущую сцену SubGame.
    this.scene.stop();

    // 2. Возобновляем основную сцену (WorldMap), которую мы ранее приостановили.
    //    Важно: обращаемся к сцене по ее системному ключу (указанному в game.config).
    this.scene.resume('WorldMap');
}
Важные нюансы:
- `this.scene.stop()`: Останавливает именно эту сцену (`SubGame`). Все ее игровые объекты, таймеры и слушатели событий будут корректно уничтожены, что предотвращает утечки памяти.
- `this.scene.resume('WorldMap')`: Снова активирует логику обновления (`update`) и рендеринг сцены `WorldMap`. Игрок увидит мир в том же состоянии, в котором его оставил.
- Можно передавать данные между сценами через аргументы методов `launch()` и `start()`, что полезно для настройки мини-игры.

Практические советы по реализации

Чтобы паттерн SubGame работал гладко, следуйте этим рекомендациям:

1. **Изоляция данных:** SubGame должна быть максимально независима. Выносите все ее конфигурации, текстуры и звуки в локальные свойства класса или отдельный файл данных.

2. **Управление вводом:** Убедитесь, что события клавиатуры или мыши обрабатываются только активной сценой. Phaser делает это автоматически для сцены на переднем плане, но если сцены запущены параллельно, может потребоваться ручное управление фокусом ввода.

3. **Очистка ресурсов:** Все созданные в create() объекты будут автоматически уничтожены при вызове scene.stop(). Однако, если вы создавали слушатели событий за пределами Phaser (например, document.addEventListener), их нужно удалять в методе shutdown() или destroy() класса сцены.

// В классе SubGame
shutdown() {
    // Явная очистка кастомных слушателей, если они были добавлены.
    document.removeEventListener('customEvent', this.handler);
}

4. **Системный ключ сцены:** Убедитесь, что вы обращаетесь к сценам по правильным ключам. По умолчанию Phaser использует имя класса как ключ ('WorldMap', 'SubGame'). Вы можете задать его явно, передав строку конфигурации в super() внутри конструктора сцены.

Что попробовать дальше

Паттерн SubScene — это мощный инструмент для структурирования игр на Phaser. Он превращает монолитную сцену в модульную систему, где каждая мини-игра живет в отдельном, тестируемом контейнере. Для экспериментов попробуйте

  1. Передавать параметры из WorldMap в SubGame (например, сложность)
  2. Создать менеджер под-игр, который бы управлял их очередностью
  3. Реализовать плавные переходы между сценами с помощью затемнений или анимаций камеры