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

Одним из ключевых преимуществ Phaser является мощная интеграция физических движков. Этот пример демонстрирует, как использовать Matter.js для создания интерактивной, физически точной линии, которую можно рисовать курсором, а затем взаимодействовать с ней объектами игры. Такой подход полезен для создания динамических барьеров, веревок, мостов или любых деформируемых поверхностей в ваших играх, где важна реалистичная реакция на столкновения.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('ball', 'assets/sprites/pangball.png');
    }

    create ()
    {
        this.matter.world.setBounds(0, 0, 800, 600, 32, true, true, false, true);

        const lineCategory = this.matter.world.nextCategory();
        const ballsCategory = this.matter.world.nextCategory();

        const sides = 4;
        const size = 14;
        const distance = size;
        const stiffness = 0.1;
        const lastPosition = new Phaser.Math.Vector2();
        const options = { friction: 0, frictionAir: 0, restitution: 0, ignoreGravity: true, inertia: Infinity, isStatic: true, angle: 0, collisionFilter: { category: lineCategory } };

        let current = null;
        let previous = null;

        const curves = [];
        let curve = null;

        const graphics = this.add.graphics();

        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, options);

            curve = new Phaser.Curves.Spline([ pointer.x, pointer.y ]);

            curves.push(curve);

        }, 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)
                {
                    options.angle = Phaser.Math.Angle.Between(x, y, lastPosition.x, lastPosition.y);

                    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;

                    curve.addPoint(x, y);

                    graphics.clear();
                    graphics.lineStyle(size * 1.5, 0xffffff);

                    curves.forEach(c =>
                    {
                        c.draw(graphics, 64);
                    });
                }
            }

        }, this);

        this.input.once('pointerup', function (pointer)
        {

            this.time.addEvent({
                delay: 1000,
                callback: function ()
                {
                    const ball = this.matter.add.image(Phaser.Math.Between(100, 700), Phaser.Math.Between(-600, 0), 'ball');

                    ball.setCircle();
                    ball.setCollisionCategory(ballsCategory);
                    ball.setFriction(0.005).setBounce(0.9);
                },
                callbackScope: this,
                repeat: 100
            });

        }, this);
    }
}

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

const game = new Phaser.Game(config);

Настройка мира Matter.js и категорий столкновений

В начале сцены в методе create() настраивается физический мир. Важным шагом является определение границ и категорий столкновений.

this.matter.world.setBounds(0, 0, 800, 600, 32, true, true, false, true);
const lineCategory = this.matter.world.nextCategory();
const ballsCategory = this.matter.world.nextCategory();

Метод setBounds задает границы мира с толщиной в 32 пикселя. Флаги указывают, какие границы (верх, низ, лево, право) создавать. Здесь не создается левая граница (false).

Методы nextCategory() генерируют битовые маски для фильтрации столкновений. Это позволяет разделить объекты на категории: одна для линии, другая для шаров, чтобы они могли сталкиваться друг с другом, но не взаимодействовали внутри своей группы без необходимости.

Подготовка параметров и реакция на начало рисования

Задаются константы для будущих сегментов линии: количество сторон многоугольника, размер, расстояние между точками привязки и жесткость соединяющих их ограничений (constraints). Создается объект options для физических тел.

const sides = 4;
const size = 14;
const distance = size;
const stiffness = 0.1;
const options = { friction: 0, frictionAir: 0, restitution: 0, ignoreGravity: true, inertia: Infinity, isStatic: true, angle: 0, collisionFilter: { category: lineCategory } };

Обработчик события pointerdown создает первую точку линии. В этой позиции создается статичное физическое тело в форме многоугольника (this.matter.add.polygon) и начинается новая кривая Phaser.Curves.Spline для её визуального отображения через Graphics.

previous = this.matter.add.polygon(pointer.x, pointer.y, sides, size, options);
curve = new Phaser.Curves.Spline([ pointer.x, pointer.y ]);

Рисование линии: связывание физических тел и графика

Основная логика находится в обработчике pointermove. При движении зажатой мыши с заданным интервалом (distance) создаются новые тела и соединяются с предыдущими.

if (Phaser.Math.Distance.Between(x, y, lastPosition.x, lastPosition.y) > distance) {
    options.angle = Phaser.Math.Angle.Between(x, y, lastPosition.x, lastPosition.y);
    current = this.matter.add.polygon(pointer.x, pointer.y, sides, size, options);
    this.matter.add.constraint(previous, current, distance, stiffness);

Phaser.Math.Angle.Between вычисляет угол между точками и задает вращение тела (options.angle), чтобы ромбы выстраивались вдоль линии. this.matter.add.constraint создает пружинное соединение (ограничение) между двумя телами с заданной длиной и жесткостью (stiffness). Низкое значение жесткости (0.1) делает линию очень гибкой.

Затем кривая (curve) обновляется новой точкой, и весь массив кривых перерисовывается с помощью объекта Graphics.

graphics.clear();
graphics.lineStyle(size * 1.5, 0xffffff);
curves.forEach(c => { c.draw(graphics, 64); });

Запуск симуляции: взаимодействие с шарами

Когда пользователь отпускает кнопку мыши (pointerup), запускается таймер, который каждую секунду создает падающие шары.

this.time.addEvent({
    delay: 1000,
    callback: function () {
        const ball = this.matter.add.image(Phaser.Math.Between(100, 700), Phaser.Math.Between(-600, 0), 'ball');
        ball.setCircle();
        ball.setCollisionCategory(ballsCategory);
        ball.setFriction(0.005).setBounce(0.9);
    },
    callbackScope: this,
    repeat: 100
});

Шар создается как физический спрайт (matter.add.image). setCircle() задает ему коллайдер в форме круга. setCollisionCategory() назначает категорию шаров, отличную от категории линии, что обеспечивает их столкновение. Настройки трения и упругости (setFriction, setBounce) делают поведение шаров реалистичным.

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

Этот пример отлично демонстрирует симбиоз физической симуляции Matter.js и инструментов визуализации Phaser. Вы создаете не просто графическую линию, а цепочку связанных физических тел, что открывает огромные возможности для геймдизайна. Для экспериментов попробуйте изменить stiffness на 1.0, чтобы линия стала абсолютно жесткой, или restitution в опциях тел, чтобы они отскакивали. Можно заменить многоугольники (polygon) на круги (circle) или создать интерактивный механизм удаления сегментов линии по клику.