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

В одном HTML-документе можно запустить несколько независимых экземпляров Phaser.Game. Эта техника полезна для создания раздельных игровых областей, изолированных демо-примеров или модульных интерфейсов, где каждая часть живёт по своим правилам. Статья разберёт практический пример с двумя играми на одном экране, покажет, как они не мешают друг другу, и объяснит ключевые моменты их настройки и взаимодействия.

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

Живой запуск

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

Исходный код


// Game 1
class Example1 extends Phaser.Scene
{
    constructor ()
    {
        super();
    }

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('taikodrummaster', 'assets/pics/taikodrummaster.jpg');
        this.load.image('sukasuka-chtholly', 'assets/pics/sukasuka-chtholly.png');
    }

    create ()
    {
        this.add.image(400, 300, 'taikodrummaster');

        const chtholly = this.add.image(400, 500, 'sukasuka-chtholly').setInteractive();

        const tween = this.tweens.add({
            targets: chtholly,
            y: 600,
            ease: 'Sine.easeInOut',
            duration: 2000,
            yoyo: true,
            repeat: -1
        });

        chtholly.on('pointerdown', function () {

            if (tween.isPlaying())
            {
                tween.pause();
            }
            else
            {
                tween.resume();
            }

        });
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    parent: 'phaser-example',
    scene: [ Example1 ]
};

const game1 = new Phaser.Game(config);


// Game 2
class Example2 extends Phaser.Scene
{
    constructor ()
    {
        super();
    }

    preload ()
    {
        // this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('taikodrummaster', 'assets/pics/taikodrummaster.jpg');
        this.load.image('sukasuka-chtholly', 'assets/pics/sukasuka-chtholly.png');
    }

    create ()
    {
        this.add.image(400, 300, 'taikodrummaster');

        const chtholly = this.add.image(400, 500, 'sukasuka-chtholly').setInteractive();

        const tween = this.tweens.add({
            targets: chtholly,
            x: 200,
            ease: 'Sine.easeInOut',
            duration: 2000,
            yoyo: true,
            repeat: -1
        });

        chtholly.on('pointerdown', function () {

            if (tween.isPlaying())
            {
                tween.pause();
            }
            else
            {
                tween.resume();
            }

        });
    }
}

const config2 = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    parent: 'phaser-example',
    scene: [ Example2 ]
};

const game2 = new Phaser.Game(config2);

Анатомия примера: два независимых мира

Исходный код демонстрирует создание двух полностью автономных игровых экземпляров (game1 и game2). Каждый из них имеет собственную конфигурацию (config, config2) и свой класс сцены (Example1, Example2). Несмотря на то что они используют одни и те же ассеты (изображения), загружают и отображают их они в своих собственных контекстах — this.load и this.add каждого экземпляра ссылаются на свои собственные системы.

Оба экземпляра создаются с помощью конструктора new Phaser.Game(config). Важно, что для их размещения в DOM используется один и тот же родительский элемент с id='phaser-example'. Phaser автоматически разместит оба canvas внутри этого контейнера.

Настройка конфигурации и загрузка ресурсов

Конфигурация каждого экземпляра задаёт базовые параметры рендеринга и указывает, какую сцену (или список сцен) использовать. Обратите внимание, что в Example1 явно задан setBaseURL, а в Example2 эта строка закомментирована. Это показывает, что настройки загрузчика (this.load) локальны для каждой игры.

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    parent: 'phaser-example',
    scene: [ Example1 ]
};
preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.image('taikodrummaster', 'assets/pics/taikodrummaster.jpg');
    this.load.image('sukasuka-chtholly', 'assets/pics/sukasuka-chtholly.png');
}

Создание сцены и интерактивность

В методе create() каждой сцены происходит одно и то же: добавляется фоновое изображение, а затем интерактивное изображение персонажа, к которому привязан tween. Ключевое различие — ось анимации: в Example1 объект двигается по вертикали (свойство `y), а вExample2— по горизонтали (свойствоx`). Это наглядно показывает их независимость.

Объект делается интерактивным с помощью .setInteractive(). Это позволяет назначать обработчики событий, такие как pointerdown.

const chtholly = this.add.image(400, 500, 'sukasuka-chtholly').setInteractive();

const tween = this.tweens.add({
    targets: chtholly,
    y: 600, // В Example2 здесь будет x: 200
    ease: 'Sine.easeInOut',
    duration: 2000,
    yoyo: true,
    repeat: -1
});

Управление анимацией по клику

Обработчик события pointerdown проверяет состояние твина с помощью метода tween.isPlaying(). В зависимости от результата, твин либо приостанавливается (pause()), либо возобновляет проигрывание (resume()). Важно, что твин создаётся через this.tweens.add, что делает его частью системы твинов конкретного экземпляра игры. Управление твином из game1 никак не повлияет на твин в game2.

chtholly.on('pointerdown', function () {
    if (tween.isPlaying())
    {
        tween.pause();
    }
    else
    {
        tween.resume();
    }
});

Потенциальные проблемы и их решение

1. **Конфликт ресурсов**: Если бы оба экземпляра пытались загрузить один и тот же ресурс по разным URL (из-за разного BaseURL), это могло бы привести к ошибкам. В нашем примере это решено либо одинаковым BaseURL, либо загрузкой из дефолтных путей. 2. **Производительность**: Два активных рендерера и две физические петли — это двойная нагрузка. Следите за оптимизацией, особенно на мобильных устройствах. 3. **Взаимодействие между экземплярами**: По умолчанию его нет. Для связи потребуется использовать глобальную шину событий (например, EventEmitter), общее состояние или postMessage, если экземпляры в разных iframe.

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

Запуск нескольких экземпляров Phaser — мощный приём для структурирования сложных проектов. Каждый экземпляр изолирован, что упрощает отладку и переиспользование кода. Для экспериментов попробуйте: изменить parent у одного из конфигов на другой DOM-элемент; реализовать обмен сообщениями между game1 и game2 через кастомную шину событий; или динамически создавать и уничтожать экземпляры игры в ответ на действия пользователя.