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

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

Версия 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/pics/neoncircle.png');
        this.load.image('particle', 'assets/sprites/particle1.png');
    }

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

        //  Create our sprites to place within the circle
        const particles = [];

        for (let i = 0; i < 512; i++)
        {
            particles.push(this.add.image(0, 0, 'particle'));
        }

        //  The Circle geometry object
        const circle = new Phaser.Geom.Circle(400, 300, 210);

        //  Randomly position the sprites within the circle
        Phaser.Actions.RandomCircle(particles, circle);
    }
}

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

const game = new Phaser.Game(config);

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

Как и в любом проекте на Phaser, начинаем с создания класса сцены. В методе preload мы загружаем два изображения: фоновую картинку (bg) и спрайт частицы (particle), который будет использоваться для заполнения круга. Обратите внимание, что базовый URL задаётся через this.load.setBaseURL, что удобно для загрузки ассетов из удалённого репозитория.

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.image('bg', 'assets/pics/neoncircle.png');
    this.load.image('particle', 'assets/sprites/particle1.png');
}

В методе create мы сначала добавляем фоновое изображение по центру экрана, чтобы визуально обозначить рабочую область. Затем создаём массив particles и в цикле наполняем его 512 спрайтами частиц. На этом этапе все частицы создаются с координатами (0, 0), то есть в левом верхнем углу экрана, и накладываются друг на друга.

Создание геометрической области

Чтобы распределить объекты, нам нужна целевая область. В Phaser для работы с геометрией есть отдельный модуль Phaser.Geom. Мы создадим объект Circle, который определяет круглую область. Конструктор принимает три параметра: координаты центра по осям X и Y, а также радиус.

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

В нашем примере круг создаётся с центром в точке (400, 300), что совпадает с центром фонового изображения и экрана (при разрешении 800x600). Радиус в 210 пикселей выбран так, чтобы частицы оставались в пределах видимой окружности на фоне. Этот объект служит математическим описанием области и не отрисовывается на экране.

Массовое распределение объектов

Самая интересная часть — использование метода Phaser.Actions.RandomCircle. Модуль Actions содержит набор функций для выполнения массовых операций над массивами игровых объектов. Метод RandomCircle принимает два обязательных аргумента: массив объектов (в нашем случае — спрайтов частиц) и геометрический объект Circle.

Phaser.Actions.RandomCircle(particles, circle);

Эта функция перебирает все элементы массива particles и для каждого случайным образом вычисляет новую позицию внутри границ переданного круга. Важно, что распределение является равномерным, то есть вероятность попадания частицы в любую точку круга одинакова. Координаты `xиy` каждого спрайта автоматически обновляются. В результате все 512 частиц, которые изначально были в одной точке, мгновенно "разлетаются" и заполняют круглую область, создавая эффект роя или скопления.

Конфигурация и запуск игры

Как и всегда, для запуска игры необходимо создать конфигурационный объект и инстанс Phaser.Game. В конфиге мы указываем базовые настройки: тип рендерера (Phaser.AUTO), размеры холста, цвет фона и родительский 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 автоматически запускает жизненный цикл сцены: вызовет preload, затем create, и начнёт игровой цикл update. В нашем примере логика в update отсутствует, поэтому сцена статична после создания.

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

Метод Phaser.Actions.RandomCircle — это отличный пример того, как Phaser упрощает рутинные задачи. Одной строкой кода мы решили проблему случайного, но равномерного размещения сотен объектов. Этот приём можно использовать не только для визуальных эффектов, но и для геймдизайна: например, для размещения лута, врагов или точек интереса на локации. Попробуйте поэкспериментировать: замените круг на другой геометрический объект, например, Rectangle или Triangle, используя соответствующие методы из модуля Actions (RandomRectangle, RandomTriangle). Добавьте движение частицам через update, чтобы создать анимированный рой, или изменяйте радиус круга со временем для эффекта "пульсации".