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

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