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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{

    stars = [];
    starGraphics;

    create() {
        var colorsTable = [
            0xFF0000,
            0x00FF00,
            0x0000FF,
            0xFF00FF,
            0xFFFF00,
            0x00FFFF
        ];
        for (let i = 0; i < 500; ++i)
        {
            this.starGraphics = this.add.graphics({x: Math.random() * 800, y:Math.random() * 600});
            this.drawStar(this.starGraphics, 0, 0,  5, 100, 50, colorsTable[Math.floor(Math.random() * 6)], 0xFF0000);
            this.starGraphics.fillRect(100, 100, 100, 100);
            this.starGraphics.rotation = Math.random();
            this.starGraphics.scaleX = 0.1 + Math.abs(Math.sin(this.starGraphics.rotation)) * 0.2;
            this.starGraphics.scaleY = 0.1 + Math.abs(Math.sin(this.starGraphics.rotation)) * 0.2;
            this.stars.push(this.starGraphics);
        }
    }

    update() {
        for (let i = 0; i < this.stars.length; ++i)
        {
            const star = this.stars[i];
            star.rotation += 0.01;
            star.scaleX = 0.1 + Math.abs(Math.sin(this.starGraphics.rotation)) * 0.2;
            star.scaleY = 0.1 + Math.abs(Math.sin(this.starGraphics.rotation)) * 0.2;
        }
    }

    drawStar (
        graphics,
        cx, cy,
        spikes,
        outerRadius, innerRadius,
        color, lineColor) {
        let rot = Math.PI / 2 * 3;
        let x = cx;
        let y = cy;
        const step = Math.PI / spikes;
        graphics.lineStyle(10, lineColor, 1.0);
        graphics.fillStyle(color, 1.0);
        graphics.beginPath();
        graphics.moveTo(cx, cy - outerRadius);
        for (let i = 0; i < spikes; i++) {
            x = cx + Math.cos(rot) * outerRadius;
            y = cy + Math.sin(rot) * outerRadius;
            graphics.lineTo(x, y);
            rot += step;

            x = cx + Math.cos(rot) * innerRadius;
            y = cy + Math.sin(rot) * innerRadius;
            graphics.lineTo(x, y);
            rot += step;
        }
        graphics.lineTo(cx, cy - outerRadius);
        graphics.closePath();
        //graphics.strokePath();
        graphics.fillPath();
    }
}

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

const game = new Phaser.Game(config);

Основа: объект Graphics и его преимущества

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

В примере мы создаём не один сложный Graphics-объект со множеством точек, а 500 независимых инстансов. Каждый инстанс имеет собственные координаты (`x,y), угол поворота (rotation) и масштаб (scaleX,scaleY`). Это позволяет управлять ими по отдельности, например, заставлять вращаться с разной скоростью (хотя в примере скорость одинаковая).

Инициализация одного графического объекта выглядит так:

this.starGraphics = this.add.graphics({x: Math.random() * 800, y:Math.random() * 600});

Здесь `xиy` задаются случайным образом при создании, позиционируя звезду в произвольной точке экрана размером 800x600.

Рисование сложной формы: функция drawStar

Чтобы не дублировать код отрисовки для каждой звезды, мы выносим логику в отдельную функцию drawStar. Эта функция принимает целевой объект graphics и параметры фигуры: координаты центра (cx, cy), количество лучей, внешний и внутренний радиусы, цвет заливки и цвет обводки.

Ключевой момент: функция рисует форму **относительно локальных координат (0,0) переданного объекта Graphics**. В нашем случае мы всегда передаём cx = 0 и cy = 0. Это значит, что "центр" звезды привязан к локальному началу координат её графического объекта. Позже, устанавливая `xиyсамому объектуthis.starGraphics`, мы перемещаем всю нарисованную фигуру на сцене.

Посмотрите на основные методы рисования:

graphics.lineStyle(10, lineColor, 1.0); // Толщина, цвет, альфа для линий
graphics.fillStyle(color, 1.0);         // Цвет и альфа для заливки
graphics.beginPath();                    // Начинаем новый путь
graphics.moveTo(cx, cy - outerRadius);   // Перемещаем "перо" в стартовую точку
// ... вычисление и отрисовка лучей через graphics.lineTo(x, y)
graphics.closePath();                    // Замыкаем путь
graphics.fillPath();                     // Заливаем путь
// graphics.strokePath();                // Обводим путь (закомментировано в примере)

