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

При разработке игр часто возникает задача создать группу объектов, которые движутся не хаотично, а слаженно, как стая птиц или косяк рыб. Phaser предоставляет мощный инструментарий для физики, но для плавного, органичного движения иногда требуется дополнительная обработка. В этой статье мы разберем, как использовать линейную интерполяцию скорости в Arcade Physics для создания эффекта "следования" объектов друг за другом, что идеально подходит для симуляции стайного поведения.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    fishes;

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('sea', 'assets/pics/undersea.jpg');
        this.load.spritesheet('fish', 'assets/sprites/fish-136x80.png', { frameWidth: 136, frameHeight: 80 });
    }

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

        this.fishes = this.physics.add.group({
            key: 'fish',
            frame: [ 0, 1, 2 ],
            repeat: 1,
            setXY: { x: 400, y: 300 },
            bounceX: 1,
            bounceY: 1,
            collideWorldBounds: true
        });

        this.fishes.getChildren()[0].setVelocity(100, -200);
    }

    update ()
    {
        let prev;

        for (const fish of this.fishes.getChildren())
        {
            fish.rotation = fish.body.angle;

            if (prev)
            {
                fish.body.velocity.lerp(prev.body.velocity, 0.05);
            }

            prev = fish;
        }
    }
}

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

const game = new Phaser.Game(config);

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

Класс Example расширяет Phaser.Scene. В методе preload загружаются фон и спрайтшит с тремя кадрами рыбы.

В методе create мы добавляем фон и создаем физическую группу объектов. Группа — это удобный способ управлять множеством однотипных объектами. Ключевые параметры конфигурации:

this.fishes = this.physics.add.group({
    key: 'fish',
    frame: [ 0, 1, 2 ], // Используем три разных кадра из спрайтшита
    repeat: 1,          // Создаем по одному экземпляру на каждый кадр
    setXY: { x: 400, y: 300 }, // Начальная позиция для всех
    bounceX: 1,
    bounceY: 1,         // Объекты отскакивают от границ мира
    collideWorldBounds: true
});

После создания группы первому объекту в ней вручную задается начальная скорость с помощью setVelocity. Остальные объекты пока неподвижны.

Магия интерполяции в методе Update

Вся логика связывания движения происходит в методе update, который вызывается на каждом кадре. Здесь мы перебираем всех рыб в группе.

for (const fish of this.fishes.getChildren())
{
    fish.rotation = fish.body.angle;

    if (prev)
    {
        fish.body.velocity.lerp(prev.body.velocity, 0.05);
    }

    prev = fish;
}

1. fish.rotation = fish.body.angle; — синхронизирует графический поворот спрайта с углом, рассчитанным физическим движком (это важно, так как тело может вращаться при столкновениях). 2. Переменная prev хранит предыдущую рыбу в цепочке. 3. Для каждой рыбы, кроме первой, мы вызываем метод lerp (Linear intERPolation) на векторе ее скорости fish.body.velocity.

Как работает метод lerp

Метод lerp(target, amount) — это ключевой элемент примера. Он постепенно сближает текущий вектор (скорость рыбы) с целевым вектором (скоростью предыдущей рыбы).

- **Первый аргумент (target)**: целевой вектор скорости, к которому мы стремимся. В нашем случае — это скорость предыдущей рыбы в списке. - **Второй аргумент (amount)**: коэффициент интерполяции (в примере 0.05). Значение 0.0 оставит скорость неизменной, 1.0 мгновенно заменит ее на целевую. Значение 0.05 означает, что на каждом кадре скорость текущей рыбы будет на 5% ближе к скорости предыдущей.

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

Настройка физического мира

Для работы примера необходима правильная конфигурация игры. В объекте config активируется Arcade Physics.

physics: {
    default: 'arcade',
    arcade: {
        debug: false // Включите true для отладки границ тел
    }
},

Именно движок arcade предоставляет объектам свойство body с векторами velocity и методом lerp. Без активированной физики этот код не будет работать.

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

Использование velocity.lerp() — это простой, но мощный прием для создания сложного на вид группового поведения. Он позволяет избежать жесткой привязки позиций, обеспечивая более естественное, физически правдоподобное движение. **Идеи для экспериментов:** 1. Измените коэффициент 0.05 на большее значение (например, 0.2), чтобы увидеть, как рыбы становятся более «резвыми» и быстрее синхронизируются. 2. Попробуйте применить интерполяцию не к скорости, а к позиции (fish.x) для другого визуального эффекта. 3. Сделайте первую рыбу управляемой с клавиатуры или курсором мыши, чтобы вести за собой всю стаю.