О чем этот пример
Визуальные эффекты, состоящие из тысяч элементов, могут серьёзно нагружать производительность. Этот пример демонстрирует, как эффективно использовать объекты `Graphics` в Phaser для отрисовки и анимации большого количества звёзд без потери FPS. Вы научитесь создавать геометрические фигуры программно, управлять их трансформациями и использовать ключевую настройку `batchSize` для группового рендеринга, что критически важно для particle-эффектов, фонов и UI-элементов в играх.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
cameraRotation = 0;
stars = [];
create ()
{
const radius = 10;
const radius2 = radius * 2;
const maxWidth = (800 / radius2)|0;
for (let i = 0; i < 1200; ++i)
{
const graphics = this.add.graphics({x: radius + (i % maxWidth) * radius2, y: radius + ((i / maxWidth)|0) * radius2});
this.drawStar(graphics, 0, 0, 5, radius, radius / 2, 0xff0000, 0xFFFF00);
this.stars.push(graphics);
this.stars[i].rotation += i * 0.01;
}
}
update ()
{
for (let i = 0; i < this.stars.length; ++i)
{
const star = this.stars[i];
star.rotation += 0.01;
star.scaleX = 0.5 + Math.abs(Math.sin(star.rotation));
star.scaleY = 0.5 + Math.abs(Math.sin(star.rotation));
}
}
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(1, 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.fillPath();
graphics.strokePath();
}
}
const config = {
type: Phaser.WEBGL,
parent: 'phaser-example',
scene: Example,
batchSize: 8000,
width: 800,
height: 600
};
const game = new Phaser.Game(config);
Зачем нужны Graphics?
В отличие от готовых спрайтов, объекты Graphics позволяют рисовать примитивы (линии, фигуры) непосредственно на канвасе или в WebGL-контексте. Это идеально для динамически генерируемой графики, которую нецелесообразно хранить в виде текстур. В примере каждая звезда — это отдельный объект Graphics, что даёт полный контроль над её формой, цветом и анимацией.
Ключевой метод this.add.graphics() создаёт и добавляет объект на сцену. Обратите внимание на параметр batchSize в конфигурации игры — он увеличивает лимит объектов в одном отрисовочном батче, что значительно ускоряет рендеринг множества однотипных элементов.
Расстановка звёзд на сетке
Код создаёт 1200 звёзд, равномерно распределённых по экрану. Чтобы не выйти за границы, используется простая математика сетки.
const radius = 10;
const radius2 = radius * 2;
const maxWidth = (800 / radius2)|0;
for (let i = 0; i < 1200; ++i)
{
const graphics = this.add.graphics({x: radius + (i % maxWidth) * radius2, y: radius + ((i / maxWidth)|0) * radius2});
this.drawStar(graphics, 0, 0, 5, radius, radius / 2, 0xff0000, 0xFFFF00);
this.stars.push(graphics);
this.stars[i].rotation += i * 0.01;
}
Переменная maxWidth вычисляет, сколько звёзд поместится в ряд (ширина экрана 800 пикселей). Позиция каждой звезды (`x,y) рассчитывается на основе индексаiс использованием остатка от деления (%) и целочисленного деления (|0`). Это создаёт аккуратную сетку. Каждой звезде сразу задаётся начальный угол поворота, зависящий от индекса, чтобы избежать однообразия в стартовой анимации.
Алгоритм отрисовки звезды
Функция drawStar — это сердце примера. Она использует API Graphics для рисования контура и заливки пятиконечной звезды.
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(1, 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.fillPath();
graphics.strokePath();
}
Методы lineStyle() и fillStyle() задают стиль границы и заливки. Алгоритм последовательно вычисляет вершины звезды, чередуя внешний (outerRadius) и внутренний (innerRadius) радиус, и соединяет их линиями с помощью lineTo(). Цикл выполняется spikes раз (5 раз для пятиконечной звезды). В конце fillPath() и strokePath() применяют заливку и обводку к построенному контуру.
Цикл анимации в update()
В каждом кадре метод update() обходит массив stars и обновляет свойства каждой звезды, создавая живую, пульсирующую анимацию.
update ()
{
for (let i = 0; i < this.stars.length; ++i)
{
const star = this.stars[i];
star.rotation += 0.01;
star.scaleX = 0.5 + Math.abs(Math.sin(star.rotation));
star.scaleY = 0.5 + Math.abs(Math.sin(star.rotation));
}
}
Здесь происходит две вещи:
1. Постоянное увеличение угла поворота (rotation).
2. Динамическое изменение масштаба (scaleX, scaleY) на основе синуса текущего угла поворота. Функция Math.abs() гарантирует, что масштаб всегда положительный, создавая эффект «пульсации» от 0.5 до 1.5. Поскольку расчёт масштаба привязан к собственному вращению звезды, каждая из них пульсирует в своём уникальном ритме, что добавляет визуальной сложности.
Критическая конфигурация: batchSize
Без правильной настройки рендерера производительность упадёт. Ключевой параметр находится в объекте конфигурации игры.
const config = {
type: Phaser.WEBGL,
parent: 'phaser-example',
scene: Example,
batchSize: 8000,
width: 800,
height: 600
};
По умолчанию batchSize в Phaser равен 2000. Это значит, что за один проход (draw call) может быть отрисовано не более 2000 объектов типа Graphics или Sprite. В нашем примере 1200 звёзд, и при дефолтном значении они всё равно поместятся в один батч. Однако, если вы планируете рисовать больше объектов (например, 3000 частиц), необходимо явно увеличить batchSize, как показано в коде (до 8000). Использование Phaser.WEBGL также обязательно для максимальной производительности при таком количестве элементов.
Что попробовать дальше
Использование Graphics для программной генерации множества анимированных объектов — мощный и производительный подход в Phaser. Он открывает двери для создания сложных динамических фонов, эффектов частиц и нестандартных UI-элементов прямо в коде, без привлечения графических редакторов.
**Идеи для экспериментов:**
1. Измените параметры в drawStar: количество лучей (spikes), соотношение радиусов, цвета.
2. Добавьте взаимодействие: заставьте звезды реагировать на курсор мыши, изменяя цвет или масштаб при наведении.
3. Замените равномерную сетку на случайное распределение (Phaser.Math.Between) для создания звёздного неба.
4. Поэкспериментируйте с формулой пульсации в update(), например, используя Math.cos или комбинируя несколько тригонометрических функций для более сложного движения.
