О чем этот пример
При отрисовке скруглённых прямоугольников в Phaser можно столкнуться с неочевидной ошибкой: часть обводки не отображается. Эта статья разбирает пример из баг-трекера Phaser и показывает, как работает низкоуровневый рендеринг через `commandBuffer`. Понимание этой механики поможет вам отлаживать сложную векторную графику и писать собственные функции рисования, избегая подобных артефактов.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
var config = {
width: 800,
height: 600,
type: Phaser.AUTO,
parent: 'phaser-example',
scene: {
create: create
}
};
var game = new Phaser.Game(config);
function arc (g, x, y, radius, startAngle, endAngle, anticlockwise, overshoot)
{
if (anticlockwise === undefined) { anticlockwise = false; }
if (overshoot === undefined) { overshoot = 0; }
g.commandBuffer.push(
0,
x, y, radius, startAngle, endAngle, anticlockwise, overshoot
);
}
function strokeRoundedRect (g, x, y, width, height, radius)
{
if (radius === undefined) { radius = 20; }
var tl = radius;
var tr = radius;
var bl = radius;
var br = radius;
var maxRadius = Math.min(width, height) / 2;
if (typeof radius !== 'number')
{
tl = Phaser.Utils.Objects.GetFastValue(radius, 'tl', 20);
tr = Phaser.Utils.Objects.GetFastValue(radius, 'tr', 20);
bl = Phaser.Utils.Objects.GetFastValue(radius, 'bl', 20);
br = Phaser.Utils.Objects.GetFastValue(radius, 'br', 20);
}
tl = Math.min(tl, maxRadius);
tr = Math.min(tr, maxRadius);
bl = Math.min(bl, maxRadius);
br = Math.min(br, maxRadius);
g.beginPath();
// g.moveTo(x + tl, y);
// g.lineTo(x + width - tr, y);
g.moveTo(480, y);
// g.moveTo(x + width - tr, y);
// g.arc(x + width - tr, y + tr, tr, -Phaser.Math.TAU, 0);
// The issue is a moveTo that ends on the exact same x as the arc - if they differ, even slightly, it renders fine
g.commandBuffer.push(
0,
x + width - tr, y + tr, tr, -Phaser.Math.TAU, 0, false, 0
);
// g.lineTo(x + width, y + height - br);
// g.moveTo(x + width, y + height - br);
// g.arc(x + width - br, y + height - br, br, 0, Phaser.Math.TAU);
// g.lineTo(x + bl, y + height);
// g.moveTo(x + bl, y + height);
// g.arc(x + bl, y + height - bl, bl, Phaser.Math.TAU, Math.PI);
// g.lineTo(x, y + tl);
// g.moveTo(x, y + tl);
// g.arc(x + tl, y + tl, tl, -Math.PI, -Phaser.Math.TAU);
g.strokePath();
}
function create ()
{
this.add.text(500, 32, 'v16');
var graphics = this.add.graphics();
graphics.lineStyle(6, 0xffff00, 1);
graphics.strokeRoundedRect(200, 140, 300, 100);
// strokeRoundedRect(graphics, 200, 140, 300, 100);
}
Суть проблемы: пропадающая линия
В исходном примере функция strokeRoundedRect должна нарисовать контур скруглённого прямоугольника с помощью Graphics. Однако верхняя правая дуга (arc) не отображается.
Ключевая строка, где возникает проблема:
g.moveTo(480, y);
Далее сразу вызывается низкоуровневая команда для отрисовки дуги. Координата X точки, куда выполнен moveTo (480), в точности совпадает с координатой X центра дуги (x + width - tr). Если бы эти значения хоть немного отличались, дуга нарисовалась бы корректно. Это и есть баг в конвейере рендеринга Phaser на момент версии 3.16.
Как работает commandBuffer в Phaser Graphics
Класс Graphics в Phaser использует буфер команд (commandBuffer) для оптимизации отрисовки. Вместо немедленного вызова методов Canvas API, команды (тип операции и её параметры) складываются в массив и выполняются позже.
В примере автор вручную эмулирует вызов g.arc, напрямую помещая данные в буфер. Вот как выглядит структура команды для дуги:
g.commandBuffer.push(
0, // Код операции (0 = arc)
x + width - tr, // centerX
y + tr, // centerY
tr, // radius
-Phaser.Math.TAU, // startAngle
0, // endAngle
false, // anticlockwise
0 // overshoot
);
Прямая работа с commandBuffer — это мощный, но низкоуровневый приём. В обычной разработке вы используете методы вроде graphics.arc() или graphics.strokeRoundedRect(), которые сами управляют буфером.
Анализ функции strokeRoundedRect
Давайте разберём кастомную функцию из примера, чтобы понять логику построения контура.
function strokeRoundedRect (g, x, y, width, height, radius) {
// ... код валидации и вычисления радиусов ...
g.beginPath();
g.moveTo(480, y);
// Ручной вызов arc через commandBuffer
g.commandBuffer.push(0, x + width - tr, y + tr, tr, -Phaser.Math.TAU, 0, false, 0);
g.strokePath();
}
Функция начинает новый путь (beginPath), перемещает «перо» в точку (480, y), а затем добавляет команду на отрисовку дуги. В конце вызывается strokePath(), который и отправляет все накопленные в буфере команды на отрисовку. Проблема возникает именно на стыке команд moveTo и arc из-за совпадения координат.
Практическое решение и обходной путь
Простейший способ избежать бага — гарантировать, что конечная точка moveTo не совпадает с центром последующей дуги. Например, можно сдвинуть точку на микроскопическое значение:
// Вместо g.moveTo(480, y);
g.moveTo(480.001, y); // Минимальный сдвиг исправляет рендеринг
Однако в реальном проекте вы, скорее всего, будете использовать штатный метод Graphics:
// Правильный и рекомендуемый способ в Phaser 3
var graphics = this.add.graphics();
graphics.lineStyle(6, 0xffff00, 1);
graphics.strokeRoundedRect(200, 140, 300, 100);
Встроенный метод strokeRoundedRect уже содержит все необходимые проверки и корректно работает. Кастомная функция из примера — это, по сути, учебный код, раскрывающий внутреннее устройство системы.
Что попробовать дальше
Разбор этого бага учит нас двум важным вещам: как Phaser оптимизирует отрисовку графики через буфер команд и насколько критична точность координат при ручном управлении путём (path). Для экспериментов попробуйте модифицировать пример: нарисуйте более сложную фигуру с несколькими дугами, попробуйте смещать координаты moveTo и наблюдайте за результатом. И всегда помните, что перед написанием собственных низкоуровневых функций стоит проверить, нет ли уже готового и отлаженного метода в API Graphics.
