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

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

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

Живой запуск

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

Исходный код


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

    create ()
    {
        this.add.text(10, 10, 'Layer 2', { font: '16px Courier', fill: '#00ff00' });

        this.input.once('pointerdown', () => {

            this.scene.start('example1');

        });
    }
}

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

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

    create ()
    {
        this.add.text(10, 10, 'Scene 1', { font: '16px Courier', fill: '#00ff00' });

        this.add.sprite(400, 300, 'spaceman');

        const bob = this.add.container();

        bob.add(this.add.sprite(500, 400, 'spaceman'));
        bob.add(this.add.sprite(550, 500, 'spaceman'));

        const spaceman = this.add.sprite(150, 300, 'spaceman');

        const layer = this.add.layer();

        layer.add([ spaceman ]);

        this.input.once('pointerdown', () => {

            this.scene.start('example2');

        });

        console.log(this.children);
    }
}

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

const game = new Phaser.Game(config);

Разбор примера: две сцены и структура объектов

В приведённом примере создаются две простые сцены: Example (ключ 'example1') и Example2. Основное действие происходит в первой сцене. Её задача — наглядно продемонстрировать, как добавление объектов в Container и Layer влияет на их положение в иерархии.

Давайте посмотрим на метод create() сцены Example. В нём создаются несколько спрайтов и группы.

Container: группа как единый объект

Контейнер (Container) — это объект-родитель, который группирует дочерние элементы. Все трансформации (позиция, масштаб, вращение), применённые к контейнеру, автоматически влияют на всех его детей. В примере создаётся контейнер bob, в который добавляются два спрайта.

const bob = this.add.container();
bob.add(this.add.sprite(500, 400, 'spaceman'));
bob.add(this.add.sprite(550, 500, 'spaceman'));

Важно понимать, что контейнер сам становится дочерним элементом сцены. Добавленные в него спрайты больше не являются прямыми детьми сцены, они — дети контейнера bob. Это меняет их глобальные координаты и порядок обработки.

Layer: менеджер отображения, а не родитель

Слой (Layer) работает иначе. Его основная задача — управлять глубиной (z-index) и состоянием рендеринга для группы объектов, но при этом он не становится их родителем в классическом понимании. Создадим слой и добавим в него отдельный спрайт spaceman.

const spaceman = this.add.sprite(150, 300, 'spaceman');
const layer = this.add.layer();
layer.add([ spaceman ]);

Ключевое отличие: спрайт spaceman, добавленный в слой, остаётся прямым дочерним объектом сцены. Слой лишь берёт на себя управление его рендерингом в рамках своей группы. Это сохраняет независимость координат объекта.

Дерево отображения и вывод в консоль

Чтобы увидеть разницу, в конце метода create() сцены Example выводится список детей сцены.

console.log(this.children);

Если открыть консоль браузера, можно наблюдать следующую структуру: 1. Текстовый объект 'Scene 1'. 2. Первый спрайт космонавта (созданный напрямую через this.add.sprite(400, 300, 'spaceman')). 3. Контейнер bob (внутри которого находятся два спрайта-ребёнка). 4. Спрайт spaceman, который был добавлен в слой. 5. Сам объект layer.

Это подтверждает: объект в слое (spaceman) и сам слой (layer) — это два отдельных, независимых ребенка сцены. В контейнере же виден только сам bob, а его содержимое скрыто на уровень ниже.

Когда использовать Container, а когда Layer?

Выбор зависит от задачи.

**Используйте Container, если:** * Вам нужно перемещать, масштабировать или вращать группу объектов как единое целое. * Логика требует чёткой родительско-дочерней иерархии (например, части составного объекта).

**Используйте Layer, если:** * Вам нужно тонко управлять порядком отрисовки (глубиной) группы объектов относительно других элементов. * Вы хотите применять общие эффекты рендеринга (например, blend mode) к группе, но при этом сохранять независимые координаты и трансформации для каждого объекта в группе. * Важно, чтобы объекты оставались на верхнем уровне иерархии сцены для прямого доступа.

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

Container и Layer в Phaser 3 решают схожие, но структурно разные задачи по группировке. Контейнер создаёт вложенную иерархию, где группа ведёт себя как один составной объект. Слой же выступает в роли менеджера рендеринга для независимых объектов. Для экспериментов попробуйте

  1. Добавить вращение контейнеру bob и увидеть, как вращаются оба его спрайта
  2. Изменить глубину (depth) объекта layer и проследить, как это повлияет на отрисовку всех его элементов относительно других объектов сцены