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

В 2D-играх игровой мир часто превышает размер экрана. Классический пример — платформер с большой картой. Phaser предоставляет мощные инструменты для управления границами мира и камеры, позволяя игроку исследовать обширные пространства, а разработчику — легко настраивать это поведение. В этой статье на примере разберем, как задавать границы для камеры и физического мира, а также заставлять камеру следовать за персонажем, не выходя за пределы уровня.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    constructor ()
    {
        super();
        this.moveCam = false;
    }

    preload () 
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('bg', 'assets/pics/uv-grid-diag.png');
        this.load.image('block', 'assets/sprites/block.png');
    }

    create () 
    {
        //  Set the camera and physics bounds to be the size of 4x4 bg images
        this.cameras.main.setBounds(-1024, -1024, 1024 * 2, 1024 * 2);
        this.physics.world.setBounds(-1024, -1024, 1024 * 2, 1024 * 2);

        this.add.image(-1024, -1024, 'bg').setOrigin(0);
        this.add.image(0, -1024, 'bg').setOrigin(0);
        this.add.image(-1024, 0, 'bg').setOrigin(0);
        this.add.image(0, 0, 'bg').setOrigin(0);

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

        this.player = this.physics.add.image(0, 0, 'block');

        this.player.setCollideWorldBounds(true);

        this.cameras.main.startFollow(this.player, true);

        // this.cameras.main.setDeadzone(400, 200);
        // this.cameras.main.setZoom(0.5);

        if (this.cameras.main.deadzone)
        {
           const graphics = this.add.graphics().setScrollFactor(0);
            graphics.lineStyle(2, 0x00ff00, 1);
            graphics.strokeRect(200, 200, this.cameras.main.deadzone.width, this.cameras.main.deadzone.height);
        }

        this.text = this.add.text(32, 32).setScrollFactor(0).setFontSize(32).setColor('#ffffff');
    }

    update () 
    {
        var cam = this.cameras.main;

        if (cam.deadzone)
        {
            this.text.setText([
                'ScrollX: ' + cam.scrollX,
                'ScrollY: ' + cam.scrollY,
                'MidX: ' + cam.midPoint.x,
                'MidY: ' + cam.midPoint.y,
                'deadzone left: ' + cam.deadzone.left,
                'deadzone right: ' + cam.deadzone.right,
                'deadzone top: ' + cam.deadzone.top,
                'deadzone bottom: ' + cam.deadzone.bottom
            ]);
        }
        else
        {
            this.text.setText([
                'ScrollX: ' + cam.scrollX,
                'ScrollY: ' + cam.scrollY,
                'MidX: ' + cam.midPoint.x,
                'MidY: ' + cam.midPoint.y
            ]);
        }
        this.player.setVelocity(0);
    
        if (this.cursors.left.isDown)
        {
            this.player.setVelocityX(-300);
        }
        else if (this.cursors.right.isDown)
        {
            this.player.setVelocityX(300);
        }
    
        if (this.cursors.up.isDown)
        {
            this.player.setVelocityY(-300);
        }
        else if (this.cursors.down.isDown)
        {
            this.player.setVelocityY(300);
        }
    }
}

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


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

В методе preload() загружаются два изображения: фон (bg) и спрайт игрока (block). Базовый URL задается для удобства.

preload () 
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.image('bg', 'assets/pics/uv-grid-diag.png');
    this.load.image('block', 'assets/sprites/block.png');
}

В методе create() происходит основная настройка. Сначала определяются границы, в которых может перемещаться камера и физические тела. Это ключевой момент для создания большого мира.

//  Set the camera and physics bounds to be the size of 4x4 bg images
this.cameras.main.setBounds(-1024, -1024, 1024 * 2, 1024 * 2);
this.physics.world.setBounds(-1024, -1024, 1024 * 2, 1024 * 2);

