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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    tempLine = new Phaser.Geom.Line();
    duration = 5000;
    t = -1;
    block;
    curve;

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('block', 'assets/sprites/flower-exo.png');
    }

    create ()
    {
        const graphics = this.add.graphics();

        // let line = new Phaser.Geom.Line(Phaser.Math.Between(100, 700), Phaser.Math.Between(100, 500), Phaser.Math.Between(100, 700), Phaser.Math.Between(100, 500));

        let line = new Phaser.Geom.Line(100, 500, 700, 100);

        // graphics.fillStyle(0xff0000, 1);
        // graphics.fillCircle(line.x1, line.y1, 8);
        // graphics.fillStyle(0xff00ff, 1);
        // graphics.fillCircle(line.x2, line.y2, 8);

        let points = [];

        points.push(line.getPointA());

        const length = Phaser.Geom.Line.Length(line);
        const waves = Math.ceil(length / 200);

        let vx = 100;
        let vy = 100;
        let prevX = line.x1;
        let prevY = line.y1;

        for (let i = 1; i <= waves; i++)
        {
            let currentPoint = line.getPoint(i / waves);

            // graphics.fillStyle(0xffff00).fillCircle(currentPoint.x, currentPoint.y, 4);

            let ray = new Phaser.Geom.Line(prevX, prevY, currentPoint.x, currentPoint.y);

            // graphics.lineStyle(1, 0xffffff).strokeLineShape(ray);

            let normal = Phaser.Geom.Line.GetNormal(ray);
            let midPoint = Phaser.Geom.Line.GetMidPoint(ray);

            // graphics.fillStyle(0x00ff00).fillCircle(midPoint.x + normal.x * vx, midPoint.y + normal.y * vy, 4);

            points.push(new Phaser.Math.Vector2(midPoint.x + normal.x * vx, midPoint.y + normal.y * vy));

            prevX = currentPoint.x;
            prevY = currentPoint.y;

            vx *= -1;
            vy *= -1;
        }

        points.push(line.getPointB());

        this.curve = new Phaser.Curves.Spline(points);

        graphics.lineStyle(1, 0xffffff, 1);
        this.curve.draw(graphics, 64);

        this.block = this.matter.add.image(line.x1, line.y1, 'block');

        this.block.setFriction(0);
        this.block.setFrictionAir(0);
        this.block.setBounce(0);

        this.input.once('pointerdown', () =>
        {
            this.t = 0;
        }, this);
    }

    update (time, delta)
    {
        if (this.t === -1)
        {
            return;
        }

        this.t += delta;

        if (this.t >= this.duration)
        {
            //  Reached the end
            this.block.setVelocity(0, 0);
        }
        else
        {
            const d = (this.t / this.duration);

            const p = this.curve.getPoint(d);

            this.block.setPosition(p.x, p.y);
        }
    }
}

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

const game = new Phaser.Game(config);

Подготовка сцены и построение базовой линии

Вся логика примера содержится в классе сцены, который наследуется от Phaser.Scene. В начале определяются свойства класса: временная линия для геометрических операций, общая длительность движения, счётчик времени и ссылки на сам объект и кривую.

В методе preload загружается спрайт для нашего движущегося объекта.

В create начинается основная работа. Сначала создаётся графический объект graphics для отладки и визуализации. Затем определяется прямая линия от точки (100, 500) до (700, 100). Эта линия будет служить основным направляющим вектором для нашей будущей извилистой траектории.

let line = new Phaser.Geom.Line(100, 500, 700, 100);

Генерация волнообразного пути (сплайна)

Цель — превратить прямую линию в плавную волнообразную кривую. Для этого мы создадим массив контрольных точек для сплайна.

Сначала в массив points добавляется начальная точка линии (Point A). Затем линия делится на несколько сегментов, количество которых (waves) зависит от её длины. Чем длиннее линия, тем больше "волн" будет у траектории.

В цикле для каждого сегмента вычисляется его середина (midPoint) и нормаль (normal) — перпендикулярный вектор к отрезку линии. Добавляя к середине отрезка нормаль, умноженную на переменные vx и vy, мы получаем точку, смещённую в сторону от прямой линии. На каждой итерации знаки vx и vy инвертируются, что создаёт характерный волнообразный зигзаг. Эти смещённые точки и становятся контрольными точками сплайна.

