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

Работа со временем — ключевой аспект геймдизайна. Задержки, повторяющиеся события, таймеры обратного отсчета — всё это требует понятной визуализации для отладки и создания игровых механик. Встроенный модуль Time в Phaser предоставляет мощный инструментарий для управления временными интервалами. В этой статье мы разберем практический пример создания анимированных часов, которые отображают прогресс таймера. Вы научитесь использовать `TimeEvent` и графический контекст `Graphics` для рисования динамических индикаторов, что пригодится при создании интерфейсов, способностей с перезарядкой или любых других элементов, зависящих от времени.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    clockSize = 240;
    timerEvent;
    graphics;

    create ()
    {
        this.timerEvent = this.time.addEvent({ delay: 4000, repeat: 9 });

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

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

        this.drawClock(400, 300, this.timerEvent);
    }

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

        //  The frame
        this.graphics.lineStyle(6, 0xffffff, 1);
        this.graphics.strokeCircle(x, y, this.clockSize);

        let angle;
        let dest;
        let p1;
        let p2;
        let size;

        //  The overall progress hand (only if repeat > 0)
        if (timer.repeat > 0)
        {
            size = this.clockSize * 0.9;

            angle = (360 * timer.getOverallProgress()) - 90;
            dest = Phaser.Math.RotateAroundDistance({ x: x, y: y }, x, y, Phaser.Math.DegToRad(angle), size);

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

            this.graphics.beginPath();

            this.graphics.moveTo(x, y);

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

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

            this.graphics.moveTo(x, y);

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

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

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

        //  The current iteration hand
        size = this.clockSize * 0.95;

        angle = (360 * timer.getProgress()) - 90;
        dest = Phaser.Math.RotateAroundDistance({ x: x, y: y }, x, y, Phaser.Math.DegToRad(angle), size);

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

        this.graphics.beginPath();

        this.graphics.moveTo(x, y);

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

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

        this.graphics.moveTo(x, y);

        p2 = Phaser.Math.RotateAroundDistance({ x: x, y: y }, x, y, Phaser.Math.DegToRad(angle + 5), size * 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);

Создание и настройка таймера

В Phaser за временные события отвечает модуль Time, доступный в каждой сцене через this.time. Основной инструмент — метод addEvent, который создает и запускает таймер с заданными параметрами.

В примере таймер настраивается на 10 повторений (первый запуск + 9 повторов) с интервалом в 4 секунды. Объект события сохраняется в свойстве сцены для доступа из других методов.

this.timerEvent = this.time.addEvent({ delay: 4000, repeat: 9 });

Также создается объект Graphics — холст для рисования примитивов. Его координаты устанавливаются в (0, 0).

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

Принцип работы метода update

Метод update вызывается на каждом кадре. Внутри него важно очищать графический холст от предыдущего кадра, иначе рисунки будут накладываться друг на друга.

this.graphics.clear();

После очистки вызывается метод drawClock, который рисует часы, передавая им текущие координаты и объект таймера. Координаты (400, 300) — это центр сцены при разрешении 800x600.

this.drawClock(400, 300, this.timerEvent);

Рисование индикаторов прогресса

Метод drawClock отвечает за всю визуализацию. Он рисует циферблат и две стрелки, используя методы объекта Graphics. Ключевую роль играют два метода таймера: - getOverallProgress() — возвращает общий прогресс всех повторений от 0 до 1. - getProgress() — возвращает прогресс текущего повторения от 0 до 1.

Эти значения преобразуются в углы для отрисовки стрелок.

Для рисования окружности (циферблата) используется strokeCircle.

this.graphics.lineStyle(6, 0xffffff, 1);
this.graphics.strokeCircle(x, y, this.clockSize);

Математика для стрелок: RotateAroundDistance

Отрисовка стрелки — это линия от центра к точке на окружности. Чтобы вычислить эту точку, используется функция Phaser.Math.RotateAroundDistance. Она поворачивает точку вокруг центра на заданный угол и расстояние.

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

angle = (360 * timer.getOverallProgress()) - 90;
dest = Phaser.Math.RotateAroundDistance({ x: x, y: y }, x, y, Phaser.Math.DegToRad(angle), size);

Функция Phaser.Math.DegToRad конвертирует градусы в радианы, так как тригонометрические функции в JavaScript работают с радианами.

Стрелка рисуется как два треугольника, исходящих из центра, чтобы придать ей форму стрелки. Точки p1 и p2 — это боковые вершины стрелки, смещенные на ±5 градусов от основного угла.

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

Два типа индикаторов: общий и текущий

В примере рисуются две стрелки разного цвета и длины, что наглядно демонстрирует разницу между методами таймера.

Красная стрелка (длина 90% от радиуса) показывает общий прогресс всех повторений таймера с помощью getOverallProgress(). Она рисуется только если timer.repeat > 0, то есть если таймер настроен на повторения.

if (timer.repeat > 0)
{
    // ... рисование красной стрелки с timer.getOverallProgress()
}

Желтая стрелка (длина 95% от радиуса) показывает прогресс текущего повторения с помощью getProgress(). Она обновляется плавно в течение каждой 4-секундной итерации.

Таким образом, красная стрелка делает полный оборот за все 10 повторений (40 секунд), а желтая — за каждые 4 секунды, возвращаясь на старт.

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

Вы научились визуализировать временные события в Phaser, используя Graphics и математические функции движка. Этот паттерн универсален: он подходит для отладки сложных таймингов, создания UI-индикаторов перезарядки, health-bars, которые восстанавливаются со временем, или любых циклических анимаций. Для экспериментов попробуйте: 1. Изменить параметры таймера: delay, repeat, callback. 2. Связать длину стрелки или цвет с другим параметром игры (например, здоровьем игрока). 3. Сделать часы интерактивными — останавливать или сбрасывать таймер по клику мыши.