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

Создание сложных визуальных эффектов, где сотни объектов движутся и вращаются согласованно, — частая задача в играх. Ручное управление каждым спрайтом через циклы в `update()` быстро приводит к беспорядку в коде и падению производительности. Встроенный модуль `Phaser.Actions` предоставляет набор методов для массового применения трансформаций к массивам игровых объектов. В этой статье мы разберем, как с помощью `IncXY` и `Rotate` создать гипнотический эффект двух слоев вращающихся фруктов, движущихся по круговым траекториям. Этот подход не только элегантен, но и высокоэффективен.

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

Живой запуск

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

Исходный код


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

        this.move = 0;
        this.layer1 = [];
        this.layer2 = [];
    }

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.atlas('atlas', 'assets/tests/fruit/veg.png', 'assets/tests/fruit/veg.json');
    }

    create ()
    {
        for (let i = 0; i < 1024; i++)
        {
            const x = Phaser.Math.Between(100, 700);
            const y = Phaser.Math.Between(100, 500);
            const frame = `veg0${Phaser.Math.Between(1, 9)}`;

            this.layer1.push(this.add.image(x, y, 'atlas', frame));
        }

        for (let i = 0; i < 1024; i++)
        {
            const x = Phaser.Math.Between(100, 700);
            const y = Phaser.Math.Between(100, 500);
            const frame = `veg0${Phaser.Math.Between(1, 9)}`;

            this.layer2.push(this.add.image(x, y, 'atlas', frame));
        }
    }

    update ()
    {
        Phaser.Actions.IncXY(this.layer1, Math.cos(this.move), Math.sin(this.move));
        Phaser.Actions.Rotate(this.layer1, -0.01);

        Phaser.Actions.IncXY(this.layer2, -Math.cos(this.move), -Math.sin(this.move));
        Phaser.Actions.Rotate(this.layer2, 0.01);

        this.move += 0.01;
    }
}

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

const game = new Phaser.Game(config);

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

Класс сцены Example инициализирует необходимые свойства в конструкторе. Свойства layer1 и layer2 — это пустые массивы, которые впоследствии будут хранить ссылки на наши игровые объекты. Свойство move будет использоваться как счетчик для расчета смещения.

В методе preload загружается атлас текстур. Атлас veg.png содержит несколько кадров (frame) с изображениями фруктов и овощей, описанные в JSON-файле.

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.atlas('atlas', 'assets/tests/fruit/veg.png', 'assets/tests/fruit/veg.json');
}

Создание двух слоев объектов

В методе create мы создаем 1024 спрайта для первого слоя и еще 1024 — для второго. Каждый спрайт — это изображение (this.add.image) со случайными координатами в пределах заданной области и случайным кадром из атласа (от veg01 до veg09). Созданные объекты сразу помещаются в соответствующие массивы this.layer1 и this.layer2.

for (let i = 0; i < 1024; i++)
{
    const x = Phaser.Math.Between(100, 700);
    const y = Phaser.Math.Between(100, 500);
    const frame = `veg0${Phaser.Math.Between(1, 9)}`;

    this.layer1.push(this.add.image(x, y, 'atlas', frame));
}

Аналогичный цикл заполняет this.layer2. Теперь у нас есть два массива, каждый с тысячей независимых спрайтов, готовых к анимации.

Массовое обновление позиций с `Phaser.Actions.IncXY`

Метод update вызывается на каждом кадре игры. Именно здесь Phaser.Actions проявляет свою мощь. Метод Phaser.Actions.IncXY принимает массив объектов и значения инкремента для осей X и Y, затем прибавляет эти значения к текущим координатам каждого объекта в массиве.

Ключевой момент: для расчета смещения используется тригонометрия. Значение this.move плавно увеличивается каждые кадр, создавая непрерывно меняющийся угол.

Phaser.Actions.IncXY(this.layer1, Math.cos(this.move), Math.sin(this.move));

Здесь Math.cos(this.move) и Math.sin(this.move) дают координаты точки на единичной окружности. Это заставляет все объекты в layer1 двигаться по плавной круговой траектории. Для layer2 значения инкремента инвертированы (-Math.cos, -Math.sin), что заставляет этот слой двигаться в противоположном направлении.

Массовое вращение с `Phaser.Actions.Rotate`

Параллельно с движением применяется вращение. Метод Phaser.Actions.Rotate принимает массив объектов и угол в радианах, на который нужно повернуть каждый объект вокруг его центра.

Phaser.Actions.Rotate(this.layer1, -0.01);
Phaser.Actions.Rotate(this.layer2, 0.01);

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

Итоговый шаг в update — инкремент счетчика this.move, который обеспечивает плавное изменение параметров движения для следующего кадра.

this.move += 0.01;

Настройка игры и конфигурация

Код завершается созданием экземпляра игры Phaser.Game с конфигурационным объектом. В нем задаются базовые параметры: тип рендерера (AUTO), размеры холста, цвет фона, ID родительского HTML-элемента и главная сцена (Example).

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

const game = new Phaser.Game(config);

Эта конфигурация является стандартной точкой входа для большинства проектов на Phaser 3.

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

Использование Phaser.Actions для групповых операций — это профессиональный и производительный подход к анимации множества объектов. Вместо обработки каждого спрайта в цикле вы одним вызовом применяете трансформацию ко всему массиву. Для экспериментов попробуйте: изменить формулы расчета инкремента для IncXY на другие математические функции (например, синусоидальные волны разной частоты); применить другие методы Actions, такие как SetScale или SetTint; или разделить объекты на большее количество слоев с разным поведением, создавая еще более сложные композиции.