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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('eye', 'assets/pics/lance-overdose-loader-eye.png');
        this.load.image('grid', 'assets/pics/debug-grid-1920x1920.png');
    }

    create ()
    {
        this.add.image(0, 0, 'grid').setOrigin(0).setAlpha(0.5);

        const bounds = new Phaser.Geom.Rectangle(0, 0, 1024, 768);

        const containers = [];

        let container = this.add.container(0, 0).setName('Container1');

        containers.push(container);

        window['Container1'] = container;

        let c = 1;

        for (let i = 0; i < 128; i++)
        {
            const x = Phaser.Math.Between(bounds.left, bounds.right);
            const y = Phaser.Math.Between(bounds.top, bounds.bottom);

            const s = this.add.sprite(x, y, 'eye').setName(`Sprite${i}`);
            window[`Sprite${i}`] = s;

            s.setInteractive();
            s.setAngle(Phaser.Math.Between(0, 359));
            s.setScale(0.1 + Math.random());

            if (i > 0 && i % 8 === 0)
            {
                container = this.add.container(0, 0).setName(`Container${c}`);

                if (c > 1)
                {
                    const p = Phaser.Utils.Array.GetRandom(containers).add(container);
                    console.log(container.name, 'child of', p.name);
                }

                containers.push(container);

                window[`Container${c}`] = container;

                c++;
            }

            Phaser.Utils.Array.GetRandom(containers).add(s);
        }

        this.input.on('gameobjectover', (pointer, gameObject) =>
        {

            gameObject.setTint(0xff0000);

            // console.log(gameObject.name);
            // console.table(gameObject.getIndexList());

        });

        this.input.on('gameobjectout', (pointer, gameObject) =>
        {

            gameObject.clearTint();

        });

        /*
        this.tweens.add({
            targets: containers,
            angle: 360,
            duration: 20000,
            ease: 'Linear',
            repeat: -1
        });
        */
    }
}

const config = {
    type: Phaser.WEBGL,
    parent: 'phaser-example',
    width: 1024,
    height: 768,
    backgroundColor: '#000000',
    scene: Example
};

const game = new Phaser.Game(config);

Подготовка сцены и создание базового контейнера

В методе create мы начинаем с добавления фонового изображения сетки для визуальной ориентации. Затем создается прямоугольная область (bounds), которая будет использоваться для случайного размещения спрайтов.

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

Мы создаем массив containers для хранения всех контейнеров и добавляем в него первый. Также для отладки контейнер добавляется в глобальный объект window.

const bounds = new Phaser.Geom.Rectangle(0, 0, 1024, 768);
const containers = [];
let container = this.add.container(0, 0).setName('Container1');
containers.push(container);
window['Container1'] = container;

Цикл создания спрайтов и иерархии контейнеров

В цикле создается 128 спрайтов с изображением глаза. Каждому спрайту задаются случайные координаты в пределах bounds, угол поворота и масштаб. Важный шаг — вызов setInteractive(). Без него объект не будет генерировать события ввода (например, наведение мыши).

const s = this.add.sprite(x, y, 'eye').setName(`Sprite${i}`);
window[`Sprite${i}`] = s;
s.setInteractive();
s.setAngle(Phaser.Math.Between(0, 359));
s.setScale(0.1 + Math.random());

Каждые 8 спрайтов создается новый контейнер. Здесь реализуется иерархия: начиная со второго контейнера, каждый новый случайным образом (Phaser.Utils.Array.GetRandom) добавляется как ребенок к одному из уже существующих контейнеров. Таким образом, мы строим древовидную структуру. Все созданные спрайты также случайным образом распределяются по всем существующим контейнерам (включая вновь созданные).

if (i > 0 && i % 8 === 0)
{
    container = this.add.container(0, 0).setName(`Container${c}`);
    if (c > 1)
    {
        const p = Phaser.Utils.Array.GetRandom(containers).add(container);
    }
    containers.push(container);
    c++;
}
Phaser.Utils.Array.GetRandom(containers).add(s);

Обработка событий наведения мыши (Hover)

Обработка событий ввода — сильная сторона Phaser. Мы навешиваем обработчики на глобальные события сцены gameobjectover (курсор наведен на объект) и gameobjectout (курсор убран с объекта).

Когда событие gameobjectover срабатывает, объект, на который навели (gameObject), получает красный оттенок с помощью setTint. При событии gameobjectout оттенок сбрасывается.

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

this.input.on('gameobjectover', (pointer, gameObject) => {
    gameObject.setTint(0xff0000);
});

this.input.on('gameobjectout', (pointer, gameObject) => {
    gameObject.clearTint();
});

Анимация контейнеров (закомментированный код)

В исходном примере есть закомментированный блок, который демонстрирует мощь контейнеров в анимации. С помощью системной анимации (tweens) можно анимировать сразу все контейнеры в массиве containers.

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

this.tweens.add({
    targets: containers,
    angle: 360,
    duration: 20000,
    ease: 'Linear',
    repeat: -1
});

Раскомментируйте этот блок, чтобы увидеть, как вся созданная структура начнет плавно вращаться, сохраняя при этом возможность взаимодействия с каждым элементом.

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

Контейнеры в Phaser — это мощный инструмент для организации сложных игровых сцен. Они позволяют управлять группами объектов как единым целым, что упрощает анимацию, позиционирование и обработку событий. В рассмотренном примере мы создали динамическую иерархию объектов, где события ввода корректно обрабатываются на любом уровне вложенности. **Идеи для экспериментов:** 1. Раскомментируйте анимацию и попробуйте анимировать не все контейнеры, а только корневые. 2. Измените логику распределения спрайтов, чтобы они добавлялись только в контейнеры нижнего уровня (листья иерархии). 3. Добавьте обработку клика (gameobjectdown) и реализуйте логику перетаскивания (drag) для контейнеров. Убедитесь, что при перетаскивании родительского контейнера двигаются и все его дети.