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

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

Версия 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('grid', 'assets/pics/uv-grid-4096-ian-maclachlan.png');
    }

    create ()
    {
        this.add.image(0, 0, 'grid')
            .setOrigin(0);

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

        const controlConfig = {
            camera: this.cameras.main,
            left: cursors.left,
            right: cursors.right,
            up: cursors.up,
            down: cursors.down,
            acceleration: 0.02,
            drag: 0.0005,
            maxSpeed: 1.0
        };

        this.controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig);
        const cam = this.cameras.main;

        cam.setBounds(0, 0, 4096, 4096).setZoom(1);

        const gui = new dat.GUI();
        gui.addFolder('Camera');
        gui.add(cam.midPoint, 'x').listen();
        gui.add(cam.midPoint, 'y').listen();
        gui.add(cam, 'scrollX').listen();
        gui.add(cam, 'scrollY').listen();
        gui.add(cam, 'width').listen();
        gui.add(cam, 'height').listen();
        gui.add(cam, 'displayWidth').listen();
        gui.add(cam, 'displayHeight').listen();
        gui.add(cam, 'zoom', 0.1, 4).step(0.1);
        gui.add(cam.worldView, 'left').listen();
        gui.add(cam.worldView, 'top').listen();
        gui.add(cam.worldView, 'right').listen();
        gui.add(cam.worldView, 'bottom').listen();
    }

    update (time, delta)
    {
        this.controls.update(delta);
    }
}

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

const game = new Phaser.Game(config);

Создание мира и плавного управления камерой

В примере создается большой мир размером 4096x4096 пикселей, представленный текстурой-сеткой. Ключевой элемент — это плавное управление камерой с помощью клавиш-стрелок.

Сначала мы загружаем изображение и размещаем его в начале координат мира (0,0). Затем создаем объект управления SmoothedKeyControl. Эта система обеспечивает инерционное движение камеры с ускорением и замедлением, что чувствуется гораздо естественнее, чем мгновенное перемещение.

const controlConfig = {
    camera: this.cameras.main,
    left: cursors.left,
    right: cursors.right,
    up: cursors.up,
    down: cursors.down,
    acceleration: 0.02,
    drag: 0.0005,
    maxSpeed: 1.0
};

this.controls = new Phaser.Cameras.Controls.SmoothedKeyControl(controlConfig);

Конфигурация связывает камеру с клавишами, задает ускорение (acceleration), сопротивление (drag) для плавной остановки и максимальную скорость. Объект this.controls затем обновляется в каждом кадре в методе update.

Настройка камеры: границы и зум

Чтобы камера не вылетала за пределы нашего мира, необходимо задать ее границы. Это делается с помощью метода setBounds. Также мы сразу устанавливаем базовый уровень масштабирования (zoom) равным 1.

const cam = this.cameras.main;
cam.setBounds(0, 0, 4096, 4096).setZoom(1);

Метод setBounds принимает начальные координаты X, Y и размеры области, за которую камера не может выйти. Это критически важно для игр с фиксированной картой, чтобы игрок не уводил взгляд в пустоту за ее границами.

Свойства `scrollX/Y` и `midPoint`: где находится камера?

Phaser предоставляет несколько свойств для отслеживания позиции камеры. Наиболее интуитивно понятные — это scrollX и scrollY. Они представляют собой координаты верхнего левого угла видимой области камеры в мировом пространстве.

Другой полезной точкой отсчета является midPoint — объект с полями `xиy. Это координаты центра камеры в мире. Когда вы двигаете камеру, именноscrollX/YиmidPoint` изменяются первыми и напрямую.

gui.add(cam.midPoint, 'x').listen();
gui.add(cam.midPoint, 'y').listen();
gui.add(cam, 'scrollX').listen();
gui.add(cam, 'scrollY').listen();

Код выше добавляет эти свойства в панель отладки dat.GUI с флагом listen(), чтобы они обновлялись в реальном времени.

Секрет видимой области: объект `worldView`

Самое важное свойство для расчетов видимости — это cam.worldView. Это объект типа Phaser.Geom.Rectangle. Он описывает прямоугольную область мира, которая в данный момент видна на экране игрока.

В отличие от scrollX/Y (координаты угла), worldView хранит абсолютные мировые координаты всех четырех границ видимой области.

gui.add(cam.worldView, 'left').listen();
gui.add(cam.worldView, 'top').listen();
gui.add(cam.worldView, 'right').listen();
gui.add(cam.worldView, 'bottom').listen();

Используйте worldView, когда вам нужно определить, находится ли игровой объект (например, враг или предмет) в поле зрения камеры для его активации или отрисовки. Это гораздо эффективнее, чем расчеты на основе scrollX/Y и размера экрана.

Разрешение, зум и их влияние на `worldView`

Камера имеет два набора размеров: внутренние (width, height) и отображаемые (displayWidth, displayHeight). Внутренние размеры — это разрешение области, которую рендерит камера (часто равно размеру canvas). Отображаемые размеры — это внутренние размеры, умноженные на текущий zoom.

Именно отображаемые размеры (displayWidth, displayHeight) определяют, какую область мира (worldView) захватывает камера. При увеличении зума (zoom > 1) отображаемые размеры уменьшаются, а worldView сужается, показывая меньший участок мира, но в большем масштабе.

gui.add(cam, 'displayWidth').listen();
gui.add(cam, 'displayHeight').listen();
gui.add(cam, 'zoom', 0.1, 4).step(0.1);

Изменение значения zoom в панели управления наглядно демонстрирует эту связь: границы worldView меняются, а scrollX/Y остаются прежними, так как точка привязки камеры (ее верхний левый угол) не двигается.

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

Понимание разницы между scrollX/Y, midPoint и worldView — ключ к управлению видимой областью в Phaser. worldView — ваш главный инструмент для проверки видимости объектов и оптимизации рендеринга. Для экспериментов попробуйте

  1. Реализовать следящую камеру, которая мягко обновляет scrollX/Y, и наблюдайте за изменением worldView
  2. Написать функцию, которая активирует врагов только когда они попадают в worldView камеры
  3. Связать зум камеры с колесом мыши для динамического приближения и отдаления