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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    followers;
    graphics;
    path;

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

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

        //  Create the zig-zag path

        this.path = new Phaser.Curves.Path(100, -50);

        this.path.lineTo(100, 50);

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

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

        this.path.lineTo(100, 650);

        //  Create the path followers

        this.followers = this.add.group();

        for (let i = 0; i < 32; i++)
        {
            const ball = this.followers.create(0, -50, 'ball');

            ball.setData('vector', new Phaser.Math.Vector2());

            this.tweens.add({
                targets: ball,
                z: 1,
                ease: 'Linear',
                duration: 12000,
                repeat: -1,
                delay: i * 100
            });
        }
    }

    update ()
    {
        this.graphics.clear();

        this.graphics.lineStyle(1, 0xffffff, 1);

        this.path.draw(this.graphics);

        const balls = this.followers.getChildren();

        for (let i = 0; i < balls.length; i++)
        {
            const t = balls[i].z;
            const vec = balls[i].getData('vector');

            //  The vector is updated in-place
            this.path.getPoint(t, vec);

            balls[i].setPosition(vec.x, vec.y);

            balls[i].setDepth(balls[i].y);
        }
    }
}

const config = {
    type: Phaser.AUTO,
    width: 800,
    height: 600,
    backgroundColor: '#2d2d2d',
    parent: 'phaser-example',
    scene: Example
};

const game = new Phaser.Game(config);

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

Основу анимации составляет объект Phaser.Curves.Path. Он позволяет последовательно строить путь из сегментов — линий, сплайнов или дуг.

В методе create() мы создаем путь, начиная с точки (100, -50). Затем добавляем первый сегмент до точки (100, 50). Далее в цикле формируется зигзаг: если индекс четный, линия проводится вправо к X=700, если нечетный — обратно влево к X=100. Координата Y с каждой итерацией увеличивается, создавая ступенчатую структуру.

Для отладки и визуализации путь рисуется с помощью объекта Graphics. В update() мы каждый кадр очищаем старую отрисовку и заново рисуем линию пути белым цветом.

this.path = new Phaser.Curves.Path(100, -50);
this.path.lineTo(100, 50);

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

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

this.path.lineTo(100, 650);
// В методе update():
this.graphics.clear();
this.graphics.lineStyle(1, 0xffffff, 1);
this.path.draw(this.graphics);

Подготовка последователей (followers)

Для движения по пути создается группа спрайтов (Group). Каждому спрайту-«шарику» в пользовательские данные (setData) записывается вектор — экземпляр Phaser.Math.Vector2. Этот вектор будет использоваться как буфер для получения координат точки на кривой, что эффективнее, чем создание нового объекта каждый кадр.

Ключевой момент — анимация свойства `zс помощьюtweens. Хотяzобычно ассоциируется с глубиной, здесь оно творчески переиспользуется как прогресс движения вдоль пути (значениеtот 0 до 1). Каждый следующий шар получает задержку (delay`), что создает волнообразный эффект.

this.followers = this.add.group();

for (let i = 0; i < 32; i++)
{
    const ball = this.followers.create(0, -50, 'ball');
    ball.setData('vector', new Phaser.Math.Vector2());

    this.tweens.add({
        targets: ball,
        z: 1,
        ease: 'Linear',
        duration: 12000,
        repeat: -1,
        delay: i * 100
    });
}

Обновление позиций и управление глубиной

В каждом кадре игры, в методе update(), для каждого шара из группы происходит обновление его позиции на экране.

Сначала из свойства `zшара, которое анимируется твином, получаем значение прогрессаt. Затем, используя метод путиgetPoint(t, vector)`, мы вычисляем координаты точки на пути для данного прогресса. Второй аргумент — вектор — передается для обновления «на месте» (in-place), что избегает создания мусора.

Полученные координаты (vec.x, vec.y) задаются как позиция шара через setPosition. Дополнительно, глубина отрисовки (depth) шара привязывается к его координате Y, чтобы шары, находящиеся ниже по экрану, перекрывали те, что выше, создавая простую параллакс-перспективу.

const balls = this.followers.getChildren();

for (let i = 0; i < balls.length; i++)
{
    const t = balls[i].z;
    const vec = balls[i].getData('vector');

    //  Вектор обновляется in-place
    this.path.getPoint(t, vec);

    balls[i].setPosition(vec.x, vec.y);
    balls[i].setDepth(balls[i].y);
}

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

Комбинируя Curves.Path, Tweens и Groups, можно создавать сложные и производительные паттерны движения. Для экспериментов попробуйте: заменить lineTo на splineTo для плавных изгибов, изменить расчет глубины для другого 3D-эффекта или запустить твины с разной длительностью для разнообразия движения.