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

Движение объектов по заданным траекториям — частая задача в играх: от патрулирования врагов до движения платформ. Встроенный в Phaser 3 Arcade Physics не имеет готового метода `followPath`, но его можно реализовать, комбинируя возможности модулей Curves и Physics. Эта статья покажет, как создать физическое тело, которое будет перемещаться по сложному пути, сохраняя все свойства физической модели, такие как коллизии и гравитация. Мы разберем пример, где один объект ведет другого, и вы сможете адаптировать этот подход для своих игровых механик.

Версия 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('block', 'assets/sprites/block.png');
        this.load.image('lemming', 'assets/sprites/lemming.png');
    }

    create ()
    {
        // const path = this.createLoopPath();
        const path = this.createZigZagPath();

        const graphics = this.add.graphics({
            fillStyle: { color: 0xffff00, alpha: 0.6 },
            lineStyle: { width: 2, color: 0x0000ff, alpha: 0.6 }
        });

        const start = path.getStartPoint();
        const distance = path.getLength();
        const duration = 20000;
        const speed = distance / duration;
        const speedSec = 1000 * speed;
        const tSpeed = 1 / duration;
        const tSpeedSec = 1000 * tSpeed;

        let t = 0;

        console.log('distance (px)', distance);
        console.log('time (ms)', duration);
        console.log('speed (px/ms)', speed);
        console.log('speed (px/s)', speedSec);
        console.log('speed (t/ms)', tSpeed);
        console.log('speed (t/s)', tSpeedSec);

        const block = this.physics.add.image(start.x, start.y, 'block')
            .setImmovable(true)
            .setAlpha(0.5);

        const lemming = this.physics.add.image(block.x, block.y - 128, 'lemming')
            .setGravityY(30000);

        this.physics.add.collider(block, lemming);

        this.physics.world.on('worldstep', (delta) =>
        {
            t += delta * tSpeedSec;

            if (t > 1)
            {
                t -= 1;
                block.body.reset(start.x, start.y);
                graphics.clear();
                path.draw(graphics);
            }

            path.getTangent(t, block.body.velocity);
            block.body.velocity.scale(speedSec);
            block.setRotation(block.body.velocity.angle());
            graphics.fillPointShape(block.body.center, 2);
        });
    }

    createLoopPath ()
    {
        const path = new Phaser.Curves.Path(50, 500);

        path.splineTo([ 164, 446, 274, 542, 412, 457, 522, 541, 664, 464 ]);
        path.lineTo(700, 300);
        path.lineTo(600, 350);
        path.ellipseTo(200, 100, 100, 250, false, 0);
        path.cubicBezierTo(222, 119, 308, 107, 208, 368);
        path.ellipseTo(60, 60, 0, 360, true);

        return path;
    }

    createZigZagPath ()
    {
        const path = new Phaser.Curves.Path(100, -50);

        path.lineTo(100, 50);

        const max = 8;
        const h = 500 / max;

        for (let i = 0; i < max; i++)
        {
            if (i % 2 === 0)
            {
                path.lineTo(700, 50 + h * (i + 1));
            }
            else
            {
                path.lineTo(100, 50 + h * (i + 1));
            }
        }

        path.lineTo(100, 650);

        return path;
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    parent: 'phaser-example',
    physics: {
        default: 'arcade',
        arcade: {
            debug: false,
            fps: 60,
            timeScale: 1
        }
    },
    scene: Example
};

const game = new Phaser.Game(config);

Подготовка сцены и создание пути

В методе preload загружаются два спрайта. В create создается объект пути (Path). В примере закомментирован вызов createLoopPath, который создает петлевидную трассу, но активен createZigZagPath, генерирующий зигзагообразный путь.

Класс Phaser.Curves.Path позволяет строить сложные траектории из отрезков (lineTo), сплайнов (splineTo), эллипсов (ellipseTo) и кривых Безье (cubicBezierTo). Путь задается в координатах игрового мира.

const path = this.createZigZagPath();

Для визуализации пути создается объект Graphics. Метод path.draw(graphics) отрисует линию пути на сцене.