let normal = Phaser.Geom.Line.GetNormal(ray);
let midPoint = Phaser.Geom.Line.GetMidPoint(ray);
points.push(new Phaser.Math.Vector2(midPoint.x + normal.x * vx, midPoint.y + normal.y * vy));
vx *= -1;
vy *= -1;

После цикла в массив добавляется конечная точка линии (Point B). Все собранные точки передаются конструктору Phaser.Curves.Spline, который строит по ним плавную кривую. Эту кривую можно отрисовать на graphics для наглядности.

Создание и настройка физического тела

Теперь создадим физический объект, который будет двигаться по построенной кривой. Используя менеджер физики Matter (this.matter), мы добавляем изображение в начальную точку линии.

Критически важный шаг — настройка физических свойств тела. Чтобы движение по кривой было чётким и не подвергалось влиянию симуляции, мы отключаем все мешающие силы: - setFriction(0) убирает трение о другие тела. - setFrictionAir(0) отключает сопротивление воздуха. - setBounce(0) убирает упругость, чтобы тело не отскакивало от границ мира (хотя в данном примере гравитация тоже отключена).

this.block = this.matter.add.image(line.x1, line.y1, 'block');
this.block.setFriction(0);
this.block.setFrictionAir(0);
this.block.setBounce(0);

Движение начинается по клику мыши, что устанавливает счётчик времени this.t в 0, активируя логику в методе update.

Анимация движения в методе Update

Логика плавного перемещения реализована в методе update. Пока this.t равен -1 (ожидание клика), метод просто завершается.

После старта this.t увеличивается на значение delta (время, прошедшее с прошлого кадра). Это обеспечивает плавную анимацию, не зависящую от частоты кадров.

Ключевая формула вычисляет прогресс движения `dкак отношение прошедшего времени к общей длительности. Значениеd` изменяется от 0 (начало пути) до 1 (конец пути).

Затем мы запрашиваем у объекта кривой this.curve точку, соответствующую этому прогрессу, с помощью метода getPoint(d). Полученные координаты p.x и p.y напрямую задаются как новая позиция физического тела через setPosition.

const d = (this.t / this.duration);
const p = this.curve.getPoint(d);
this.block.setPosition(p.x, p.y);

Когда время истекает (this.t >= this.duration), тело останавливается, и его скорость явно обнуляется. Прямое задание позиции в обход физического движка — это и есть основной "фокус", позволяющий совместить сложную траекторию и физическое тело.

Конфигурация игры и физики Matter

Для работы примера необходима правильная настройка игры. В конфигурационном объекте важно активировать физический плагин Matter.js и задать его параметры.

Параметр default: 'matter' указывает Phaser использовать Matter.js в качестве основной физической системы. Внутри блока matter можно включить отладку (debug: true), чтобы видеть хитбоксы тел, и, что самое важное, отключить гравитацию, установив её компоненты `xиy` в 0. Это необходимо, чтобы тело не начало падать вниз и двигалось исключительно по заданной нами траектории.

physics: {
    default: 'matter',
    matter: {
        debug: true,
        gravity: {
            x: 0,
            y: 0
        }
    }
}

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

Этот пример наглядно демонстрирует мощный гибридный подход: мы используем физический движок Matter.js для коллизий и взаимодействий, но при этом берём управление позицией тела в свои руки, чтобы реализовать сложную, заранее определённую траекторию. Это открывает двери для создания разнообразных игровых механик. **Идеи для экспериментов:** 1. Подставьте в код другую кривую, например, Phaser.Curves.Ellipse или Phaser.Curves.Path, чтобы создать круговые или многоугольные траектории. 2. Измените логику в update: попробуйте задавать не позицию, а скорость (setVelocity) в направлении следующей точки на кривой, чтобы получить более "физичное", но всё же управляемое движение. 3. Добавьте несколько тел, которые начинают движение по одной кривой с задержкой, создавая "змейку" из объектов. 4. Включите гравитацию и наблюдайте, как тело, движущееся по кривой, будет на неё реагировать при столкновении с другими физическими объектами.