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

Эмиттеры частиц в Phaser 3 по умолчанию распределяют частицы равномерно по заданной геометрической зоне. Но что, если вам нужно, чтобы частицы чаще появлялись в центре области или, наоборот, по её краям? Встроенные зоны так не умеют. В этой статье мы разберём, как создать собственную взвешенную зону эмиссии, реализовав специальный метод, что даст вам полный контроль над распределением частиц в пространстве и позволит создавать более сложные и интересные визуальные эффекты.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    circle = new Phaser.Geom.Circle(0, 0, 180);

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

    create ()
    {
        const particles = this.add.particles('flares');

        const weightedCircle = {
            getRandomPoint: (vec) =>
            {
                const t = Phaser.Math.PI2 * Math.random();
                const r = Math.pow(Math.random(), -0.1);

                vec.x = this.circle.x + r * Math.cos(t) * this.circle.radius;
                vec.y = this.circle.y + r * Math.sin(t) * this.circle.radius;

                return vec;
            }
        };

        particles.createEmitter({
            frame: 'red',
            x: 400, y: 300,
            lifespan: 2000,
            quantity: 4,
            scale: 0.2,
            alpha: { start: 1, end: 0 },
            blendMode: 'ADD',
            emitZone: { type: 'random', source: weightedCircle }

            // Compare:
            // emitZone: { type: 'random', source: circle }
        });
    }
}

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

const game = new Phaser.Game(config);

Проблема равномерного распределения

В Phaser 3 для эмиттера частиц можно задать emitZone — область, из которой будут появляться новые частицы. Если в качестве источника (source) передать объект геометрии, например Phaser.Geom.Circle, частицы будут появляться в случайных точках, равномерно распределённых по всей площади круга.

emitZone: { type: 'random', source: circle }

Это стандартное поведение. Однако для многих эффектов (например, имитации взрыва, магической ауры или скопления света) такое распределение выглядит слишком однородным и неестественным. Частицам часто нужно группироваться вокруг центра или, наоборот, концентрироваться на границах зоны.

Решение: пользовательская зона со взвешенной вероятностью

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

В примере создаётся объект weightedCircle. Его метод getRandomPoint использует полярную систему координат для вычисления позиции. Угол (`t) выбирается случайно, а вот расстояние от центра (r`) вычисляется по специальной формуле, которая смещает распределение.

const weightedCircle = {
    getRandomPoint: (vec) =>
    {
        const t = Phaser.Math.PI2 * Math.random();
        const r = Math.pow(Math.random(), -0.1);

        vec.x = this.circle.x + r * Math.cos(t) * this.circle.radius;
        vec.y = this.circle.y + r * Math.sin(t) * this.circle.radius;

        return vec;
    }
};

Обратите внимание: метод получает и модифицирует готовый вектор vec, который затем возвращает. Это стандартный паттерн Phaser для оптимизации и избегания создания новых объектов в каждом кадре.

Магия формулы распределения

Давайте разберём строку, которая отвечает за взвешенность:

const r = Math.pow(Math.random(), -0.1);

Функция Math.random() возвращает значение от 0 до 1. Возведение этого значения в отрицательную степень, меньшую единицы (-0.1), даёт интересный эффект. Результат `rбудет чаще принимать значения, близкие к 0, чем к 1. Поскольку позжеrумножается на радиус круга, это приводит к тому, что большая часть частиц будет генерироваться ближе к центру зоны (r * radius` будет маленьким).

Меняя показатель степени, можно управлять распределением: - Math.pow(Math.random(), -2.0) — очень сильное смещение к центру. - Math.pow(Math.random(), -0.01) — очень слабое смещение, почти равномерное распределение. - Math.pow(Math.random(), 2.0) — смещение к краям (частицы будут чаще появляться на границе круга).

Это и есть ядро механизма взвешенной случайности.

Подключение кастомной зоны к эмиттеру

Созданный объект weightedCircle подключается к конфигурации эмиттера так же просто, как и стандартная геометрия. Нужно лишь указать его в качестве source для зоны типа 'random'.

particles.createEmitter({
    frame: 'red',
    x: 400, y: 300,
    // ... другие параметры эмиттера ...
    emitZone: { type: 'random', source: weightedCircle }
});

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

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

Реализация метода getRandomPoint — мощный и элегантный способ получить полный контроль над распределением частиц в Phaser 3. Вы не ограничены кругом или формулой из примера. Вы можете создавать зоны любой формы (звезды, кольца, линии) и с любым законом распределения. Для экспериментов попробуйте

  1. Использовать шум Перлина для генерации `rиt`, чтобы создать органические, плавные скопления
  2. Сделать зону в форме отрезка, где частицы будут чаще появляться у одного из его концов
  3. Динамически менять показатель степени в формуле в зависимости от времени или состояния игры, чтобы зона 'дышала'