О чем этот пример
Визуализация прогресса — ключевой элемент геймдизайна. Статические индикаторы скучны. В этой статье мы создадим динамическую шкалу, которая плавно меняет цвет по мере заполнения, используя конический градиент и маскирование в Phaser. Этот приём полезен для отображения здоровья, маны, уровня зарядки или любого другого прогресса, где важна не только величина, но и её критичность, выраженная через цвет.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
graphics;
create ()
{
// Asymmetrical conic gradient creates color that changes with angle.
const gradient = this.add.gradient({
shapeMode: 4, // CONIC_ASYMMETRIC
bands: { colorStart: 0x22ff22, colorEnd: 0xff2222, colorSpace: 1 },
start: { x: 0.5, y: 0.5 }, // Start at middle
shape: { x: 0, y: 0.5 }
}, 400, 350, 600, 600);
// Create an arc to act as the gauge.
const graphics = this.add.graphics();
graphics.lineStyle(64, 0xffffff, 1);
graphics.beginPath();
graphics.arc(400, 350, 260, Phaser.Math.DegToRad(30), Phaser.Math.DegToRad(150), true);
graphics.strokePath();
graphics.setVisible(false);
// Mask the gradient with the arc.
gradient.enableFilters().filters.external.addMask(graphics);
this.graphics = graphics;
}
update (time)
{
// Animate the gauge to rise and fall.
const graphics = this.graphics;
graphics.clear();
graphics.lineStyle(64, 0xffffff, 1);
graphics.beginPath();
graphics.arc(400, 350, 260, Phaser.Math.DegToRad(30 - 240 * (0.5 + 0.5 * Math.sin(time / 1200)) + 4 * (0.5 + 0.5 * Math.sin(time / 3.3))), Phaser.Math.DegToRad(150), true);
graphics.strokePath();
}
}
const config = {
type: Phaser.WEBGL,
width: 800,
height: 600,
backgroundColor: '#2d2d2d',
parent: 'phaser-example',
scene: Example
};
const game = new Phaser.Game(config);
Создаём асимметричный конический градиент
Основу нашего индикатора составит конический градиент, цвета в котором меняются в зависимости от угла. В Phaser для этого используется объект Gradient.
Ключевые параметры:
- shapeMode: 4 — устанавливает режим асимметричного конического градиента (CONIC_ASYMMETRIC).
- bands — определяет полосы градиента. Мы задаём начальный и конечный цвета, а также цветовое пространство (1 — RGB).
- start — точка, от которой начинается градиент. Мы помещаем её в центр (x: 0.5, y: 0.5).
- shape — определяет форму градиента. Параметр x: 0 задаёт начальный угол.
const gradient = this.add.gradient({
shapeMode: 4, // CONIC_ASYMMETRIC
bands: { colorStart: 0x22ff22, colorEnd: 0xff2222, colorSpace: 1 },
start: { x: 0.5, y: 0.5 },
shape: { x: 0, y: 0.5 }
}, 400, 350, 600, 600);
В результате мы получим круглую текстуру, где цвет плавно перетекает от зелёного (0x22ff22) в левой части к красному (0xff2222) в правой.
Рисуем дугу-маску с помощью Graphics
Чтобы градиент принял форму индикатора-дуги, нам нужна маска. В Phaser маской может выступать любой объект, способный к отрисовке, например, Graphics. Мы нарисуем белую дугу, которая и будет определять видимую область градиента.
Сначала создаём объект Graphics и настраиваем стиль линии: толщина 64 пикселя, белый цвет.
Затем с помощью команды arc рисуем дугу. Её параметры:
- Центр в точке (400, 350).
- Радиус 260 пикселей.
- Начальный угол 30 градусов, конечный — 150 градусов (дуга в верхней части круга).
- true означает, что дуга рисуется против часовой стрелки.
После отрисовки мы временно скрываем объект graphics, так как он нужен только как маска, а не для отображения.
const graphics = this.add.graphics();
graphics.lineStyle(64, 0xffffff, 1);
graphics.beginPath();
graphics.arc(400, 350, 260, Phaser.Math.DegToRad(30), Phaser.Math.DegToRad(150), true);
graphics.strokePath();
graphics.setVisible(false);
Применяем маску к градиенту
Теперь нужно связать градиент и маску. Phaser предоставляет для этого систему фильтров.
1. Сначала активируем фильтры для объекта градиента с помощью метода enableFilters().
2. Затем обращаемся к коллекции внешних фильтров filters.external и добавляем туда нашу маску методом addMask().
Это ключевой шаг: теперь градиент будет виден только в тех местах, где нарисована белая дуга.
gradient.enableFilters().filters.external.addMask(graphics);
Мы также сохраняем ссылку на объект graphics в свойстве сцены (this.graphics), чтобы иметь к нему доступ в методе обновления update.
Анимируем индикатор в реальном времени
Статический индикатор — скучно. Заставим нашу шкалу «дышать», плавно заполняясь и опустошаясь. Вся анимация происходит в методе update, который вызывается каждый кадр.
Логика проста: мы постоянно перерисовываем маску (graphics.clear() и новая команда arc), меняя её начальный угол. Конечный угол остаётся фиксированным (150 градусов).
Формула для расчёта начального угла выглядит сложно, но она создаёт плавное колебательное движение с двумя частотами:
- Math.sin(time / 1200) — медленное основное колебание (заполнение/опустошение шкалы).
- Math.sin(time / 3.3) — очень быстрое дрожание, добавляющее эффект «напряжения» или «помех».
Phaser.Math.DegToRad конвертирует градусы в радианы, как того требует API arc.
update (time)
{
const graphics = this.graphics;
graphics.clear();
graphics.lineStyle(64, 0xffffff, 1);
graphics.beginPath();
graphics.arc(400, 350, 260, Phaser.Math.DegToRad(30 - 240 * (0.5 + 0.5 * Math.sin(time / 1200)) + 4 * (0.5 + 0.5 * Math.sin(time / 3.3))), Phaser.Math.DegToRad(150), true);
graphics.strokePath();
}
Поскольку маска обновляется каждый кадр, а градиент использует её, мы видим анимированную шкалу, цвет которой меняется от зелёного к красному по мере «заполнения».
Что попробовать дальше
Мы создали динамичный визуальный индикатор, объединив мощь градиентов, масок и анимации в Phaser. Этот подход гораздо гибче, чем использование набора готовых спрайтов. Для экспериментов попробуйте: заменить конический градиент на линейный или радиальный; изменить форму маски на прямоугольник или сложный контур; привязать анимацию не ко времени, а к реальным игровым значениям, например, к здоровью персонажа; добавить свечение или другие пост-эффекты к самой маске.
