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

В Phaser 3 игровые объекты могут находиться либо на основном дисплейном списке сцены (`Display List`), либо внутри `Container`. Умение динамически перемещать объекты между этими структурами — ключ к гибкому управлению группами спрайтов, их видимостью, трансформациями и порядком отрисовки. Эта статья на практическом примере покажет, как переключать объект между контейнером и основным списком по клику, что полезно для реализации инвентаря, перемещаемых панелей или динамических слоёв интерфейса.

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

Живой запуск

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

Исходный код


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

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('beer', 'assets/sprites/beer.png');
        this.load.image('watermelon', 'assets/sprites/watermelon.png');
        this.load.image('cake', 'assets/sprites/cake.png');
    }

    create ()
    {
        this.add.text(10, 10, 'Click Sprite to move between Container');

        const size1 = this.add.text(10, 48);
        const size2 = this.add.text(410, 48);

        this.add.line(400, 300, 0, 0, 0, 600, 0xffffff);

        const container = this.add.container();

        const clickSprite = function ()
        {
            if (this.parentContainer === container)
            {
                //  Remove from Container, this places it back on the Display List
                container.remove(this);

                this.x -= 400;
            }
            else
            {
                //  Move from Display List to Container
                //  Doing this will automatically remove it from the Display List
                container.add(this);

                this.x += 400;
            }

            size1.setText('Display List size: ' + this.scene.children.length);
            size2.setText('Container size: ' + container.length);
        }

        for (let i = 0; i < 8; i++)
        {
            const x1 = Phaser.Math.Between(64, 336);
            const y1 = Phaser.Math.Between(128, 536);

            const x2 = Phaser.Math.Between(464, 736);
            const y2 = Phaser.Math.Between(128, 536);

            const sprite1 = new Phaser.GameObjects.Sprite(this, x1, y1, 'cake');
            const sprite2 = new Phaser.GameObjects.Sprite(this, x2, y2, 'watermelon');

            sprite1.addToDisplayList();

            container.add(sprite2);

            sprite1.setInteractive();
            sprite2.setInteractive();

            sprite1.on('pointerdown', clickSprite);
            sprite2.on('pointerdown', clickSprite);
        }

        size1.setText('Display List size: ' + this.children.length);
        size2.setText('Container size: ' + container.length);
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#2d2d2d',
    parent: 'phaser-example',
    scene: Example
};

const game = new Phaser.Game(config);

Зачем нужны Container?

Container — это специальный игровой объект, который выступает как родитель для других объектов. Все дочерние элементы наследуют его трансформации (позицию, масштаб, вращение, видимость). Это позволяет управлять целой группой объектов как единым целым. В то же время, объекты на основном Display List сцены (доступном через this.children) управляются независимо.

Главное различие: объект может находиться либо на дисплейном списке сцены, либо внутри контейнера, но не в обоих местах одновременно. Методы add() и remove() контейнера автоматически обновляют эти связи.

Структура примера и загрузка ресурсов

В примере создаётся сцена с двумя областями, разделёнными вертикальной линией. Слева объекты находятся на основном Display List, справа — внутри Container. Загружаются три спрайта, но используются только два: 'cake' и 'watermelon'.

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.image('beer', 'assets/sprites/beer.png');
    this.load.image('watermelon', 'assets/sprites/watermelon.png');
    this.load.image('cake', 'assets/sprites/cake.png');
}

Обрати внимание, что спрайт 'beer' загружается, но не используется в коде — это нормально для демонстрации.

Создание контейнера и текстовых счётчиков

В методе create() создаются текстовые метки для отображения количества объектов в каждой структуре и разделительная линия. Контейнер создаётся с помощью this.add.container().

this.add.text(10, 10, 'Click Sprite to move between Container');

const size1 = this.add.text(10, 48);
const size2 = this.add.text(410, 48);

this.add.line(400, 300, 0, 0, 0, 600, 0xffffff);

const container = this.add.container();

Текстовые объекты size1 и size2 будут динамически обновляться. Линия (this.add.line) визуально разделяет экран на зону Display List (x < 400) и зону Container (x >= 400).

Функция-обработчик клика и логика перемещения

Ключевая функция clickSprite вызывается при клике на любой спрайт. Она проверяет, где сейчас находится объект (в каком родительском контейнере), и перемещает его в другую структуру, одновременно сдвигая по горизонтали для визуального эффекта.

const clickSprite = function ()
{
    if (this.parentContainer === container)
    {
        //  Удаляем из Container, объект возвращается на Display List
        container.remove(this);
        this.x -= 400;
    }
    else
    {
        //  Перемещаем с Display List в Container
        //  Это автоматически удалит объект из Display List
        container.add(this);
        this.x += 400;
    }
    // Обновляем текстовые счётчики
    size1.setText('Display List size: ' + this.scene.children.length);
    size2.setText('Container size: ' + container.length);
}

Проверка this.parentContainer === container определяет, находится ли спрайт внутри контейнера. Свойство parentContainer будет равно null, если объект на основном Display List. Метод container.remove(this) удаляет объект из контейнера, и он автоматически возвращается в this.scene.children. Метод container.add(this), наоборот, забирает объект с основного списка в контейнер. После перемещения координата X спрайта корректируется на 400 пикселей, чтобы он оставался в своей визуальной зоне. Свойство container.length показывает текущее количество объектов в контейнере.

Создание и первоначальное размещение спрайтов

В цикле создаются 16 спрайтов (8 'cake' и 8 'watermelon'). Каждая пара размещается в случайных позициях в левой и правой частях экрана. Спрайты 'cake' добавляются на основной Display List с помощью addToDisplayList(), а 'watermelon' сразу помещаются в контейнер методом container.add().

for (let i = 0; i < 8; i++)
{
    const x1 = Phaser.Math.Between(64, 336);
    const y1 = Phaser.Math.Between(128, 536);
    const x2 = Phaser.Math.Between(464, 736);
    const y2 = Phaser.Math.Between(128, 536);
    const sprite1 = new Phaser.GameObjects.Sprite(this, x1, y1, 'cake');
    const sprite2 = new Phaser.GameObjects.Sprite(this, x2, y2, 'watermelon');
    sprite1.addToDisplayList();
    container.add(sprite2);
    sprite1.setInteractive();
    sprite2.setInteractive();
    sprite1.on('pointerdown', clickSprite);
    sprite2.on('pointerdown', clickSprite);
}

Обрати внимание: спрайты создаются через new Phaser.GameObjects.Sprite, а не через фабричные методы вроде this.add.sprite. Поэтому для добавления на дисплейный список сцены у sprite1 явно вызывается addToDisplayList(). Оба спрайта делаются интерактивными (setInteractive()) и на них вешается один и тот же обработчик clickSprite.

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

Пример наглядно демонстрирует, как объекты в Phaser 3 могут динамически менять свою принадлежность к Display List или Container, обеспечивая гибкую архитектуру управления группами. Для экспериментов попробуй

  1. Добавить в контейнер трансформации (масштаб, поворот) и понаблюдать, как они применяются ко всем его дочерним элементам при перемещении
  2. Создать несколько контейнеров и реализовать перемещение объектов между ними
  3. Использовать это поведение для реализации перетаскивания предметов между разными панелями интерфейса или слоями игры