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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('spark0', 'assets/particles/blue.png');
        this.load.image('spark1', 'assets/particles/red.png');
        this.load.image('logo', 'assets/sprites/phaser2.png');
    }

    create ()
    {
        const p0 = new Phaser.Math.Vector2(200, 500);
        const p1 = new Phaser.Math.Vector2(200, 200);
        const p2 = new Phaser.Math.Vector2(600, 200);
        const p3 = new Phaser.Math.Vector2(600, 500);

        const curve = new Phaser.Curves.CubicBezier(p0, p1, p2, p3);

        const max = 28;
        const points = [];
        const tangents = [];

        for (let c = 0; c <= max; c++)
        {
            const t = curve.getUtoTmapping(c / max);

            points.push(curve.getPoint(t));
            tangents.push(curve.getTangent(t));
        }

        const tempVec = new Phaser.Math.Vector2();

        const spark0 = this.add.particles('spark0');
        const spark1 = this.add.particles('spark1');

        for (let i = 0; i < points.length; i++)
        {
            const p = points[i];

            tempVec.copy(tangents[i]).normalizeRightHand().scale(-32).add(p);

            const angle = Phaser.Math.RadToDeg(Phaser.Math.Angle.BetweenPoints(p, tempVec));

            const particles = (i % 2 === 0) ? spark0 : spark1;

            particles.createEmitter({
                x: tempVec.x,
                y: tempVec.y,
                angle: angle,
                speed: { min: -100, max: 500 },
                gravityY: 200,
                scale: { start: 0.4, end: 0.1 },
                lifespan: 800,
                blendMode: 'SCREEN'
            });
        }

        this.add.image(400, 400, 'logo');
    }
}

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

const game = new Phaser.Game(config);

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

Класс Example расширяет Phaser.Scene. В методе preload() мы загружаем три изображения: две текстуры для частиц разных цветов (spark0 и spark1) и логотип для фона. Базовый URL задается для удобства, чтобы указывать относительные пути к файлам примеров с GitHub.

preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.image('spark0', 'assets/particles/blue.png');
    this.load.image('spark1', 'assets/particles/red.png');
    this.load.image('logo', 'assets/sprites/phaser2.png');
}

Построение кривой и расчет точек

В методе create() сначала определяется форма нашего фейерверка — кубическая кривая Безье. Для ее создания нужны четыре опорные точки (вектора p0, p1, p2, p3), которые задают начальную, конечную и две контрольные точки.

const p0 = new Phaser.Math.Vector2(200, 500);
const p1 = new Phaser.Math.Vector2(200, 200);
const p2 = new Phaser.Math.Vector2(600, 200);
const p3 = new Phaser.Math.Vector2(600, 500);

const curve = new Phaser.Curves.CubicBezier(p0, p1, p2, p3);

Затем мы разбиваем кривую на сегменты. Количество сегментов (max) определяет плотность размещения эмиттеров. Для каждого сегмента вычисляется позиция точки на кривой (getPoint) и вектор касательной (getTangent). Касательная — это направление кривой в данной точке, которое позже будет использовано для задания угла разлета частиц.

const max = 28;
const points = [];
const tangents = [];

for (let c = 0; c <= max; c++)
{
    const t = curve.getUtoTmapping(c / max);

    points.push(curve.getPoint(t));
    tangents.push(curve.getTangent(t));
}

Создание систем частиц и настройка эмиттеров

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

const spark0 = this.add.particles('spark0');
const spark1 = this.add.particles('spark1');

Теперь ключевой шаг: для каждой рассчитанной точки на кривой создается отдельный эмиттер. Чтобы частицы вылетали не из самой точки на кривой, а с небольшым смещением наружу (создавая объем), используется вектор tempVec. Он копирует касательную, нормализует ее, поворачивает на 90 градусов вправо (normalizeRightHand), масштабирует и добавляет к исходной точке. Угол (angle) для эмиттера вычисляется между исходной точкой и этой смещенной точкой, что заставляет частицы вылетать перпендикулярно кривой.

const tempVec = new Phaser.Math.Vector2();

for (let i = 0; i < points.length; i++)
{
    const p = points[i];
    tempVec.copy(tangents[i]).normalizeRightHand().scale(-32).add(p);
    const angle = Phaser.Math.RadToDeg(Phaser.Math.Angle.BetweenPoints(p, tempVec));
    // ...
}

Конфигурация эмиттеров частиц

Цвет частиц чередуется: для четных точек используется система spark0 (синие), для нечетных — spark1 (красные). Каждый эмиттер настраивается с помощью конфигурационного объекта.

const particles = (i % 2 === 0) ? spark0 : spark1;

particles.createEmitter({
    x: tempVec.x,
    y: tempVec.y,
    angle: angle,
    speed: { min: -100, max: 500 },
    gravityY: 200,
    scale: { start: 0.4, end: 0.1 },
    lifespan: 800,
    blendMode: 'SCREEN'
});

Важные параметры: - speed: широкий разброс (min: -100, max: 500) создает хаотичный, но направленный взрыв. - gravityY: придает частицам вес, заставляя их падать вниз, как искры фейерверка. - scale: уменьшение размера от 0.4 до 0.1 создает эффект затухания. - lifespan: время жизни частицы в миллисекундах. - blendMode: 'SCREEN' делает яркие цвета частиц светящимися на темном фоне.

Запуск сцены и итоговый конфиг

В конце сцены добавляется фоновое изображение логотипа. Основная конфигурация игры (config) задает тип рендерера, размеры холста, цвет фона и корневой класс сцены.

this.add.image(400, 400, 'logo');
const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#000000',
    parent: 'phaser-example',
    scene: Example
};

const game = new Phaser.Game(config);

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

Мы разобрали, как создать анимацию фейерверка, жестко привязав множество эмиттеров частиц к точкам кривой Безье. Это дает полный контроль над формой и направлением эффекта. Для экспериментов попробуйте: изменить форму кривой, перемещая опорные точки; использовать другие типы кривых из Phaser (QuadraticBezier, Ellipse); анимировать опорные точки во времени, чтобы фейерверк "двигался"; или заменить текстуры частиц на спрайты с несколькими кадрами для более сложных эффектов.