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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    palette;
    graphics;
    inc = 0;
    time = 0;

    create ()
    {
        this.graphics = this.add.graphics({ x: 400, y: 300 });

        this.palette = [ 0, 1911635, 8267091, 34641, 11227702, 6248271, 12764103, 16773608, 16711757, 16753408, 16772135, 58422, 2731519, 8615580, 16742312, 16764074 ];
    }

    update ()
    {
        this.time += 0.03;

        this.time = Phaser.Math.Wrap(this.time, -32765, 32765);

        this.graphics.clear();

        const f = this.time / 9;
        const n = 650 + 60 * this.sin(f / 3);

        for (let i = 1; i < n; i++)
        {
            let a = f + Math.random();
            let d = 0.3 + Math.random() * 2;
            let y = -2;

            if (i > 400)
            {
                const j = i - 400;
                y = j * 2 / n - 1;
                a = j * 40 / n + f + j / 3;
                d = j * 3 / n;
            }

            let x = d * this.cos(a);
            const z = 2 + this.cos(f) + d * this.sin(a);

            x = 64 + x * 64 / z;
            y = 64 + y * 64 / z;

            const c = 6 + i % 5;
            const e = 5 / z;

            if (z > 0.1)
            {
                this.graphics.fillStyle(this.palette[c]);

                if (i > 400)
                {
                    this.graphics.fillCircle(x, y, e);
                }
                else
                {
                    this.graphics.fillRect(x, y, e, e);
                }

                // graphics.fillStyle(palette[c / 4]);
                // graphics.fillCircle(x, 128 - y, e);
            }

        }

    }

    cos (f)
    {
        return Math.cos(f * (Math.PI * 2));
    }

    sin (f)
    {
        return Math.sin(f * (Math.PI * 2));
    }
}

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

const game = new Phaser.Game(config);

Подготовка сцены и палитры

В методе create() инициализируются ключевые объекты. Graphics — это холст для рисования векторных фигур. Мы позиционируем его в центре экрана.

Палитра palette — это массив чисел, представляющих цвета в формате RGB (например, 16711757 — это красный). Эти цвета будут использоваться для отрисовки частиц.

this.graphics = this.add.graphics({ x: 400, y: 300 });
this.palette = [ 0, 1911635, 8267091, 34641, 11227702, 6248271, 12764103, 16773608, 16711757, 16753408, 16772135, 58422, 2731519, 8615580, 16742312, 16764074 ];

Управление временем и очистка кадра

В update() управляется анимация. Переменная time увеличивается каждому кадру, создавая основу для движения. Чтобы избежать переполнения чисел (очень больших или очень малых значений), используется Phaser.Math.Wrap, который заворачивает значение time в заданный диапазон.

Перед каждой отрисовкой нового кадра старые фигуры нужно стереть с помощью clear().

this.time += 0.03;
this.time = Phaser.Math.Wrap(this.time, -32765, 32765);
this.graphics.clear();

Магия псевдо-3D: расчет позиции и глубины

Здесь создается иллюзия трехмерности. Параметр `n` определяет общее количество частиц, и оно слегка пульсирует благодаря синусу.

В цикле для каждой частицы рассчитываются углы (`a), расстояние (d`) и координата Y. Для частиц с индексом больше 400 (внешнее кольцо) расчеты сложнее — они формируют спиральную структуру.

Ключевой трюк — расчет координат `xиy. Деление наz` (расчетную "глубину") имитирует перспективу: чем дальше объект, тем он меньше и ближе к центру экрана.

const f = this.time / 9;
const n = 650 + 60 * this.sin(f / 3);

let a = f + Math.random();
let d = 0.3 + Math.random() * 2;
let y = -2;

// ... логика для i > 400 ...

let x = d * this.cos(a);
const z = 2 + this.cos(f) + d * this.sin(a);

x = 64 + x * 64 / z;
y = 64 + y * 64 / z;

Отрисовка фигур с учетом глубины

После расчета позиции и глубины частицы можно рисовать. Размер частицы (`e) обратно пропорционален глубинеz` — еще один штрих к 3D-иллюзии.

Цвет выбирается из палитры по индексу. Внутренние частицы (индекс <= 400) рисуются как квадраты (fillRect), а внешние — как круги (fillCircle), что добавляет визуального разнообразия.

Условие if (z > 0.1) отсекает отрисовку частиц, которые теоретически находятся "позади" камеры.

const c = 6 + i % 5;
const e = 5 / z;

if (z > 0.1)
{
    this.graphics.fillStyle(this.palette[c]);

    if (i > 400)
    {
        this.graphics.fillCircle(x, y, e);
    }
    else
    {
        this.graphics.fillRect(x, y, e, e);
    }
}

Вспомогательные функции для цикличных волн

Стандартные Math.sin и Math.cos работают с радианами. Наши функции sin и cos принимают аргумент `f, который является долей полного круга (где 1 = 360 градусов). Это делает код вupdate()` более читаемым, так как мы оперируем не радианами, а «оборотами».

cos (f)
{
    return Math.cos(f * (Math.PI * 2));
}

sin (f)
{
    return Math.sin(f * (Math.PI * 2));
}

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

Этот пример — отличная отправная точка для создания собственных procedural visual effects. Экспериментируйте: измените палитру на более холодную или огненную, замените квадраты на треугольники (используя fillPath), управляйте количеством частиц в реальном времени в ответ на действия игрока или добавьте вторую симметричную спираль, раскомментировав закомментированные строки в коде. Главный вывод: мощная визуализация в Phaser часто строится не на сложных ассетах, а на грамотном применении математики к простым графическим примитивам.