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

Создание игровых интерфейсов, индикаторов загрузки или визуальных эффектов, зависящих от времени — частые задачи разработчика. В этом примере из официальных примеров Phaser показана мощная связка двух систем: `Time` для управления событиями и `Graphics` для их отрисовки. Вы научитесь создавать независимые таймеры и визуализировать их прогресс в виде кастомных циферблатов, что пригодится для создания игровых HUD, мини-игр или просто красивых анимаций.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    clockSize = 100;
    graphics;
    timerEvents = [];

    create ()
    {
        //  4 x 3
        for (let i = 0; i < 12; i++)
        {
            this.timerEvents.push(this.time.addEvent({ delay: 200 + (i * 400), loop: true }));
        }

        this.graphics = this.add.graphics({ x: 0, y: 0 });
    }

    update ()
    {
        this.graphics.clear();

        let x = this.clockSize;
        let y = this.clockSize;

        for (let i = 0; i < this.timerEvents.length; i++)
        {
            this.drawClock(x, y, this.timerEvents[i].getProgress());

            x += (this.clockSize * 2);

            if (x >= 800)
            {
                x = this.clockSize;
                y += (this.clockSize * 2);
            }
        }
    }

    drawClock (x, y, progress)
    {
        //  Progress is between 0 and 1, where 0 = the hand pointing up and then rotating clockwise a full 360

        const angle = (360 * progress) - 90;

        this.graphics.lineStyle(4, 0xffffff, 1);
        this.graphics.strokeCircle(x, y, this.clockSize * 0.95);

        this.graphics.lineStyle(2, 0xffff00, 1);

        const dest = Phaser.Math.RotateAroundDistance({ x: x, y: y }, x, y, Phaser.Math.DegToRad(angle), this.clockSize * 0.95);

        this.graphics.beginPath();

        this.graphics.moveTo(x, y);

        const p1 = Phaser.Math.RotateAroundDistance({ x: x, y: y }, x, y, Phaser.Math.DegToRad(angle - 5), this.clockSize * 0.7);

        this.graphics.lineTo(p1.x, p1.y);
        this.graphics.lineTo(dest.x, dest.y);

        this.graphics.moveTo(x, y);

        const p2 = Phaser.Math.RotateAroundDistance({ x: x, y: y }, x, y, Phaser.Math.DegToRad(angle + 5), this.clockSize * 0.7);

        this.graphics.lineTo(p2.x, p2.y);
        this.graphics.lineTo(dest.x, dest.y);

        this.graphics.strokePath();
        this.graphics.closePath();

    }

}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#2d2d2d',
    parent: 'phaser-example',
    scene: Example
};

const game = new Phaser.Game(config);

Подготовка: создание массива таймеров

В классе сцены объявлены ключевые свойства: размер циферблата, ссылка на объект Graphics и массив для хранения событий времени.

В методе create() инициализируется массив из 12 таймеров. Каждый таймер создаётся с помощью this.time.addEvent. Задержка (delay) между срабатываниями у каждого следующего таймера увеличивается на 400 мс, а первый запускается через 200 мс. Параметр loop: true делает событие повторяющимся.

this.timerEvents.push(this.time.addEvent({ delay: 200 + (i * 400), loop: true }));

Также здесь создаётся основной объект Graphics, который будет использоваться для рисования всех циферблатов в кадре.

Игровой цикл: обновление и позиционирование

Метод update() вызывается каждый кадр. Первым делом мы очищаем холст отрисованной графики с помощью clear(), чтобы перерисовать всё заново.

Затем в цикле проходим по всем созданным таймерам. Для каждого вычисляется позиция `xиyна экране в сетке 4x3. Ключевой параметр для отрисовки — прогресс таймера от 0 до 1, который получается методомgetProgress()`.

this.drawClock(x, y, this.timerEvents[i].getProgress());

После отрисовки каждого циферблата позиция `xувеличивается. Когда мы доходим до правого края экрана (800px), сбрасываемxи переходим на следующую строку, увеличиваяy`.

Рисование циферблата: от прогресса к графике

Сердце примера — метод drawClock(x, y, progress). Он принимает координаты центра и значение прогресса таймера.

Сначала прогресс (0..1) конвертируется в угол от 0 до 360 градусов. Вычитание 90 градусов нужно, чтобы начальное положение стрелки (при прогрессе 0) было направлено вверх, как у настоящих часов.

const angle = (360 * progress) - 90;

Затем рисуется белый контур циферблата с помощью strokeCircle. Далее цвет линии меняется на жёлтый для рисования стрелки.

Ключевой метод Phaser.Math.RotateAroundDistance вычисляет точку на окружности, в которую будет указывать конец стрелки. Он принимает объект с исходными координатами, точку вращения, угол в радианах и расстояние.

const dest = Phaser.Math.RotateAroundDistance({ x: x, y: y }, x, y, Phaser.Math.DegToRad(angle), this.clockSize * 0.95);

Стрелка рисуется как два треугольника, образующих стрелку. Для этого от центра до точки dest рисуются две линии через промежуточные точки p1 и p2, которые смещены от основного угла на ±5 градусов. Это создаёт эффект заострённой стрелки.

Конфигурация игры и запуск

Стандартная конфигурация игры Phaser. Важно, что в свойстве scene указан наш класс Example. Это делает его активной сценой.

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#2d2d2d',
    parent: 'phaser-example',
    scene: Example
};

Создание экземпляра игры с этой конфигурацией запускает весь описанный цикл.

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

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