О чем этот пример
Движущийся текст по сложной траектории — эффектный приём для титров, интерфейсов или внутриигровых сообщений. В этом примере показано, как заставить каждую букву динамического BitmapText следовать по заранее заданному пути, создавая плавную волнообразную анимацию. Мы разберём ключевые концепции Phaser: создание кривых Path, использование `setDisplayCallback` для управления отображением символов и синхронизацию анимации в игровом цикле.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
let t = 0;
let path;
class Example extends Phaser.Scene
{
constructor ()
{
super();
}
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.bitmapFont('desyrel', 'assets/fonts/bitmap/desyrel.png', 'assets/fonts/bitmap/desyrel.xml');
}
create ()
{
path = new Phaser.Curves.Path(1500, 500);
path.lineTo(700, 500);
path.splineTo([ 745, 256, 550, 145, 300, 250, 260, 450, 50, 500 ]);
path.lineTo(-100, 500);
const text = this.add.dynamicBitmapText(0, 0, 'desyrel', 'Phaser 3', 64);
text.setDisplayCallback(this.positionOnPath);
const graphics = this.add.graphics();
graphics.lineStyle(1, 0xffffff, 1);
path.draw(graphics, 128);
}
update()
{
t += 0.001;
if (t >= (1 - 0.24))
{
t = 0;
}
}
// data = { color: color, index: index, charCode: charCode, x: x, y: y, scaleX: scaleX, scaleY: scaleY }
positionOnPath (data)
{
var pathVector = path.getPoint(t + ((6 - data.index) * 0.04));
if (pathVector)
{
data.x = pathVector.x;
data.y = pathVector.y;
}
return data;
}
}
const config = {
type: Phaser.AUTO,
parent: 'phaser-example',
width: 800,
height: 600,
scene: Example
};
const game = new Phaser.Game(config);
Подготовка сцены и загрузка ресурсов
Класс Example расширяет Phaser.Scene. В методе preload загружается bitmap-шрифт. Установка базового URL упрощает загрузку ассетов из удалённого репозитория примеров.
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.bitmapFont('desyrel', 'assets/fonts/bitmap/desyrel.png', 'assets/fonts/bitmap/desyrel.xml');
}
Создание пути и текста
В методе create инициализируется главная логика. Сначала создаётся объект Phaser.Curves.Path. Он определяет траекторию движения. Путь строится от начальной точки методом lineTo, затем добавляется сложная кривая splineTo и завершается ещё одним отрезком lineTo.
create ()
{
path = new Phaser.Curves.Path(1500, 500);
path.lineTo(700, 500);
path.splineTo([ 745, 256, 550, 145, 300, 250, 260, 450, 50, 500 ]);
path.lineTo(-100, 500);
}
Затем создаётся динамический bitmap-текст. Ключевой метод setDisplayCallback назначает функцию обратного вызова positionOnPath, которая будет вызываться для каждого символа при его отрисовке, позволяя модифицировать его свойства.
const text = this.add.dynamicBitmapText(0, 0, 'desyrel', 'Phaser 3', 64);
text.setDisplayCallback(this.positionOnPath);
Для визуализации пути (что полезно при отладке) создаётся объект Graphics и рисуется траектория.
const graphics = this.add.graphics();
graphics.lineStyle(1, 0xffffff, 1);
path.draw(graphics, 128);
Функция обратного вызова для позиционирования
Функция positionOnPath — сердце анимации. Она вызывается для каждого символа текста. Параметр data содержит свойства символа: индекс, координаты и другие. Наша задача — изменить data.x и data.y.
Мы вычисляем позицию на пути с помощью path.getPoint(t). Параметр `t— это нормализованное расстояние вдоль кривой (от 0 до 1). Чтобы буквы не накладывались друг на друга, кtдобавляется смещение, зависящее от индекса символа ((6 - data.index) * 0.04`). Это создаёт эффект «волны», где каждая следующая буква немного отстаёт.
positionOnPath (data)
{
var pathVector = path.getPoint(t + ((6 - data.index) * 0.04));
if (pathVector)
{
data.x = pathVector.x;
data.y = pathVector.y;
}
return data;
}
Игровой цикл и управление временем
В методе update увеличивается глобальная переменная `t, которая управляет движением вдоль пути. Увеличение на небольшую фиксированную величину (0.001) обеспечивает плавную анимацию. Условиеif (t >= (1 - 0.24))сбрасываетtв ноль, когда анимация почти завершилась, создавая бесконечный цикл. Значение0.24` подобрано эмпирически, чтобы последняя буква успела пройти весь путь до сброса.
update()
{
t += 0.001;
if (t >= (1 - 0.24))
{
t = 0;
}
}
Конфигурация и запуск игры
Стандартная конфигурация игры Phaser. Указывается тип рендерера (Phaser.AUTO), элемент-контейнер, размеры холста и главный класс сцены.
const config = {
type: Phaser.AUTO,
parent: 'phaser-example',
width: 800,
height: 600,
scene: Example
};
const game = new Phaser.Game(config);
Что попробовать дальше
Комбинируя DynamicBitmapText, Curves.Path и callback-функцию, можно создавать сложные текстовые анимации без использования спрайтовых листов. Для экспериментов попробуйте: изменить форму пути с помощью других методов, например circleTo или ellipseTo; варьировать смещение между буквами для другого визуального эффекта; модифицировать в callback не только координаты, но и scaleX, scaleY или color для добавления масштабирования или изменения цвета вдоль пути.
