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

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

Версия 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('rick', 'assets/demoscene/large-raster32.png');
        // this.load.image('rick', 'assets/demoscene/raster-bw-800x16.png');
        // this.load.image('rick2', 'assets/demoscene/rastercarpet32.png');
        this.load.image('rick2', 'assets/demoscene/rastercarpet32.png');
        this.load.image('rick', 'assets/demoscene/multi-color-raster.png');
    }

    create ()
    {
        const sprite1 = this.add.sprite(0, 100, 'rick');
        const sprite2 = this.add.sprite(-100, 0, 'rick2').setAngle(90);
        const sprite3 = this.add.sprite(0, -100, 'rick').setAngle(180);
        const sprite4 = this.add.sprite(100, 0, 'rick2').setAngle(270);

        const containers = [];

        for (let i = 0; i < 128; i++)
        {
            const container = this.add.container(400, 300);

            if (i > 0)
            {
                container.setExclusive(false);
            }

            container.add([ sprite1, sprite2, sprite3, sprite4 ]);

            containers.push(container);
        }

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

        this.tweens.add({
            targets: sprite2,
            x: 300,
            ease: 'Sine.easeInOut',
            duration: 4000,
            repeat: -1,
            yoyo: true
        });

        this.tweens.add({
            targets: sprite3,
            y: 200,
            ease: 'Sine.easeInOut',
            duration: 4000,
            repeat: -1,
            yoyo: true
        });

        this.tweens.add({
            targets: sprite4,
            x: -300,
            ease: 'Sine.easeInOut',
            duration: 4000,
            repeat: -1,
            yoyo: true
        });

        this.tweens.add({
            targets: containers,
            angle: { value: 360, duration: 6000 },
            scaleX: { value: 2, duration: 3000, yoyo: true, ease: 'Quad.easeInOut' },
            scaleY: { value: 4, duration: 3000, yoyo: true, ease: 'Cubic.easeInOut' },
            repeat: -1,
            delay: function (target, key, value, index, total, tween)
            {
                return index * 32;
            }
        });
    }
}

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

const game = new Phaser.Game(config);

Создание и настройка спрайтов

Основу эффекта составляют четыре спрайта, загруженные в методе preload. В create они создаются и позиционируются вокруг центра сцены (точки 0,0), образуя крест. Каждому спрайту задается свой угол поворота методом .setAngle().

const sprite1 = this.add.sprite(0, 100, 'rick');
const sprite2 = this.add.sprite(-100, 0, 'rick2').setAngle(90);
const sprite3 = this.add.sprite(0, -100, 'rick').setAngle(180);
const sprite4 = this.add.sprite(100, 0, 'rick2').setAngle(270);

Массовое создание контейнеров

Ключевой элемент — Container. Это игровой объект, который может содержать в себе другие объекты, выступая для них родителем. В цикле создается 128 контейнеров, каждый центрируется в точке (400, 300). Все четыре спрайта добавляются в каждый контейнер методом .add(). Важный нюанс: по умолчанию объект может принадлежать только одному Container. Флаг exclusive управляет этим. Для первого контейнера он true (по умолчанию), для всех последующих — false, что позволяет одним и тем же спрайтам находиться во множестве контейнеров одновременно.

for (let i = 0; i < 128; i++) {
    const container = this.add.container(400, 300);
    if (i > 0) {
        container.setExclusive(false);
    }
    container.add([ sprite1, sprite2, sprite3, sprite4 ]);
    containers.push(container);
}

Анимация внутренних спрайтов

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

this.tweens.add({
    targets: sprite1,
    y: -200,
    ease: 'Sine.easeInOut',
    duration: 4000,
    repeat: -1,
    yoyo: true
});
// ... аналогичные твины для sprite2, sprite3, sprite4

Сложная анимация контейнеров

Главный визуальный эффект достигается анимацией самих контейнеров. Один твин, назначенный на массив containers, управляет сразу всеми 128 объектами. Он выполняет три действия одновременно: непрерывный поворот на 360 градусов, пульсирующее масштабирование по осям X и Y с разными функциями плавности (ease). Параметр delay с функцией-задержкой создает "волну" анимации, запуская твин для каждого следующего контейнера с небольшим опозданием.

this.tweens.add({
    targets: containers,
    angle: { value: 360, duration: 6000 },
    scaleX: { value: 2, duration: 3000, yoyo: true, ease: 'Quad.easeInOut' },
    scaleY: { value: 4, duration: 3000, yoyo: true, ease: 'Cubic.easeInOut' },
    repeat: -1,
    delay: function (target, key, value, index, total, tween) {
        return index * 32;
    }
});

Итоговая механика и производительность

В результате мы имеем 128 контейнеров, каждый из которых: 1. Содержит четыре анимированных спрайта. 2. Независимо вращается и масштабируется. 3. Запускает анимацию с задержкой относительно соседей.

Использование container.setExclusive(false) для общих дочерних объектов — это ключ к высокой производительности. Phaser рендерит всего четыре уникальных текстуры спрайтов, но трансформирует их 128 раз в соответствии с параметрами каждого контейнера. Это эффективнее, чем создание 512 отдельных спрайтов.

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

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