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

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

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

Живой запуск

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

Исходный код


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

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.spritesheet('balls', 'assets/sprites/balls.png', { frameWidth: 17, frameHeight: 17 });
    }

    create ()
    {
        const circle = new Phaser.Geom.Circle(400, 300, 220);

        this.group = this.add.group({ key: 'balls', frame: [0, 1, 5], repeat: 10 });

        Phaser.Actions.PlaceOnCircle(this.group.getChildren(), circle);

        this.tween = this.tweens.addCounter({
            from: 220,
            to: 100,
            duration: 3000,
            delay: 2000,
            ease: 'Sine.easeInOut',
            repeat: -1,
            yoyo: true
        });
    }

    update ()
    {
        Phaser.Actions.RotateAroundDistance(this.group.getChildren(), { x: 400, y: 300 }, 0.02, this.tween.getValue());
    }
}

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

const game = new Phaser.Game(config);

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

В методе preload мы загружаем спрайтшит — изображение, содержащее несколько кадров-спрайтов в одной картинке. Это эффективный способ хранения похожих ассетов.

this.load.spritesheet('balls', 'assets/sprites/balls.png', { frameWidth: 17, frameHeight: 17 });

Здесь 'balls' — это ключ, по которому мы будем обращаться к ресурсу. Второй параметр — путь к файлу. Третий параметр — объект конфигурации, где мы указываем ширину и высоту одного кадра (фрейма) в пикселях. Движок нарезает большое изображение на множество маленьких спрайтов размером 17x17.

Создание группы и геометрической фигуры

В методе create происходит основная настройка. Сначала мы определяем геометрическую фигуру — круг, который будет служить нашим шаблоном для расстановки объектов.

const circle = new Phaser.Geom.Circle(400, 300, 220);

Конструктор Phaser.Geom.Circle принимает три аргумента: координаты центра по осям X и Y и радиус. В нашем случае круг будет расположен в центре экрана (400, 300) с начальным радиусом 220 пикселей.

Далее мы создаем группу объектов (Group). Группа — это мощный контейнер Phaser для управления множеством однотипных игровых объектов.

this.group = this.add.group({ key: 'balls', frame: [0, 1, 5], repeat: 10 });

При создании группы мы передаем конфигурационный объект: - key: ключ загруженного спрайтшита. - frame: массив индексов кадров, которые будут использоваться. Группа будет создавать объекты, циклически перебирая эти кадры (0, 1, 5, 0, 1, 5...). - repeat: число, указывающее, сколько *дополнительных* раз повторить последовательность кадров. Вместе с тремя исходными кадрами это даст 33 объекта (3 кадра * (10 повторов + 1)).

Статичная расстановка объектов по окружности

Теперь, когда у нас есть группа из 33 спрайтов и геометрический круг, мы можем их совместить. Для этого используется статический метод Phaser.Actions.PlaceOnCircle.

Phaser.Actions.PlaceOnCircle(this.group.getChildren(), circle);

Метод принимает два основных параметра: 1. Массив объектов для размещения. Мы получаем его вызовом this.group.getChildren(). 2. Экземпляр Phaser.Geom.Circle, по границе которого будут равномерно распределены все объекты из массива.

После выполнения этой строки все 33 шара окажутся равномерно разбросаны по линии нашей окружности.

Анимация радиуса с помощью Tween

Чтобы круг "дышал", мы создаем анимацию (твин) для числового значения — в данном случае, для радиуса. Для этого используется менеджер твинов сцены this.tweens и метод addCounter.

this.tween = this.tweens.addCounter({
    from: 220,
    to: 100,
    duration: 3000,
    delay: 2000,
    ease: 'Sine.easeInOut',
    repeat: -1,
    yoyo: true
});

- from / to: начальное и конечное значение. Радиус будет меняться от 220 до 100 пикселей. - duration: длительность одного цикла анимации в миллисекундах (3000 мс = 3 секунды). - delay: задержка перед началом анимации. - ease: функция плавности (Sine.easeInOut обеспечивает мягкое ускорение и замедление). - repeat: -1: анимация повторяется бесконечно. - yoyo: true: после достижения конечного значения анимация проигрывается в обратном порядке, создавая эффект пульсации.

Объект this.tween позволяет в любой момент запросить текущее анимированное значение с помощью метода getValue().

Динамическое вращение в игровом цикле

Статичная расстановка — это только половина дела. В методе update, который вызывается на каждом кадре, мы заставляем всю конструкцию вращаться и изменять свой радиус в реальном времени.

Phaser.Actions.RotateAroundDistance(this.group.getChildren(), { x: 400, y: 300 }, 0.02, this.tween.getValue());
Метод `RotateAroundDistance` выполняет две операции одновременно для каждого объекта в массиве:
1. **Вращение**: Второй параметр — точка (`{x, y}`), вокруг которой происходит вращение (центр экрана). Третий параметр (`0.02`) — угол в радианах, на который объекты поворачиваются каждый кадр.
2. **Изменение расстояния**: Четвертый параметр — это новое расстояние от центра вращения до каждого объекта. Мы передаем текущее значение из нашего твина `this.tween.getValue()`. Так как это значение постоянно меняется от 100 до 220, наш круг из шаров синхронно сжимается и расширяется.

Таким образом, в update мы каждый кадр пересчитываем позицию всех 33 шаров, совмещая вращение и пульсацию радиуса.

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

Комбинация Phaser.Actions.PlaceOnCircle и RotateAroundDistance — это лаконичный и эффективный способ создания сложной круговой анимации из десятков объектов. Вы управляете всей системой как единым целым, а не каждым спрайтом в отдельности. Для экспериментов попробуйте: заменить Circle на Ellipse или Line; использовать другой ease-эффект для твина, например Bounce.easeOut; изменять угол вращения в зависимости от времени или внешних событий; или применить RotateAroundDistance к нескольким независимым группам, создавая сложные планетарные системы.