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

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

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

Живой запуск

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

Исходный код


// Test whether transforming a Container matches the expected transforms.
class Example extends Phaser.Scene {
    preload ()
    {
        
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('block', 'assets/sprites/block.png');
    }

    create ()
    {
        const bg = this.add.image(0, 0, 'block').setOrigin(0).setScale(10).setTint(0x404040);

        // Create a set of game objects.
        const sprite1 = this.add.sprite(0, 0, 'block');
        const blitter1 = this.add.blitter(0, 0, 'block');
        blitter1.create(100, 0);
        const rect1 = this.add.rectangle(200, 0, 128, 128, 0xffffff);

        this.tweens.add({
            targets: sprite1,
            rotation: 1,
            duration: 1000,
            ease: 'Sine.easeInOut',
            yoyo: true,
            repeat: -1
        });

        this.tweens.add({
            targets: sprite1,
            scale: 1.5,
            duration: 1000,
            ease: 'Sine.easeInOut',
            yoyo: true,
            repeat: -1
        });

        // Create a set of game objects and add them to a Container.
        const sprite2 = this.add.sprite(0, 0, 'block');
        const blitter2 = this.add.blitter(0, 0, 'block');
        blitter2.create(100, 0);
        const rect2 = this.add.rectangle(200, 0, 128, 128, 0xffffff);
        const container = this.add.container(200, 200, [sprite2, blitter2, rect2]);

        this.tweens.add({
            targets: container,
            rotation: 1,
            duration: 1000,
            ease: 'Sine.easeInOut',
            yoyo: true,
            repeat: -1
        });

        this.tweens.add({
            targets: container,
            scale: 1.5,
            duration: 1000,
            ease: 'Sine.easeInOut',
            yoyo: true,
            repeat: -1
        });

        // Create a set of game objects and nest them inside several Containers.
        const sprite3 = this.add.sprite(0, 0, 'block');
        const blitter3 = this.add.blitter(0, 0, 'block');
        blitter3.create(100, 0);
        const rect3 = this.add.rectangle(200, 0, 128, 128, 0xffffff);
        const container1 = this.add.container(200, 200, [sprite3, blitter3, rect3]);
        const container2 = this.add.container(200, 200, [container1]);
        const container3 = this.add.container(200, 200, [container2]);

        this.tweens.add({
            targets: [container3, container2, container1],
            rotation: 1,
            duration: 1000,
            ease: 'Sine.easeInOut',
            yoyo: true,
            repeat: -1
        });

        this.tweens.add({
            targets: [container3, container2, container1],
            scale: 1.5,
            duration: 1000,
            ease: 'Sine.easeInOut',
            yoyo: true,
            repeat: -1
        });

        // Transform the camera, to ensure no transform issues are sneaking in.
        this.cameras.main.setZoom(0.5).setScroll(-100, -100).setRotation(1);
    }
}

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

const game = new Phaser.Game(config);

Зачем нужны контейнеры?

Контейнер (Phaser.GameObjects.Container) — это особый тип игрового объекта, который сам по себе не отрисовывается, но может содержать в себе другие объекты (спрайты, текст, графику). Его главная задача — служить родительской системой координат для своих детей.

Все трансформации, примененные к контейнеру, автоматически наследуются его содержимым. Например, если вы перемещаете контейнер, все его дочерние объекты движутся вместе с ним относительно его позиции. Это позволяет: * Группировать логически связанные объекты (например, персонаж + оружие + индикатор здоровья). * Эффективно управлять сложными анимациями всей группы. * Упрощать расчёты позиционирования в интерфейсах.

В примере мы создаём три группы одинаковых объектов, чтобы сравнить их поведение.

Объекты вне контейнера (базовый уровень)

Первая группа состоит из трёх независимых объектов: спрайта (Sprite), блиттера (Blitter) и прямоугольника (Rectangle). Они добавлены напрямую на сцену и не имеют общего родителя.