Интересно, что после отрисовки звезды в примере добавляется прямоугольник: this.starGraphics.fillRect(100, 100, 100, 100);. Эта строка, вероятно, добавлена для демонстрации и может быть удалена — она рисует неподвижный прямоугольник в локальных координатах каждой звезды, что создаёт неожиданный визуальный шум.

Управление множеством инстансов: массивы и циклы

Сердце примера — управление жизненным циклом 500 созданных объектов. Для этого используется массив this.stars = [], в который при создании в методе create() помещается каждый новый объект Graphics.

В методе create(): 1. Определяется палитра из шести цветов (colorsTable). 2. В цикле 500 раз создаётся объект Graphics со случайной позицией. 3. Для этого объекта вызывается drawStar для отрисовки формы. 4. Задаются его индивидуальные свойства: случайный начальный угол rotation и зависимый от этого угла масштаб (scaleX, scaleY). 5. Объект добавляется в массив this.stars для последующего обновления.

Код инициализации в цикле:

for (let i = 0; i < 500; ++i) {
    this.starGraphics = this.add.graphics({x: Math.random() * 800, y:Math.random() * 600});
    this.drawStar(this.starGraphics, 0, 0,  5, 100, 50, colorsTable[Math.floor(Math.random() * 6)], 0xFF0000);
    this.starGraphics.rotation = Math.random(); // Случайный начальный угол
    this.starGraphics.scaleX = 0.1 + Math.abs(Math.sin(this.starGraphics.rotation)) * 0.2;
    this.starGraphics.scaleY = 0.1 + Math.abs(Math.sin(this.starGraphics.rotation)) * 0.2;
    this.stars.push(this.starGraphics);
}

В методе update() мы проходим по этому массиву и для каждой звезды обновляем её состояние, создавая анимацию:

for (let i = 0; i < this.stars.length; ++i) {
    const star = this.stars[i];
    star.rotation += 0.01; // Постоянное вращение
    star.scaleX = 0.1 + Math.abs(Math.sin(this.starGraphics.rotation)) * 0.2;
    star.scaleY = 0.1 + Math.abs(Math.sin(this.starGraphics.rotation)) * 0.2;
}

**Важное замечание по коду:** В оригинальном примере внутри update() есть потенциальная ошибка/опечатка. Для расчёта масштаба используется this.starGraphics.rotation (последняя созданная звезда), а не star.rotation (текущая звезда в цикле). Для корректной работы, где масштаб каждой звезды зависит от её собственного вращения, строки должны выглядеть так:

star.scaleX = 0.1 + Math.abs(Math.sin(star.rotation)) * 0.2;
star.scaleY = 0.1 + Math.abs(Math.sin(star.rotation)) * 0.2;

Оптимизация и расширение возможностей

Представленный подход — отличный баланс между гибкостью и производительностью для десятков или сотен объектов. Однако для тысяч частиц лучше рассмотреть системы частиц Phaser (ParticleEmitter).

Что можно улучшить или изменить в этом примере:
1.  **Разное вращение:** Присвойте каждой звезде в массиве индивидуальную скорость вращения, храня её в пользовательском свойстве (например, `star.rotationSpeed = 0.005 + Math.random() * 0.02;`), и используйте в `update()`: `star.rotation += star.rotationSpeed;`.
2.  **Динамический цвет:** Цвет можно менять со временем, используя свойство `star.fillColor` или перерисовывая форму с новыми параметрами (что дороже).
3.  **Взаимодействие:** Добавьте физику (`this.physics.add.existing(star)`) или проверку клика (`star.setInteractive()`), чтобы сделать звёзды интерактивными.
4.  **Группировка:** Для статического фона можно сгруппировать все объекты `Graphics` в один, чтобы уменьшить количество draw calls, но тогда потеряется индивидуальная анимация.

Пример добавления индивидуальной скорости:

// В create(), после создания starGraphics:
starGraphics.rotationSpeed = 0.005 + Math.random() * 0.01;

// В update():
star.rotation += star.rotationSpeed;

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

Использование множества инстансов Graphics открывает широкие возможности для создания процедурной графики и динамических фонов в Phaser. Вы можете генерировать уникальные миры из простых форм, управляя их жизненным циклом через массивы. Для экспериментов попробуйте: изменить форму с звезды на многоугольник или спираль; заставить звёзды плавно менять цвет по времени; добавить им движение по траектории или реакцию на курсор мыши; или создать эффект "параллакса", двигая звёзды на разных слоях с разной скоростью. Этот паттерн — ваш строительный блок для визуальной магии.