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

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

Версия 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.image('bg', 'assets/skies/darkstone.png');
        this.load.image('tree', 'assets/sprites/skullcandle.png');
        this.load.image('cave', 'assets/sprites/cave.png');
    }

    create ()
    {
        this.add.image(400, 300, 'bg');

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

        const trees = [];

        for (let i = 0; i < 14; i++)
        {
            trees.push(this.add.image(0, 0, 'tree'));
        }

        //  The starting angle
        const startAngle = Phaser.Math.DegToRad(135);

        //  The end angle can overshoot 360 as required
        const endAngle = Phaser.Math.DegToRad(425);

        Phaser.Actions.PlaceOnCircle(trees, circle, startAngle, endAngle);

        //  Depth sort based on y value
        trees.forEach(tree => {
            tree.setDepth(tree.y);
        });

        this.add.image(400, 300, 'cave');
    }
}

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

const game = new Phaser.Game(config);

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

Прежде чем размещать объекты, нужно задать область размещения. В Phaser для этого удобно использовать геометрические объекты, такие как Phaser.Geom.Circle. Он определяет окружность по координатам центра и радиусу.

В методе create после добавления фона мы создаём круг, который будет служить нашей невидимой направляющей. Все спрайты будут размещены именно на его линии.

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

Здесь 400, 300 — это координаты центра круга (примерно центр экрана при разрешении 800x600), а 220 — его радиус в пикселях.

Параллельно мы создаём массив trees, куда будем складывать все спрайты для последующего группового размещения. Это эффективный подход для работы с множеством однотипных объектов.

Создание и размещение спрайтов по дуге

Создадим несколько спрайтов, добавив их в сцену с нулевыми координатами. Временно они будут наложены друг на друга в точке (0, 0), но это не страшно, так как мы их сразу переместим.

const trees = [];
for (let i = 0; i < 14; i++)
{
    trees.push(this.add.image(0, 0, 'tree'));
}

Ключевой момент — определение сектора окружности, по которому будут распределены объекты. Мы задаём начальный и конечный угол. Углы в Phaser часто используют радианы, поэтому для удобства переводим градусы в радианы с помощью Phaser.Math.DegToRad.

const startAngle = Phaser.Math.DegToRad(135);
const endAngle = Phaser.Math.DegToRad(425);

Важно: конечный угол может превышать 360 градусов (или 2π радиан). Это означает, что размещение начнётся с угла 135° и продолжится по часовой стрелке до угла 425°, что эквивалентно полному обороту (360°) плюс ещё 65°. Таким образом, объекты будут распределены по дуге в 290° (425 - 135).

Теперь применяем метод Phaser.Actions.PlaceOnCircle. Он принимает массив объектов, геометрический круг и опциональные начальный/конечный углы. Метод автоматически вычисляет и устанавливает позиции `xиy` для каждого объекта в массиве.

Phaser.Actions.PlaceOnCircle(trees, circle, startAngle, endAngle);

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

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

В 2D-графике Phaser объект с большим значением depth отображается поверх объектов с меньшим значением. Мы можем использовать координату `yкаждого спрайта для определения его глубины: чем большеy` (т.е., чем ниже спрайт на экране), тем больше значение глубины, и он должен быть "ближе".

Проходим по массиву и устанавливаем глубину для каждого спрайта, равную его текущей координате `y`.

trees.forEach(tree => {
    tree.setDepth(tree.y);
});

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

Конфигурация игры и финальные штрихи

Конфигурационный объект игры задаёт основные параметры, такие как тип рендерера, размеры холста и начальную сцену. Обратите внимание на параметр roundPixels: true. Он включает округление координат пикселей, что предотвращает размытие спрайтов при их отрисовке на нецелых координатах и делает изображение более чётким, что особенно важно для пиксель-арта или статичных сцен.

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#000000',
    parent: 'phaser-example',
    roundPixels: true,
    scene: Example
};
const game = new Phaser.Game(config);

Итоговая сцена представляет собой тёмный фон, по дуге вокруг центра расположены свечи-черепа, корректно отсортированные по глубине, а в центре — изображение пещеры.

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

Метод Phaser.Actions.PlaceOnCircle — это мощный и простой инструмент для автоматического распределения объектов по окружности или её части. Он избавляет от тригонометрических расчётов вручную и легко интегрируется в пайплайн создания сцены. Для экспериментов попробуйте: 1. Изменить радиус и положение круга, чтобы разместить объекты по эллиптической траектории (создав несколько кругов с разными центрами). 2. Анимировать углы startAngle и endAngle во времени, чтобы создать эффект "раскрывающегося" круга объектов. 3. Комбинировать PlaceOnCircle с другими методами из Phaser.Actions, например, RotateAround или Scale, чтобы добавить динамики сразу всей группе спрайтов после их размещения.