Метод setBounds() принимает четыре аргумента: начальные координаты X, Y, ширину и высоту области. Здесь границы установлены от точки (-1024, -1024) до (1024, 1024), создавая мир размером 2048x2048 пикселей с центром в (0,0).

Создание мира и настройка камеры

После установки границ создается фон из четырех тайлов, что визуально заполняет весь заданный мир. Спрайт игрока создается как физическое тело с помощью this.physics.add.image.

this.add.image(-1024, -1024, 'bg').setOrigin(0);
this.add.image(0, -1024, 'bg').setOrigin(0);
this.add.image(-1024, 0, 'bg').setOrigin(0);
this.add.image(0, 0, 'bg').setOrigin(0);

this.player = this.physics.add.image(0, 0, 'block');
this.player.setCollideWorldBounds(true);

Вызов setCollideWorldBounds(true) заставляет физическое тело игрока сталкиваться с границами физического мира, которые мы задали ранее. Это предотвращает его выход за пределы уровня.

Далее камера начинает следовать за игроком.

this.cameras.main.startFollow(this.player, true);

Второй аргумент true включает линейную интерполяцию (Lerp), что делает движение камеры более плавным. Важно: так как границы камеры уже заданы, она не будет показывать область за их пределами, даже если игрок упрется в край мира.

Работа с мёртвой зоной (Deadzone)

Код содержит закомментированные настройки, которые можно активировать для экспериментов.

// this.cameras.main.setDeadzone(400, 200);
// this.cameras.main.setZoom(0.5);

Мёртвая зона (deadzone) — это область в центре экрана, внутри которой движение игрока не приводит к смещению камеры. Это полезно для платформеров, чтобы камера не дергалась при каждом небольшом движении. Метод setDeadzone(width, height) задает её размер.

В примере есть блок кода, который, если мёртвая зона задана, рисует её зеленую рамку на экране для наглядности.

if (this.cameras.main.deadzone)
{
   const graphics = this.add.graphics().setScrollFactor(0);
    graphics.lineStyle(2, 0x00ff00, 1);
    graphics.strokeRect(200, 200, this.cameras.main.deadzone.width, this.cameras.main.deadzone.height);
}

setScrollFactor(0) заставляет графический объект оставаться на месте относительно экрана, а не двигаться вместе с камерой.

Обработка ввода и отладка

В методе update() каждому кадру обрабатывается ввод с клавиш-стрелок и обновляется положение игрока через setVelocity(). Это стандартный для Arcade Physics способ задания скорости.

this.player.setVelocity(0);
if (this.cursors.left.isDown) { this.player.setVelocityX(-300); }
// ... и так далее для других направлений

Также в update() выводится отладочная информация о положении камеры. В зависимости от наличия мёртвой зоны отображаются разные наборы свойств.

if (cam.deadzone)
{
    this.text.setText([
        'ScrollX: ' + cam.scrollX, // Смещение камеры по X
        'ScrollY: ' + cam.scrollY, // Смещение камеры по Y
        'MidX: ' + cam.midPoint.x, // Центр камеры в мировых координатах
        'MidY: ' + cam.midPoint.y,
        'deadzone left: ' + cam.deadzone.left, // Границы мёртвой зоны
        // ...
    ]);
}

Свойства scrollX и scrollY — это ключевые координаты, определяющие, какую часть мира сейчас показывает камера. Их отслеживание крайне полезно при отладке скроллинга.

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

Установка границ через setBounds() для камеры и физического мира — фундаментальный прием для создания игр с пространством больше экрана. Камера, следующая за игроком (startFollow()), в связке с этими границами создает классическое и предсказуемое поведение. Для дальнейших экспериментов попробуйте

  1. Раскомментировать setDeadzone() и setZoom() и понаблюдать за изменениями
  2. Изменить координаты и размер границ, сместив мир, например, в положительные координаты
  3. Реализовать переход между комнатами, перенастраивая границы камеры при достижении игроком определённой зоны