О чем этот пример
Создание сложных визуальных эффектов, где сотни объектов движутся и вращаются согласованно, — частая задача в играх. Ручное управление каждым спрайтом через циклы в `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; или разделить объекты на большее количество слоев с разным поведением, создавая еще более сложные композиции.
