О чем этот пример
Визуализация диалогов — ключевой элемент для повествования во многих играх. Готовые UI-ассеты не всегда подходят под уникальный стиль проекта. В этой статье мы разберем, как с нуля создавать кастомизируемые речевые пузыри, используя графический контекст Phaser. Вы научитесь рисовать скругленные прямоугольники с тенями, добавлять стрелки-указатели и, что самое важное, — точно центрировать многострочный текст внутри графической фигуры, что пригодится для создания любых пользовательских интерфейсов.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Example extends Phaser.Scene
{
preload ()
{
this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
this.load.image('akira', 'assets/pics/akira.jpg');
}
create ()
{
this.add.image(400, 350, 'akira').setScale(0.7);
this.createSpeechBubble(20, 20, 320, 160, '“Twin ceramic rotor drives on each wheel! And these look like computer controlled anti-lock brakes! Wow, 200 horses at 12,000 rpm!”');
this.createSpeechBubble(370, 120, 400, 180, '“Kaneda, you\'ve always been a pain in the ass, you know. You\'ve been telling me what to do since we were kids. You always treat me like a kid. You always show up and start bossing me around, and don\'t you deny it!”');
this.createSpeechBubble(70, 400, 250, 100, '“And now you\'re a boss, too... of this pile of rubble.”');
}
createSpeechBubble (x, y, width, height, quote)
{
const bubbleWidth = width;
const bubbleHeight = height;
const bubblePadding = 10;
const arrowHeight = bubbleHeight / 4;
const bubble = this.add.graphics({ x: x, y: y });
// Bubble shadow
bubble.fillStyle(0x222222, 0.5);
bubble.fillRoundedRect(6, 6, bubbleWidth, bubbleHeight, 16);
// Bubble color
bubble.fillStyle(0xffffff, 1);
// Bubble outline line style
bubble.lineStyle(4, 0x565656, 1);
// Bubble shape and outline
bubble.strokeRoundedRect(0, 0, bubbleWidth, bubbleHeight, 16);
bubble.fillRoundedRect(0, 0, bubbleWidth, bubbleHeight, 16);
// Calculate arrow coordinates
const point1X = Math.floor(bubbleWidth / 7);
const point1Y = bubbleHeight;
const point2X = Math.floor((bubbleWidth / 7) * 2);
const point2Y = bubbleHeight;
const point3X = Math.floor(bubbleWidth / 7);
const point3Y = Math.floor(bubbleHeight + arrowHeight);
// Bubble arrow shadow
bubble.lineStyle(4, 0x222222, 0.5);
bubble.lineBetween(point2X - 1, point2Y + 6, point3X + 2, point3Y);
// Bubble arrow fill
bubble.fillTriangle(point1X, point1Y, point2X, point2Y, point3X, point3Y);
bubble.lineStyle(2, 0x565656, 1);
bubble.lineBetween(point2X, point2Y, point3X, point3Y);
bubble.lineBetween(point1X, point1Y, point3X, point3Y);
const content = this.add.text(0, 0, quote, { fontFamily: 'Arial', fontSize: 20, color: '#000000', align: 'center', wordWrap: { width: bubbleWidth - (bubblePadding * 2) } });
const b = content.getBounds();
content.setPosition(bubble.x + (bubbleWidth / 2) - (b.width / 2), bubble.y + (bubbleHeight / 2) - (b.height / 2));
}
}
const config = {
type: Phaser.AUTO,
width: 800,
height: 600,
parent: 'phaser-example',
scene: Example
};
const game = new Phaser.Game(config);
Подготовка сцены и основа метода
В примере используется одна сцена. В методе preload загружается фоновая картинка. Вся магия происходит в create, где фон размещается на сцене, а затем трижды вызывается пользовательский метод createSpeechBubble с разными координатами, размерами и текстом.
Метод createSpeechBubble — это сердце примера. Он принимает стартовые координаты (x, y), ширину и высоту будущего пузыря, а также строку с текстом. Внутри метода создается объект Graphics, который является основным инструментом для рисования примитивов в Phaser.
const bubble = this.add.graphics({ x: x, y: y });
Рисуем тело пузыря с тенью и обводкой
Phaser Graphics работает как холст: мы задаем стили, а затем вызываем команды для отрисовки фигур. Стили применяются ко всем последующим операциям, пока не будут изменены.
Сначала рисуется тень. Устанавливается цвет и полупрозрачность с помощью fillStyle, затем рисуется скругленный прямоугольник fillRoundedRect со смещением. Обратите внимание, что все координаты внутри Graphics считаются относительно его позиции (`x,y`), заданной при создании.
// Bubble shadow
bubble.fillStyle(0x222222, 0.5);
bubble.fillRoundedRect(6, 6, bubbleWidth, bubbleHeight, 16);
После этого стили меняются для отрисовки основного тела пузыря: белая заливка и серая обводка толщиной 4 пикселя. Сначала задается lineStyle для обводки, а затем одной командой strokeRoundedRect рисуется контур, а fillRoundedRect — заливка. Порядок важен: если рисовать заливку после обводки, она может перекрыть часть линии.
// Bubble color and outline
bubble.fillStyle(0xffffff, 1);
bubble.lineStyle(4, 0x565656, 1);
bubble.strokeRoundedRect(0, 0, bubbleWidth, bubbleHeight, 16);
bubble.fillRoundedRect(0, 0, bubbleWidth, bubbleHeight, 16);
Создание и стилизация стрелки
Стрелка, указывающая на говорящего, рисуется как залитый треугольник с обводкой. Её тень рисуется отдельной линией.
Ключевой момент — расчет координат трех вершин треугольника (point1, point2, point3). Они привязаны к пропорциям пузыря: основание стрелки располагается по центру нижней границы, а её острие опущено на величину arrowHeight (четверть высоты пузыря). Это делает стрелку адаптивной к размеру баббла.
const point1X = Math.floor(bubbleWidth / 7);
const point1Y = bubbleHeight;
const point2X = Math.floor((bubbleWidth / 7) * 2);
const point2Y = bubbleHeight;
const point3X = Math.floor(bubbleWidth / 7);
const point3Y = Math.floor(bubbleHeight + arrowHeight);
Сначала рисуется тень стрелки с помощью lineBetween. Затем сама стрелка: fillTriangle создает залитый треугольник, а lineBetween дорисовывает его границы.
bubble.fillTriangle(point1X, point1Y, point2X, point2Y, point3X, point3Y);
bubble.lineStyle(2, 0x565656, 1);
bubble.lineBetween(point2X, point2Y, point3X, point3Y);
bubble.lineBetween(point1X, point1Y, point3X, point3Y);
Добавление и точное центрирование текста
Текст создается как отдельный игровой объект с помощью this.add.text. В стилях критически важно свойство wordWrap. Оно ограничивает ширину текста, чтобы он переносился и не выходил за границы пузыря. Ширина рассчитывается как bubbleWidth - (bubblePadding * 2).
const content = this.add.text(0, 0, quote, {
fontFamily: 'Arial',
fontSize: 20,
color: '#000000',
align: 'center',
wordWrap: { width: bubbleWidth - (bubblePadding * 2) }
});
Самый изящный трюк — центрирование. Объект Text изначально помещается в (0,0). Метод getBounds() возвращает его геометрические границы (ширину и высоту с учетом переносов). Эти данные используются для вычисления позиции, которая поместит текст ровно по центру графического пузыря. Позиция рассчитывается относительно позиции (bubble.x, bubble.y) самого объекта Graphics.
const b = content.getBounds();
content.setPosition(
bubble.x + (bubbleWidth / 2) - (b.width / 2),
bubble.y + (bubbleHeight / 2) - (b.height / 2)
);
Что попробовать дальше
Вы освоили мощный подход к созданию динамических UI-элементов в Phaser, комбинируя рисование графики и работу с текстом. Этот метод дает полный контроль над стилем. Для экспериментов попробуйте: анимировать появление пузыря через tweens, менять форму стрелки или тело пузыря на эллипс, добавить разные стили текста (жирный, курсив) внутри одной цитаты с помощью setTextStyle, или сделать систему, где стрелка автоматически поворачивается в сторону заданной точки (например, спрайта персонажа).
