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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    text;
    cursors;
    player;

    preload ()
    {
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
        this.load.image('bg', 'assets/pics/the-end-by-iloe-and-made.jpg');
        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(0, 0, 1920 * 2, 1080 * 2);
        this.physics.world.setBounds(0, 0, 1920 * 2, 1080 * 2);

        //  Mash 4 images together to create our background
        this.add.image(0, 0, 'bg').setOrigin(0);
        this.add.image(1920, 0, 'bg').setOrigin(0).setFlipX(true);
        this.add.image(0, 1080, 'bg').setOrigin(0).setFlipY(true);
        this.add.image(1920, 1080, 'bg').setOrigin(0).setFlipX(true).setFlipY(true);

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

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

        this.player.setCollideWorldBounds(true);

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

        this.text = this.add.text(10, 10, 'Cursors to move', { font: '16px Courier', fill: '#00ff00' }).setScrollFactor(0);
    }

    update ()
    {
        this.player.setVelocity(0);

        if (this.cursors.left.isDown)
        {
            this.player.setVelocityX(-500);
        }
        else if (this.cursors.right.isDown)
        {
            this.player.setVelocityX(500);
        }

        if (this.cursors.up.isDown)
        {
            this.player.setVelocityY(-500);
        }
        else if (this.cursors.down.isDown)
        {
            this.player.setVelocityY(500);
        }

        this.text.setText([
            `screen x: ${this.input.x}`,
            `screen y: ${this.input.y}`,
            `world x: ${this.input.mousePointer.worldX}`,
            `world y: ${this.input.mousePointer.worldY}`
        ]);
    }
}

const config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    physics: {
        default: 'arcade'
    },
    scene: Example
};

const game = new Phaser.Game(config);

Создание большого игрового мира

Первый шаг — определение границ, в которых может существовать игрок и камера. В примере мир в четыре раза больше исходного фонового изображения. Устанавливаются границы камеры (cameras.main.setBounds) и границы физического мира (physics.world.setBounds). Это разные сущности: границы камеры ограничивают область, которую она может показывать, а границы физического мира определяют, с чем будет сталкиваться физическое тело.

this.cameras.main.setBounds(0, 0, 1920 * 2, 1080 * 2);
this.physics.world.setBounds(0, 0, 1920 * 2, 1080 * 2);

Далее создаётся фон, составленный из четырёх изображений. Каждое изображение смещается и зеркально отражается (методы .setFlipX(true) и .setFlipY(true)), чтобы заполнить весь заданный мир без швов.

this.add.image(0, 0, 'bg').setOrigin(0);
this.add.image(1920, 0, 'bg').setOrigin(0).setFlipX(true);
this.add.image(0, 1080, 'bg').setOrigin(0).setFlipY(true);
this.add.image(1920, 1080, 'bg').setOrigin(0).setFlipX(true).setFlipY(true);

Игрок, управление и привязка камеры

Создаётся физический спрайт игрока и настраивается управление с клавиатуры. Важный момент — игроку задаётся столкновение с границами мира (setCollideWorldBounds(true)), чтобы он не мог выйти за пределы уровня.

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

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

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

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

Разница между координатами экрана и мира

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

this.text.setText([
    `screen x: ${this.input.x}`,
    `screen y: ${this.input.y}`,
    `world x: ${this.input.mousePointer.worldX}`,
    `world y: ${this.input.mousePointer.worldY}`
]);
*   **Координаты экрана (`this.input.x`, `this.input.y`)**: Это позиция курсора мыши (или касания) относительно окна игры. Если вы переместите курсор в левый верхний угол окна, значения будут близки к (0,0), независимо от того, куда сейчас направлена камера в игровом мире.
*   **Мировые координаты (`this.input.mousePointer.worldX`, `this.input.mousePointer.worldY`)**: Это позиция курсора, пересчитанная в координаты игрового мира. Когда камера смещается, эти координаты меняются, указывая, на какой именно объект мира вы сейчас навели курсор. Именно эти координаты используются для взаимодействия с игровыми объектами (например, для выстрела или выбора цели).

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

Для работы примера необходима базовая конфигурация игры. Ключевой момент — активация физического движка Arcade, который позволяет использовать методы physics.add.image и setVelocity.

const config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    physics: {
        default: 'arcade' // Включаем физический движок Arcade
    },
    scene: Example
};

const game = new Phaser.Game(config);

Без указания physics: { default: 'arcade' } вызов this.physics.add.image вызовет ошибку, а методы setVelocity и setCollideWorldBounds не будут работать.

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

Понимание разницы между экранными и мировыми координатами, а также умение управлять камерой — критически важные навыки для разработки игр в Phaser. Камера, следующая за игроком, и статический интерфейс — это базовые паттерны. Для экспериментов попробуйте: изменить параметры интерполяции в startFollow для более резкого или плавного слежения; добавить зум камеры с помощью setZoom; создать вторую камеру для мини-карты или другого ракурса с помощью cameras.add. Это откроет путь к созданию сложных и визуально интересных игровых пространств.