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

При разработке игр с большими уровнями, выходящими за пределы экрана, критически важно правильно управлять камерой и оптимизировать размещение объектов. В этой статье мы разберем пример, который демонстрирует создание мира размером 10000x10000 пикселей, заполнение его интерактивными спрайтами и реализацию плавного, контролируемого с клавиатуры перемещения камеры. Вы научитесь настраивать границы камеры, использовать `SmoothedKeyControl` для инерционного движения и добавлять простой дебаг-интерфейс для мониторинга состояния камеры в реальном времени.

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    controls;

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

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

        let total = 1024;

        const text = this.add.text(10, 10, `Cursors to move. Click boxes. Remaining: ${total}`, { font: '16px Courier', fill: '#00ff00' }).setScrollFactor(0);

        let x = 0;
        let y = 0;
        const sx = 10000 / 32;

        for (let i = 0; i < total; i++)
        {
            const image = this.add.image(x, y, 'block').setInteractive();

            image.on('pointerup', function ()
            {

                total--;
                text.setText(`Cursors to move. Click boxes. Remaining: ${total}`);
                this.destroy();

            });

            x += sx;

            if (i > 0 && i % 32 === 0)
            {
                x = 0;
                y += sx;
            }
        }

        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.04,
            drag: 0.0005,
            maxSpeed: 1.0
        };

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

        const cam = this.cameras.main;

        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);
    }

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

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

const game = new Phaser.Game(config);


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

В методе preload() мы задаем базовый URL для загрузки и подгружаем один спрайт — изображение блока. Это основа для всех объектов в мире.

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

Сцена готова к созданию мира. Основная работа начинается в методе create().

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

Первым делом мы устанавливаем границы для основной камеры с помощью this.cameras.main.setBounds(). Это определяет область, по которой может перемещаться камера. Без этих границ камера могла бы уходить в пустоту.

this.cameras.main.setBounds(0, 0, 10000, 10000);

Затем создается текстовый элемент с информацией. Параметр setScrollFactor(0) фиксирует его положение на экране, независимо от движения камеры.

Далее, в цикле создается 1024 спрайта, которые формируют сетку 32x32. Ключевой момент — расчет шага sx, чтобы равномерно распределить объекты по всей ширине и высоте мира (10000 пикселей).

let x = 0;
let y = 0;
const sx = 10000 / 32;

for (let i = 0; i < total; i++)
{
    const image = this.add.image(x, y, 'block').setInteractive();
    image.on('pointerup', function ()
    {
        total--;
        text.setText(`Cursors to move. Click boxes. Remaining: ${total}`);
        this.destroy();
    });
    x += sx;
    if (i > 0 && i % 32 === 0)
    {
        x = 0;
        y += sx;
    }
}

Каждому спрайту добавляется обработчик события pointerup, который уменьшает счетчик, обновляет текст и уничтожает кликнутый объект. Это добавляет простой игровой механики.

Настройка плавного управления камерой

Для удобного перемещения по огромному миру используется стандартный объект cursors и система Phaser.Cameras.Controls.SmoothedKeyControl. Этот контроллер обеспечивает движение камеры с ускорением и плавным замедлением (инерцией), что гораздо приятнее для игрока, чем резкие телепортации.

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.04,
    drag: 0.0005,
    maxSpeed: 1.0
};

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

Конфигурация позволяет тонко настроить чувствительность: acceleration задает скорость разгона, drag — силу замедления, а maxSpeed ограничивает максимальную скорость движения. Вызов this.controls.update(delta) в методе update() обеспечивает плавное движение с учетом времени между кадрами.

Интерфейс для отладки камеры

Для визуализации и отладки параметров камеры в реальном времени в примере используется библиотека dat.GUI. Она создает панель, которая отображает и позволяет изменять ключевые свойства камеры.

const cam = this.cameras.main;
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);

Метод .listen() заставляет поля автоматически обновляться при изменении значений. Это позволяет наблюдать, как меняются координаты центра камеры (midPoint), ее смещение (scrollX, scrollY), внутренние размеры и масштаб (zoom). Ползунок zoom — единственный интерактивный элемент, который позволяет менять масштаб камеры на лету.

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

Важная деталь настройки игры — параметр resolution. Он устанавливается в значение window.devicePixelRatio, что позволяет корректно отображать графику на экранах с высоким разрешением (Retina-дисплеях), избегая размытия.

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

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

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

Этот пример дает готовый каркас для создания игр с огромными мирами в Phaser 3. Вы научились устанавливать границы камеры, заполнять мир объектами по сетке, реализовывать плавное управление и отлаживать параметры камеры. Для экспериментов попробуйте: изменить параметры acceleration и drag для другого "чувства" управления; добавить зум камеры к колесику мыши; реализовать камеру, которая следует за игроком в этом большом мире; или использовать процедурную генерацию для размещения тысяч объектов с разными спрайтами.