const graphics = this.add.graphics({
    fillStyle: { color: 0xffff00, alpha: 0.6 },
    lineStyle: { width: 2, color: 0x0000ff, alpha: 0.6 }
});
path.draw(graphics);

Расчет скорости и параметра t

Ключевая концепция — параметризация пути. Параметр `tменяется от 0 (начало пути) до 1 (конец пути). Чтобы объект прошел весь путь за заданное время, нужно рассчитать, насколько быстро должен менятьсяt`.

Сначала получаем общую длину пути в пикселях и задаем желаемое время прохождения в миллисекундах.

const start = path.getStartPoint();
const distance = path.getLength();
const duration = 20000; // 20 секунд

Затем вычисляются скорости: - speed — скорость в пикселях за миллисекунду. - tSpeed — скорость изменения параметра `t` за миллисекунду. Умножая эти значения на 1000, получаем скорости в секунду.

const speed = distance / duration;
const tSpeed = 1 / duration;
const tSpeedSec = 1000 * tSpeed; // Скорость изменения t в секунду

Эти расчеты гарантируют, что объект пройдет весь путь ровно за duration миллисекунд, независимо от длины и формы траектории.

Создание и связывание физических тел

Создаются два физических спрайта с помощью this.physics.add.image. Блок (block) будет следовать по пути, а лемминг (lemming) — падать на него под действием гравитации.

Блок делается неподвижным (setImmovable(true)), чтобы при коллизиях его не сдвигали другие тела. Леммингу задается большая гравитация по оси Y.

const block = this.physics.add.image(start.x, start.y, 'block')
    .setImmovable(true)
    .setAlpha(0.5);

const lemming = this.physics.add.image(block.x, block.y - 128, 'lemming')
    .setGravityY(30000);

this.physics.add.collider(block, lemming);

Коллайдер обеспечивает физическое взаимодействие: лемминг будет приземляться на движущийся блок и ехать на нем, как на платформе.

Движение по пути в обработчике worldstep

Логика движения реализована внутри обработчика события worldstep. Это событие генерируется на каждом шаге физического движка (с частотой fps, указанной в конфиге). Параметр delta содержит время, прошедшее с предыдущего шага, в миллисекундах.

Внутри обработчика: 1. Увеличиваем параметр `tна величину, пропорциональную прошедшему времени (delta`). 2. Если `t` превысил 1 (конец пути), сбрасываем его и телепортируем блок в начало. 3. Метод path.getTangent(t, block.body.velocity) — это ключевой момент. Он вычисляет вектор касательной к пути в точке `tи записывает его вblock.body.velocity`. Этот вектор имеет единичную длину. 4. Масштабируем этот вектор на реальную скорость в пикселях в секунду (speedSec). 5. Поворачиваем блок (setRotation) в направлении вектора скорости.

this.physics.world.on('worldstep', (delta) =>
{
    t += delta * tSpeedSec;

    if (t > 1)
    {
        t -= 1;
        block.body.reset(start.x, start.y);
        graphics.clear();
        path.draw(graphics);
    }

    path.getTangent(t, block.body.velocity);
    block.body.velocity.scale(speedSec);
    block.setRotation(block.body.velocity.angle());
    graphics.fillPointShape(block.body.center, 2);
});

Последняя строка рисует маленькую точку в центре блока на каждом шаге, создавая след его движения.

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

Мы реализовали движение физического тела по произвольному пути, используя связку Curves.Path для задания траектории и события worldstep для обновления позиции. Этот подход сохраняет все преимущества Arcade Physics: коллизии, гравитацию и высокую производительность. Идеи для экспериментов: 1. Замените путь на createLoopPath и посмотрите, как объект движется по сложной кривой. 2. Сделайте скорость блока переменной, изменяя speedSec в зависимости от `t` или по какому-либо событию. 3. Добавьте несколько независимых блоков, движущихся по одному или разным путям, и поместите на них динамических персонажей. 4. Используйте метод path.getPoint(t) для телепортации спрайта вместо плавного движения — это может пригодиться для мгновенных перемещений по рельсам.