О чем этот пример
Работа с графикой в 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. Вы можете генерировать уникальные миры из простых форм, управляя их жизненным циклом через массивы. Для экспериментов попробуйте: изменить форму с звезды на многоугольник или спираль; заставить звёзды плавно менять цвет по времени; добавить им движение по траектории или реакцию на курсор мыши; или создать эффект "параллакса", двигая звёзды на разных слоях с разной скоростью. Этот паттерн — ваш строительный блок для визуальной магии.
