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