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

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

Версия 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('bg', 'assets/skies/gradient8.png');
        this.load.image('master', 'assets/sprites/master.png');
        this.load.image('bubble', 'assets/particles/bubble.png');
    }

    create ()
    {
        this.add.image(400, 300, 'bg');

        const graphics = this.add.graphics();

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

        graphics.lineStyle(2, 0xffffff, 0.5);

        path.draw(graphics);

        const position = path.getPoint(0);

        const master = this.physics.add.image(position.x, position.y, 'master');

        master.setDirectControl();
        master.setImmovable();

        this.counter = this.tweens.addCounter({
            from: 0,
            to: 1,
            ease: 'linear',
            duration: 8000,
            repeat: -1,
            yoyo: true,
            onUpdate: tween =>
            {
                const position = path.getPoint(tween.getValue());

                master.setPosition(position.x, position.y);
            }
        });

        const bubbles = [];

        for (let i = 0; i < 64; i++)
        {
            const x = Phaser.Math.Between(0, 800);
            const y = Phaser.Math.Between(-1500, 0);

            const bubble = this.physics.add.image(x, y, 'bubble');

            bubble.setScale(0.5);
            bubble.setBounce(1);
            bubble.setDrag(5);
            bubble.setVelocityX(Phaser.Math.Between(-80, 80));
            bubble.setVelocityY(Phaser.Math.Between(10, 50));
            bubble.setMaxVelocity(700, 700);
            bubble.setCollideWorldBounds(true);

            bubbles.push(bubble);
        }

        this.physics.world.setBounds(0, -1500, 800, 2100);

        this.physics.add.collider(master, bubbles);
    }
}

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

const game = new Phaser.Game(config);

Создание и визуализация сложного пути

Класс Phaser.Curves.Path позволяет построить последовательный путь из различных сегментов. Начальная точка задаётся в конструкторе, а затем к пути добавляются элементы.

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

Далее мы используем методы пути для создания сложной траектории. Метод .splineTo() создаёт плавную кривую через массив контрольных точек. .lineTo() ведёт прямую линию к указанной точке. .ellipseTo() и .cubicBezierTo() добавляют эллиптические и кубические Безье-сегменты соответственно. Флаг в .ellipseTo() определяет, рисуется ли эллипс по или против часовой стрелки.

path.splineTo([ 164, 446, 274, 542, 412, 457, 522, 541, 664, 464 ]);
path.lineTo(700, 300);
path.ellipseTo(200, 100, 100, 250, false, 0);

Чтобы увидеть путь на экране, мы рисуем его с помощью объекта Graphics. Устанавливаем стиль линии и вызываем метод .draw().

const graphics = this.add.graphics();
graphics.lineStyle(2, 0xffffff, 0.5);
path.draw(graphics);

Плавное движение спрайта по пути с помощью Tween

Для движения объекта по пути используется твин-счётчик (Tween). Его задача — генерировать плавно меняющееся значение от 0 до 1, которое соответствует прогрессу движения от начала до конца пути.

this.counter = this.tweens.addCounter({
    from: 0,
    to: 1,
    ease: 'linear',
    duration: 8000,
    repeat: -1,
    yoyo: true
});

Ключевой параметр — onUpdate. Это callback-функция, которая вызывается на каждом кадре обновления твина. Внутри неё мы получаем текущее значение прогресса через tween.getValue(), передаём его в метод пути path.getPoint(), который возвращает конкретные координаты (x, y) на кривой для этого прогресса. Затем эти координаты применяются к спрайту.

onUpdate: tween =>
{
    const position = path.getPoint(tween.getValue());
    master.setPosition(position.x, position.y);
}

Спрайт master создаётся как физический объект (this.physics.add.image) в начальной точке пути. Методы setDirectControl() и setImmovable() отключают для него влияние физического движка (гравитацию, столкновения как динамического тела), позволяя управлять позицией вручную через setPosition.

const master = this.physics.add.image(position.x, position.y, 'master');
master.setDirectControl();
master.setImmovable();

Создание динамического фона с физическими частицами

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

const bubble = this.physics.add.image(x, y, 'bubble');

Каждому пузырю настраиваются физические свойства: - setScale(0.5) — уменьшает размер. - setBounce(1) — делает отскок абсолютно упругим. - setDrag(5) — добавляет сопротивление движению, делая его более "тяжёлым". - setVelocityX/Y() — задаёт начальную случайную скорость. - setMaxVelocity(700, 700) — ограничивает максимальную скорость. - setCollideWorldBounds(true) — включает столкновение с границами мира.

bubble.setVelocityX(Phaser.Math.Between(-80, 80));
bubble.setVelocityY(Phaser.Math.Between(10, 50));

Границы физического мира расширяются далеко за пределы экрана, чтобы пузыри могли свободно двигаться и падать сверху. Это создаёт иллюзию бесконечного пространства.

this.physics.world.setBounds(0, -1500, 800, 2100);

Наконец, коллайдер this.physics.add.collider(master, bubbles) регистрирует столкновения между главным спрайтом и всеми пузырями. Несмотря на то что master является Immovable, столкновения всё равно будут обрабатываться, и пузыри будут от него отталкиваться.

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

Базовая конфигурация игры включает настройку физического движка Arcade. Здесь важно отметить параметр gravity: { y: 50 }, который заставляет пузыри постепенно падать вниз, создавая естественное движение.

physics: {
    default: 'arcade',
    arcade: {
        debug: false,
        gravity: { y: 50 },
    }
}

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

Комбинирование предопределённых путей движения и динамической физики Arcade — мощный приём для создания живых, интерактивных сцен. Главный спрайт, двигаясь по сложной траектории, становится частью физического мира и взаимодействует с другими объектами. **Идеи для экспериментов:** 1. Измените параметры твина: попробуйте другие функции ease (например, 'sine.inout') для нелинейного движения по пути. 2. Сделайте движение пузырей более сложным: добавьте случайные импульсы через setAcceleration или заставьте их реагировать на курсор мыши. 3. Используйте path.getTangent() в onUpdate для автоматического вращения спрайта master вдоль касательной к пути, создав эффект управления кораблём. 4. Создайте несколько разных путей и несколько объектов, движущихся по ним с разной скоростью и фазами.