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

При создании игр с нестандартной геометрией объектов часто возникает задача связать графический полигон с физическим телом. Пример из бага #6178 наглядно показывает две стратегии решения: прямую привязку и корректировку вершин. Понимание этих подходов избавит вас от "плывущих" или неправильно отображающихся фигур и сделает работу с физикой предсказуемой.

Версия Phaser: код и демо в этой статье рассчитаны на Phaser 3.90.0.

Живой запуск

Ниже встроен рабочий билд примера. Оригинальный источник: GitHub.

Исходный код


class Example extends Phaser.Scene
{
    constructor ()
    {
        super();
    }

    create ()
    {
        this.matter.world.setBounds(0, 0, 800, 600);
        this.matter.add.mouseSpring({ stiffness: 0.1 });

        const verts = [
          { x: 50, y: 0 },
          { x: 100, y: 60 },
          { x: 0, y: 75 }
        ];

        //  This will work properly because the vertices are
        //  centered around 0x0

        const body = this.matter.add.fromVertices(300, 300, verts);

        const poly = this.add.polygon(body.position.x, body.position.y, verts, 0x8d8d8d);

        //  Tell the Game Object to use the existing body, don't create a new one
        this.matter.add.gameObject(poly, body, false);

        //  In this case the verts are not centered, so we cannot
        //  use them to create the Polygon from. Instead we create
        //  the body first and use _those_ verts to create the Polygon

        const verts2 = [
            { x: 0, y: -50 },
            { x: 50, y: 10 },
            { x: -50, y: 25 }
        ];

        const body2 = this.matter.add.fromVertices(500, 300, verts2);

        const polyVerts = [];

        const bx = body2.position.x;
        const by = body2.position.y;

        const cx = body2.centerOffset.x;
        const cy = body2.centerOffset.y;

        body2.vertices.forEach(vert => {
            polyVerts.push({ x: vert.x - bx + cx, y: vert.y - by + cy });
        });

        const poly2 = this.add.polygon(bx, by, polyVerts, 0x8d8d8d);

        //  Account for the fact that in this set of verts, the
        //  origin isn't the center
        poly2.setDisplayOrigin(cx, cy);

        this.matter.add.gameObject(poly2, body2, false);
    }
}

const config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    scene: Example,
    physics: {
        default: 'matter',
        matter: {
            gravity: { y: 0 },
            debug: {

                showAngleIndicator: true,
                angleColor: 0xe81153,

                showVelocity: true,
                velocityColor: 0x00aeef,

                showCollisions: true,
                collisionColor: 0xf5950c,

                showBody: true,
                showInternalEdges: true,

                showPositions: true,
                positionSize: 4,
                positionColor: 0xe042da,

                showJoint: true,
                jointColor: 0xe0e042,
                jointLineOpacity: 1,
                jointLineThickness: 2,

                pinSize: 4,
                pinColor: 0x42e0e0,

                springColor: 0xe042e0,

                anchorColor: 0xefefef,
                anchorSize: 4,

                showConvexHulls: true,
                hullColor: 0xd703d0
            }
        }
    }
};

const game = new Phaser.Game(config);

Проблема центрирования вершин

Физический движок Matter.js ожидает, что вершины полигона (verts) заданы относительно его центра масс (точки 0,0). Если вы передадите в fromVertices() координаты "как есть", тело может быть создано, но его визуальное представление и физическая модель будут расходиться.

В примере первый полигон создается из вершин, уже центрированных вокруг нуля. Это идеальный случай.

const verts = [
  { x: 50, y: 0 },
  { x: 100, y: 60 },
  { x: 0, y: 75 }
];

Простой случай: готовые центрированные вершины

Когда вершины изначально заданы относительно центра, процесс становится тривиальным. Создаем физическое тело, затем графический полигон с теми же вершинами и связываем их. Ключевой момент — передача false в gameObject(), чтобы Phaser не создавал новое тело.

// Создаем тело из вершин
const body = this.matter.add.fromVertices(300, 300, verts);
// Создаем графический полигон с теми же вершинами
const poly = this.add.polygon(body.position.x, body.position.y, verts, 0x8d8d8d);
// Связываем объект с существующим телом
this.matter.add.gameObject(poly, body, false);

Сложный случай: вершины не центрированы

Часто вершины заданы в абсолютных или произвольных координатах. В примере verts2 заданы не вокруг нуля. Если использовать их напрямую для графики, полигон нарисуется не там, где находится тело.

const verts2 = [
    { x: 0, y: -50 },
    { x: 50, y: 10 },
    { x: -50, y: 25 }
];

Поэтому используется обходной путь: сначала создаем тело, а для графики используем уже обработанные движком вершины тела (body2.vertices).

Корректировка вершин для графики

Вершины, хранящиеся в созданном теле (body2.vertices), находятся в мировых координатах. Чтобы нарисовать полигон, нужно преобразовать их в локальные координаты относительно позиции тела, учитывая смещение центра (centerOffset).

const polyVerts = [];
const bx = body2.position.x;
const by = body2.position.y;
const cx = body2.centerOffset.x;
const cy = body2.centerOffset.y;

body2.vertices.forEach(vert => {
    polyVerts.push({ x: vert.x - bx + cx, y: vert.y - by + cy });
});

Вычитание bx, by переводит мировые координаты в локальные. Добавление cx, cy компенсирует смещение центра тела, чтобы полигон отрисовался правильно.

Связывание и настройка отображения

После подготовки вершин создается графический полигон. Важный шаг — установить для него точку отображения (displayOrigin), совпадающую со смещением центра тела. Это гарантирует, что вращение и позиция графики будут синхронизированы с физикой.

const poly2 = this.add.polygon(bx, by, polyVerts, 0x8d8d8d);
poly2.setDisplayOrigin(cx, cy);
this.matter.add.gameObject(poly2, body2, false);

Метод setDisplayOrigin(cx, cy) смещает точку привязки спрайта/полигона, что особенно важно для корректного вращения несимметричных фигур.

Что попробовать дальше

Для надежной связи полигональной графики с Matter.js телом всегда проверяйте, центрированы ли исходные вершины. Если да — используйте простой путь. Если нет — создавайте тело первым, а для графики берите и преобразовывайте вершины из body.vertices. Экспериментируйте: попробуйте пропустить setDisplayOrigin и посмотрите на вращение объекта, или создайте сложный многоугольник и проверьте, как centerOffset влияет на его поведение.