О чем этот пример
При работе с геометрией в Phaser вы можете столкнуться с неожиданным поведением метода `getBounds()` у полигонов. Исходный код примера наглядно демонстрирует эту проблему: два полигона с разными координатами вершин, но одинаковой позицией на сцене, возвращают одинаковые границы. Эта статья поможет разобраться, почему так происходит, как Phaser рассчитывает границы объектов и какие альтернативные способы получения геометрических данных существуют в API. Понимание этих нюансов критично для реализации точных коллизий, отсечения видимости и других игровых механик, опирающихся на геометрию.
Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.
Живой запуск
Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.
Исходный код
class Test extends Phaser.Scene
{
create ()
{
var redPoly = this.add.polygon(100,100, [[-20,-20], [20, -20], [20, 20], [-20,20]]).setStrokeStyle(1, 0xff0000);
var greenPoly = this.add.polygon(100,100, [[0,0], [40, 0], [40, 40], [0,40]]).setStrokeStyle(1, 0x00ff00);
// console.log("Red:", redPoly)
// console.log("Green:", greenPoly)
console.log("Red Polygon bounds:", redPoly.getBounds());
console.log("Green Polygon bounds:", greenPoly.getBounds());
var p1 = new Phaser.Geom.Polygon([[-20,-20], [20, -20], [20, 20], [-20,20]]);
var b1 = Phaser.Geom.Polygon.GetAABB(p1);
console.log(b1);
}
}
var game = new Phaser.Game({
width: 800,
height: 600,
type: Phaser.AUTO,
parent: 'phaser-example',
backgroundColor: "#242424",
scene: Test
});
Проблема: неожиданные границы полигонов
В примере создаются два полигона с одинаковой позицией на сцене (100, 100), но с разными наборами вершин относительно этой точки.
var redPoly = this.add.polygon(100, 100, [[-20, -20], [20, -20], [20, 20], [-20, 20]]).setStrokeStyle(1, 0xff0000);
var greenPoly = this.add.polygon(100, 100, [[0, 0], [40, 0], [40, 40], [0, 40]]).setStrokeStyle(1, 0x00ff00);
Красный полигон (redPoly) — это квадрат 40x40, чьи вершины смещены на ±20 от центра. Зеленый полигон (greenPoly) — это квадрат 40x40, начинающийся в точке (0,0) относительно центра и уходящий в положительные координаты.
Логично ожидать, что их мировые границы будут разными. Однако вывод в консоль показывает обратное: метод getBounds() для обоих объектов возвращает одинаковый прямоугольник.
console.log("Red Polygon bounds:", redPoly.getBounds());
console.log("Green Polygon bounds:", greenPoly.getBounds());
Это происходит потому, что метод getBounds() игрового объекта (Phaser.GameObjects.Polygon) в данной версии может возвращать границы, рассчитанные не на основе реальных вершин в мировых координатах, а на основе внутреннего представления позиции и размера объекта. В данном случае оба полигона имеют одну позицию (x, y) и, по-видимому, схожие внутренние размеры, что и приводит к одинаковому результату.
Решение: используем геометрию Phaser напрямую
Для получения точных границ полигона, основанных именно на его вершинах, необходимо работать с геометрическим классом Phaser.Geom.Polygon. Этот класс существует отдельно от системы отображения и предназначен для чистых геометрических вычислений.
В примере создается геометрический полигон p1 с теми же вершинами, что и у красного игрового объекта.
var p1 = new Phaser.Geom.Polygon([[-20, -20], [20, -20], [20, 20], [-20, 20]]);
Затем с помощью статического метода Phaser.Geom.Polygon.GetAABB() вычисляется его ограничивающий прямоугольник (Axis-Aligned Bounding Box).
var b1 = Phaser.Geom.Polygon.GetAABB(p1);
console.log(b1);
Ключевое отличие: вершины геометрического полигона задаются в его локальной системе координат. Метод GetAABB() корректно анализирует массив этих вершин и возвращает прямоугольник Phaser.Geom.Rectangle, который точно описывает полигон. Если вам нужны мировые координаты, вам придется самостоятельно сдвинуть (translate) этот прямоугольник на позицию (x, y) вашего игрового объекта-полигона.
Практический пример: расчет мировых границ
Давайте напишем вспомогательную функцию, которая для любого Phaser.GameObjects.Polygon возвращает его точные мировые границы в виде Phaser.Geom.Rectangle.
function getPreciseBounds(polygonGameObject) {
// 1. Получаем геометрию полигона (вершины в локальных координатах)
const geomPoly = new Phaser.Geom.Polygon(polygonGameObject.geom.points);
// 2. Рассчитываем его локальный AABB
const localAABB = Phaser.Geom.Polygon.GetAABB(geomPoly);
// 3. Создаем новый прямоугольник, который будет мировым AABB
const worldBounds = new Phaser.Geom.Rectangle(
polygonGameObject.x + localAABB.x,
polygonGameObject.y + localAABB.y,
localAABB.width,
localAABB.height
);
return worldBounds;
}
Эту функцию можно использовать в сцене:
const redWorldBounds = getPreciseBounds(redPoly);
const greenWorldBounds = getPreciseBounds(greenPoly);
console.log("Точные границы красного:", redWorldBounds);
console.log("Точные границы зеленого:", greenWorldBounds);
Теперь redWorldBounds и greenWorldBounds будут разными прямоугольниками, правильно отражающими положение каждого квадрата на игровом поле. Обратите внимание на доступ к вершинам через polygonGameObject.geom.points.
Когда это важно: коллизии и отсечение
Понимание реальных границ объекта необходимо в нескольких ключевых сценариях:
1. **Кастомные коллизии:** Если вы реализуете проверку столкновений «полигон-полигон» или «полигон-точка» без использования физического движка Phaser (Arcade, Matter), вам потребуются точные мировые координаты вершин.
2. **Отсечение (Culling):** При оптимизации рендеринга для больших карт вы можете проверять, попадает ли полигон в область видимости камеры. Неточные границы от getBounds() приведут к тому, что объекты будут исчезать или появляться не в то время.
3. **Выравнивание и позиционирование:** Точное расположение сложных фигур относительно других элементов интерфейса или игрового мира.
Для простых AABB-коллизий (когда объекты не вращаются) полученного через GetAABB() прямоугольника будет достаточно. Для более сложных случаев используйте методы из Phaser.Geom.Polygon, такие как Contains() или ContainsPoint().
// Проверка, содержит ли полигон точку (в мировых координатах)
const geomPolyForCheck = new Phaser.Geom.Polygon(greenPoly.geom.points);
// Не забудьте сдвинуть геометрию в мировые координаты
Phaser.Geom.Polygon.Translate(geomPolyForCheck, greenPoly.x, greenPoly.y);
const pointToCheck = { x: 120, y: 120 };
const isInside = Phaser.Geom.Polygon.ContainsPoint(geomPolyForCheck, pointToCheck);
console.log(`Точка внутри зеленого полигона? ${isInside}`);
Что попробовать дальше
Встроенный метод getBounds() игровых объектов в Phaser не всегда возвращает ожидаемые границы для полигонов, так как может опираться на упрощенную внутреннюю логику. Для точных геометрических расчетов всегда работайте с чистым классом Phaser.Geom.Polygon и его статическими методами, такими как GetAABB(). Это дает полный контроль над вершинами фигуры и гарантирует корректность вычислений.
**Идеи для экспериментов:**
1. Модифицируйте вспомогательную функцию getPreciseBounds(), чтобы она учитывала также масштаб (scaleX, scaleY) и поворот (rotation) объекта.
2. Создайте сцену с множеством случайных полигонов и реализуйте систему их отсечения за пределами камеры, используя точные границы.
3. Реализуйте простую систему столкновений между полигонами, используя метод Phaser.Geom.Polygon.Translate() для перевода их в общую систему координат и проверки пересечения.
