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

В мобильных играх, платформерах и аркадах игровой мир часто превосходит размеры экрана. Чтобы игрок не потерял своего персонажа, камера должна плавно следовать за ним. В Phaser это реализуется парой методов. Разберем пример, где камера следит за кораблем на огромной карте, и научимся настраивать ее поведение, включая плавность слежения и увеличение.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    constructor ()
    {
        super();
    }

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('map', 'assets/tests/camera/earthbound-scarab.png');
        this.load.image('ship', 'assets/sprites/fmship.png');
    }

    create ()
    {
        this.cameras.main.setBounds(0, 0, 1024, 2048);

        this.add.image(0, 0, 'map').setOrigin(0);

        this.cursors = this.input.keyboard.createCursorKeys();

        this.mode = 0; // 0 = direct, 1 = physics
        this.directSpeed = 4.5;

        this.ship = this.physics.add.image(400, 300, 'ship');

        this.cameras.main.startFollow(this.ship, true);
        // this.cameras.main.startFollow(this.ship, true, 0.09, 0.09);

        // this.cameras.main.setZoom(2);
        this.cameras.main.setZoom(4);

        this.input.on('pointerdown', () => {

            console.log(this.cameras.main.scrollX, this.cameras.main.scrollY);
            console.log(this.cameras.main.matrix);

        });

        this.events.on('prerender', this.preRender, this);
    }

    preRender ()
    {
        // console.log(this.ship.x, this.ship.y, this.cameras.main.scrollX, this.cameras.main.scrollY);
    }

    update ()
    {
        if (this.mode === 0)
        {
            this.updateDirect();
        }
        else
        {
            this.updatePhysics();
        }
    }

    updatePhysics ()
    {
        this.ship.setVelocity(0);

        if (this.cursors.left.isDown)
        {
            this.ship.setAngle(-90).setVelocityX(-200);
        }
        else if (this.cursors.right.isDown)
        {
            this.ship.setAngle(90).setVelocityX(200);
        }

        if (this.cursors.up.isDown)
        {
            this.ship.setAngle(0).setVelocityY(-200);
        }
        else if (this.cursors.down.isDown)
        {
            this.ship.setAngle(-180).setVelocityY(200);
        }
    }

    updateDirect ()
    {
        if (this.cursors.left.isDown)
        {
            this.ship.setAngle(-90);
            this.ship.x -= this.directSpeed;
        }
        else if (this.cursors.right.isDown)
        {
            this.ship.setAngle(90);
            this.ship.x += this.directSpeed;
        }

        if (this.cursors.up.isDown)
        {
            this.ship.setAngle(0);
            this.ship.y -= this.directSpeed;
        }
        else if (this.cursors.down.isDown)
        {
            this.ship.setAngle(-180);
            this.ship.y += this.directSpeed;
        }
    }
}

const config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    pixelArt: true,
    physics: {
        default: 'arcade',
    },
    scene: Example
};
const game = new Phaser.Game(config);

Подготовка мира и камеры

Первым делом, мы создаем игровой мир, который больше области видимости (viewport) камеры. Для этого в методе create() устанавливаем границы для основной камеры (this.cameras.main).

Затем мы добавляем фоновое изображение карты и спрайт корабля, за которым будет следить камера. Корабль создается как физический объект, чтобы позже продемонстрировать два типа управления.

this.cameras.main.setBounds(0, 0, 1024, 2048);
this.add.image(0, 0, 'map').setOrigin(0);
this.ship = this.physics.add.image(400, 300, 'ship');

Включаем слежку камеры за объектом

Сердце примера — метод startFollow(). Он заставляет камеру центрироваться на целевом объекте. Второй аргумент (true) включает линейную интерполяцию (LERP), что делает движение камеры более плавным, а не резким.

За комментированной строкой скрывается расширенная настройка: третий и четвертый аргументы позволяют задать коэффициент сглаживания по осям X и Y. Меньшие значения (например, 0.09) делают слежку более плавной, но с задержкой.

this.cameras.main.startFollow(this.ship, true);
// this.cameras.main.startFollow(this.ship, true, 0.09, 0.09);

Управление зумом камеры

Метод setZoom() изменяет масштаб всего, что отображается камерой. Значение 1 — это 100%, 2 — увеличение в два раза, 0.5 — уменьшение в два раза.

В примере стоит зум 4, что сильно приближает корабль и окружающую его местность. Это полезно для акцента на деталях или создания эффекта боевого режима. Раскомментировав строку с setZoom(2), вы увидите менее агрессивное увеличение.

// this.cameras.main.setZoom(2);
this.cameras.main.setZoom(4);

Два типа управления объектом

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

updateDirect() напрямую меняет свойства `xиyспрайта.updatePhysics()использует методыsetVelocity()` для задания скорости, с которой движок сам изменит положение объекта.

updateDirect() {
    if (this.cursors.left.isDown) {
        this.ship.setAngle(-90);
        this.ship.x -= this.directSpeed;
    }
}

updatePhysics() {
    this.ship.setVelocity(0);
    if (this.cursors.left.isDown) {
        this.ship.setAngle(-90).setVelocityX(-200);
    }
}

Отладка: смотрим внутреннее состояние камеры

Для понимания того, как работает камера, полезно заглянуть в ее внутренние свойства. В примере по клику мыши (pointerdown) в консоль выводятся текущие координаты скролла камеры и ее матрица трансформации.

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

this.input.on('pointerdown', () => {
    console.log(this.cameras.main.scrollX, this.cameras.main.scrollY);
    console.log(this.cameras.main.matrix);
});

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

Методы setBounds(), startFollow() и setZoom() дают вам полный контроль над поведением камеры в Phaser, позволяя легко создавать скроллящиеся уровни. Для экспериментов попробуйте: изменить коэффициенты плавности слежки, динамически менять зум в зависимости от скорости корабля, или заставить камеру следовать не за центром, а за смещенной точкой спрайта, используя setFollowOffset().