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

Визуальные эффекты, симуляции частиц или динамические интерфейсы — часто требуют отрисовки сотен и тысяч элементов с высокой частотой кадров. Использование отдельных игровых объектов (Sprites) для такой задачи неэффективно и быстро приведёт к падению производительности. В этой статье мы разберём мощную альтернативу: низкоуровневый объект `Graphics`. На практическом примере с анимированными кругами вы научитесь создавать высокопроизводительные визуализации, управляя отрисовкой напрямую через Canvas или WebGL контекст.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    balls = [];
    graphics;

    create ()
    {
        this.graphics = this.add.graphics();

        for (let i = 0; i < 2000; i++)
        {
            this.balls.push({
                x: Math.random() * window.innerWidth,
                y: Math.random() * window.innerHeight,
                v: 1,
                a: Math.random() * 2 * Math.PI
            });
        }
    }

    update ()
    {
        this.graphics.clear();
        this.graphics.fillStyle(0x9966ff, 1);

        for (const b in this.balls)
        {
            const ball = this.balls[b];
            ball.x += ball.v * Math.cos(ball.a);
            ball.y += ball.v * Math.sin(ball.a);
            ball.a += 0.03;

            this.graphics.fillCircle(ball.x, ball.y, ball.a);
        }
    }
}

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

const game = new Phaser.Game(config);

Почему Graphics, а не спрайты?

Класс Phaser.GameObjects.Graphics — это инструмент для программной (примитивной) отрисовки. В отличие от спрайтов, которые являются отдельными сущностями с собственной текстурой, трансформацией и логикой обновления, Graphics работает как единый холст. Вы вручную вызываете команды для рисования линий, фигур и заливок, и они все рендерятся за один проход. Это кардинально снижает нагрузку на CPU и GPU при работе с большим количеством простых объектов.

В нашем примере 2000 анимированных кругов. Создание 2000 спрайтов через this.add.image() привело бы к 2000 вызовам на обновление трансформаций, проверке видимости и отправке данных на рендеринг. Graphics же позволяет описать все 2000 кругов в одном месте и отправить их на отрисовку одной партией.

Структура примера: хранение данных и инициализация

В примере используется классическая для Phaser структура сцены. Данные частиц хранятся не как игровые объекты, а как простые объекты JavaScript в массиве balls. Это максимально облегчённый способ хранения состояния.

В методе create() создаётся единственный объект Graphics и заполняется массив данных. Для каждой "частицы" мы сохраняем её текущие координаты (`x,y), скорость (v) и угол направления (a`).

class Example extends Phaser.Scene
{
    balls = [];
    graphics;

    create ()
    {
        this.graphics = this.add.graphics();

        for (let i = 0; i < 2000; i++)
        {
            this.balls.push({
                x: Math.random() * window.innerWidth,
                y: Math.random() * window.innerHeight,
                v: 1,
                a: Math.random() * 2 * Math.PI
            });
        }
    }

Цикл обновления: расчёт и отрисовка

Сердце анимации — метод update(), который вызывается каждый кадр. Вся логика состоит из трёх ключевых шагов.

1.  **Очистка:** Перед рисованием нового кадра необходимо стереть предыдущий. Для этого используется метод `this.graphics.clear()`.
2.  **Стиль:** Устанавливаем цвет и прозрачность заливки для последующих фигур с помощью `this.graphics.fillStyle()`.
3.  **Цикл по данным:** Для каждого объекта в массиве `balls` обновляем его состояние (позицию и угол) и рисуем круг.
update ()
    {
        this.graphics.clear();
        this.graphics.fillStyle(0x9966ff, 1);

        for (const b in this.balls)
        {
            const ball = this.balls[b];
            ball.x += ball.v * Math.cos(ball.a);
            ball.y += ball.v * Math.sin(ball.a);
            ball.a += 0.03;

            this.graphics.fillCircle(ball.x, ball.y, ball.a);
        }
    }

**Расчёт движения:** Позиция изменяется на основе текущего угла (`a) и скорости (v). ФормулыMath.cos(ball.a)иMath.sin(ball.a)дают нам вектор направления. Угол постоянно увеличивается (ball.a += 0.03`), что заставляет частицы двигаться по плавным кривым.

**Рисование:** Метод this.graphics.fillCircle(x, y, radius) рисует закрашенный круг. Интересный нюанс примера — в качестве радиуса используется текущее значение угла ball.a, которое постоянно растёт. Это создаёт эффект увеличения размера частиц со временем.

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

Конфигурационный объект определяет основные параметры игры. Ключевой момент — type: Phaser.WEBGL. Хотя Graphics работает и в Canvas-режиме, для максимальной производительности при отрисовке тысяч элементов всегда следует использовать WebGL-рендерер. Он аппаратно ускоряет отрисовку графических примитивов.

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

const game = new Phaser.Game(config);

Оптимизации и расширение примера

Даже этот эффективный код можно улучшить и адаптировать под конкретные задачи.

* **Буферизация команд:** При очень большом количестве объектов (десятки тысяч) сам JavaScript-цикл for...in может стать узким местом. Рассмотрите возможность использования типизированных массивов (Float32Array) для хранения данных и обработки их в пакетном режиме. * **Усложнение логики:** В данных частицы можно добавить свойства life, scale, color. В цикле обновления менять стиль отрисовки fillStyle на основе этих свойств, создавая градиенты по времени жизни. * **Взаимодействие:** Добавьте проверку столкновений с курсором мыши. В update() можно получить позицию мыши через this.input.activePointer и сбросить или изменить траекторию ближайших частиц.

// Пример: реакция на мышь
const pointer = this.input.activePointer;
for (const ball of this.balls) {
    const dist = Phaser.Math.Distance.Between(pointer.x, pointer.y, ball.x, ball.y);
    if (dist < 100) {
        ball.a += 0.3; // Резко меняем направление
    }
}

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

Объект Graphics в Phaser — это ключ к созданию сложных и производительных визуальных систем, где количество элементов важнее их индивидуальной сложности. Вы освоили паттерн: отдельное хранилище данных + централизованная отрисовка. Для экспериментов попробуйте заменить круги на линии (graphics.lineBetween), добавить градиентную заливку в зависимости от скорости частицы или реализовать простой физический аттрактор, который закручивает частицы вокруг центра экрана.