const sprite1 = this.add.sprite(0, 0, 'block');
const blitter1 = this.add.blitter(0, 0, 'block');
blitter1.create(100, 0);
const rect1 = this.add.rectangle(200, 0, 128, 128, 0xffffff);

К каждому из этих объектов можно применять твины (анимации) индивидуально. В коде твин анимирует только sprite1, заставляя его вращаться и менять масштаб. blitter1 и rect1 остаются неподвижными. Это даёт полный контроль над каждым объектом, но управление группой как единым целым становится сложнее.

Объекты внутри одного контейнера

Вторая группа включает такие же три объекта, но они добавлены в контейнер при его создании.

const sprite2 = this.add.sprite(0, 0, 'block');
const blitter2 = this.add.blitter(0, 0, 'block');
blitter2.create(100, 0);
const rect2 = this.add.rectangle(200, 0, 128, 128, 0xffffff);
const container = this.add.container(200, 200, [sprite2, blitter2, rect2]);

Ключевое отличие: теперь твины применяются не к отдельным объектам, а к самому контейнеру container.

this.tweens.add({
    targets: container,
    rotation: 1,
    // ...
});

В результате вращается и масштабируется весь контейнер, а вместе с ним — все его дочерние объекты. Они движутся как единое целое относительно точки (200, 200) — позиции контейнера на сцене. Это мощный способ анимировать сложную сборку одной командой.

Вложенные контейнеры и наследование трансформаций

Третья группа демонстрирует более сложный случай — цепочку вложенных контейнеров. Объекты помещаются в container1, который, в свою очередь, добавляется в container2, а тот — в container3.

const container1 = this.add.container(200, 200, [sprite3, blitter3, rect3]);
const container2 = this.add.container(200, 200, [container1]);
const container3 = this.add.container(200, 200, [container2]);

Твины в этом случае применяются ко всем трём контейнерам одновременно.

this.tweens.add({
    targets: [container3, container2, container1],
    rotation: 1,
    // ...
});

Здесь работает принцип наследования трансформаций. Физическое положение, вращение и масштаб объекта sprite3 на экране вычисляются как результат последовательного применения трансформаций всех его родителей: сначала container1, затем container2, и наконец container3. Это позволяет создавать очень сложные иерархические анимации (например, планета, вращающаяся вокруг звезды, которая сама движется по орбите).

Важные технические детали

1. **Система координат:** Позиция дочернего объекта (`x,y) задаётся относительно точки(0, 0)` его непосредственного родительского контейнера. 2. **Композиция трансформаций:** При вложенности трансформации перемножаются. Например, если container2 имеет масштаб 2, а container1 внутри него — масштаб 1.5, то итоговый масштаб объектов в container1 будет равен 3 (2 * 1.5). 3. **Порядок применения:** В примере твины применяются ко всем контейнерам в цепочке одновременно. Если бы они применялись к разным контейнерам в разное время, эффект был бы иным. 4. **Трансформация камеры:** В конце примера камера также масштабируется, сдвигается и вращается. Это проверка того, что трансформации контейнеров работают корректно и в глобальной системе координат сцены, независимо от вида камеры.

this.cameras.main.setZoom(0.5).setScroll(-100, -100).setRotation(1);

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

Контейнеры в Phaser — это фундаментальный механизм для организации сцены. Они превращают разрозненные объекты в управляемые группы, где трансформации родителя автоматически применяются ко всем потомкам. Используйте одиночные контейнеры для простых групп (игрок, платформа с декорациями), а вложенные — для сложных иерархических структур (солнечная система, многосоставной персонаж с подвижными частями). **Идеи для экспериментов:** 1. Попробуйте анимировать позицию (`x,y`) у контейнеров из второго и третьего примера и понаблюдайте за разницей в траекториях. 2. Добавьте объект в контейнер после его создания с помощью метода container.add(). 3. Поэкспериментируйте с изменением точки происхождения (setOrigin) у дочерних спрайтов внутри контейнера и посмотрите, как это влияет на вращение группы.