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

Реализация физически точных цепочек, верёвок или связей между объектами — частый запрос в играх, от головоломок до симуляторов. В этом примере мы используем мощный плагин Matter.js для создания интерактивной цепочки, которую игрок может рисовать прямо указателем мыши. Вы научитесь создавать динамические соединения с настраиваемой жёсткостью, управлять статичными точками крепления и реагировать на пользовательский ввод, создавая основу для механик гранаты на верёвке, висячих мостов или цепких щупалец.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    create ()
    {
        this.matter.world.setBounds();

        const sides = 6;
        const size = 14;
        const distance = size * 4;
        const stiffness = 0.1;
        const lastPosition = new Phaser.Math.Vector2();
        const options = { friction: 0.005, frictionAir: 0, restitution: 1 };
        const pinOptions = { friction: 0, frictionAir: 0, restitution: 0, ignoreGravity: true, inertia: Infinity, isStatic: true };

        let current = null;
        let previous = null;

        this.input.on('pointerdown', function (pointer)
        {

            lastPosition.x = pointer.x;
            lastPosition.y = pointer.y;

            previous = this.matter.add.polygon(pointer.x, pointer.y, sides, size, pinOptions);

        }, this);

        this.input.on('pointermove', function (pointer)
        {

            if (pointer.isDown)
            {
                const x = pointer.x;
                const y = pointer.y;

                if (Phaser.Math.Distance.Between(x, y, lastPosition.x, lastPosition.y) > distance)
                {
                    lastPosition.x = x;
                    lastPosition.y = y;

                    current = this.matter.add.polygon(pointer.x, pointer.y, sides, size, options);

                    this.matter.add.constraint(previous, current, distance, stiffness);

                    previous = current;
                }
            }

        }, this);

        this.input.on('pointerup', pointer =>
        {

            previous.isStatic = true;
            previous.ignoreGravity = true;

        }, this);
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#000000',
    parent: 'phaser-example',
    physics: {
        default: 'matter',
        matter: {
            gravity: {
                y: 0.1
            },
            enableSleep: true,
            debug: true
        }
    },
    scene: Example
};

const game = new Phaser.Game(config);

Настройка физического мира и параметров

В начале метода create() сцены инициализируется физический мир Matter.js. Ключевой шаг — установка границ мира, чтобы объекты не улетали за пределы экрана.

Затем определяются константы, управляющие поведением будущей цепочки: - sides и size — задают форму и размер каждого звена (в данном случае это шестиугольники). - distance — минимальное расстояние между центрами звеньев. Новое звено создаётся только если указатель отодвинулся дальше этого значения. - stiffness — жёсткость соединения между звеньями (от 0 до 1). Значение 0.1 создаёт очень гибкую, "тряпичную" связь. - Создаются два набора опций для физических тел: options для обычных звеньев и pinOptions для первого, закреплённого (пинового) звена. Пиновое звено делается статичным (isStatic: true) и невосприимчивым к гравитации, чтобы цепь начиналась с фиксированной точки.

this.matter.world.setBounds();

const sides = 6;
const size = 14;
const distance = size * 4;
const stiffness = 0.1;
const lastPosition = new Phaser.Math.Vector2();
const options = { friction: 0.005, frictionAir: 0, restitution: 1 };
const pinOptions = { friction: 0, frictionAir: 0, restitution: 0, ignoreGravity: true, inertia: Infinity, isStatic: true };

Создание точки крепления и обработка ввода

Механика рисования цепочки строится на обработке трёх событий указателя: нажатия, перемещения и отпускания.

При событии pointerdown (нажатие) фиксируется стартовая позиция. В этой позиции создаётся первое звено цепи — статичный шестиугольник с применением pinOptions. Это звено (previous) станет точкой крепления для всей цепи.

this.input.on('pointerdown', function (pointer)
{
    lastPosition.x = pointer.x;
    lastPosition.y = pointer.y;
    previous = this.matter.add.polygon(pointer.x, pointer.y, sides, size, pinOptions);
}, this);

Событие pointerup (отпускание) финализирует процесс: последнее созданное динамическое звено (previous) также делается статичным и невесомым. Это позволяет "закрепить" конец цепи в воздухе, создавая висящую конструкцию.

this.input.on('pointerup', pointer =>
{
    previous.isStatic = true;
    previous.ignoreGravity = true;
}, this);

Динамическое построение цепи при перемещении

Самое интересное происходит при перемещении нажатого указателя (pointermove).

1. Проверяется условие pointer.isDown, чтобы цепь строилась только при зажатой кнопке. 2. Вычисляется расстояние между текущей позицией указателя и позицией, где было создано последнее звено (lastPosition). Новое звено создаётся только если это расстояние превышает заданный порог distance. Это предотвращает создание множества звеньев в одной точке. 3. Если условие выполнено, обновляется lastPosition и создаётся новое динамическое звено (current) с обычными опциями options. 4. Ключевой вызов this.matter.add.constraint создаёт физическое ограничение (связь) между предыдущим (previous) и текущим (current) звеном. Параметры — максимальная длина связи (distance) и её жёсткость (stiffness). 5. Текущее звено становится previous для следующей итерации, обеспечивая последовательное соединение.

this.input.on('pointermove', function (pointer)
{
    if (pointer.isDown)
    {
        const x = pointer.x;
        const y = pointer.y;
        if (Phaser.Math.Distance.Between(x, y, lastPosition.x, lastPosition.y) > distance)
        {
            lastPosition.x = x;
            lastPosition.y = y;
            current = this.matter.add.polygon(pointer.x, pointer.y, sides, size, options);
            this.matter.add.constraint(previous, current, distance, stiffness);
            previous = current;
        }
    }
}, this);

Конфигурация игры и физического движка

Конфигурация игры активирует физический движок Matter.js. Обратите внимание на параметры внутри physics.matter: - gravity.y: 0.1 — задаёт слабую вертикальную гравитацию. - enableSleep: true — позволяет физическим телам "засыпать" при отсутствии движения для оптимизации. - debug: true — включает отладочную визуализацию тел и ограничений, что крайне полезно при разработке.

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#000000',
    parent: 'phaser-example',
    physics: {
        default: 'matter',
        matter: {
            gravity: { y: 0.1 },
            enableSleep: true,
            debug: true
        }
    },
    scene: Example
};
const game = new Phaser.Game(config);

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

Вы создали прототип интерактивной физической цепи в Phaser с помощью Matter.js. Основные строительные блоки — это создание тел (matter.add.polygon), соединение их ограничениями (matter.add.constraint) и реакция на ввод игрока. Для экспериментов попробуйте: заменить шестиугольники (sides: 6) на круги (matter.add.circle), изменить stiffness на 1.0 для создания жёсткой балки, добавить разрыв цепи при превышении определённой силы в ограничении или использовать цепь как кнут, привязав её к движущемуся игровому персонажу.