О чем этот пример

При работе с геометрией в 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() для перевода их в общую систему координат и проверки пересечения.