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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene {

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

    create() {
        // Build a random level as a 2D array.
        const level = [];
        for (let y = 0; y < 60; y++) {
            const row = [];
            for (let x = 0; x < 60; x++) {
                const tileIndex = Phaser.Math.RND.integerInRange(0, 6);
                row.push(tileIndex);
            }
            level.push(row);
        }

        const map = this.make.tilemap({ data: level, tileWidth: 32, tileHeight: 32 });
        const tileset = map.addTilesetImage('tiles');
        const layer = map.createLayer(0, tileset, 0, 0);
        const camera = this.cameras.main;
        let cameraDragStartX;
        let cameraDragStartY;

        this.input.on('pointerdown', () => {
            cameraDragStartX = camera.scrollX;
            cameraDragStartY = camera.scrollY;
        });

        this.input.on('pointermove', (pointer) => {
            if (pointer.isDown) {
                camera.scrollX = cameraDragStartX + (pointer.downX - pointer.x) / camera.zoom;
                camera.scrollY = cameraDragStartY + (pointer.downY - pointer.y) / camera.zoom;
            }
        });

        this.input.on('wheel', (pointer, gameObjects, deltaX, deltaY, deltaZ) => {
            // Get the current world point under pointer.
            const worldPoint = camera.getWorldPoint(pointer.x, pointer.y);
            const newZoom = camera.zoom - camera.zoom * 0.001 * deltaY;
            camera.zoom = Phaser.Math.Clamp(newZoom, 0.25, 2);

            // Update camera matrix, so `getWorldPoint` returns zoom-adjusted coordinates.
            camera.preRender();
            const newWorldPoint = camera.getWorldPoint(pointer.x, pointer.y);
            // Scroll the camera to keep the pointer under the same world point.
            camera.scrollX -= newWorldPoint.x - worldPoint.x;
            camera.scrollY -= newWorldPoint.y - worldPoint.y;
        });
    }
}

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

const game = new Phaser.Game(config);

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

В методе preload загружается один изображение-тайлсет. В create сначала генерируется случайный уровень в виде двумерного массива чисел (индексов тайлов). Затем этот массив превращается в объект тайловой карты (Tilemap).

const map = this.make.tilemap({ data: level, tileWidth: 32, tileHeight: 32 });
const tileset = map.addTilesetImage('tiles');
const layer = map.createLayer(0, tileset, 0, 0);

Код создает карту из данных массива level, где каждый элемент — индекс тайла размером 32x32 пикселя. Затем к карте добавляется загруженный тайлсет и создаётся слой для отображения. Основная камера сохраняется в переменную camera для дальнейшего управления.

Реализация перетаскивания камеры

Для панорамирования камеры по холсту нужно обработать события нажатия и перемещения указателя. Логика основана на сохранении начальной позиции прокрутки камеры в момент нажатия.

this.input.on('pointerdown', () => {
    cameraDragStartX = camera.scrollX;
    cameraDragStartY = camera.scrollY;
});

При событии pointermove проверяется, зажата ли кнопка мыши. Если да, позиция прокрутки камеры (scrollX, scrollY) пересчитывается. Важный нюанс: разница координат мыши делится на текущий camera.zoom, чтобы скорость перетаскивания была одинаковой при любом уровне зума.

camera.scrollX = cameraDragStartX + (pointer.downX - pointer.x) / camera.zoom;
camera.scrollY = cameraDragStartY + (pointer.downY - pointer.y) / camera.zoom;

Интеллектуальный зум колёсиком мыши

Самый интересный момент — реализация зума, который центрирован на позиции курсора. Это делает навигацию интуитивной: игрок увеличивает масштаб именно там, куда смотрит.

Сначала в обработчике события wheel получается текущая мировые координаты под указателем. Для этого используется метод камеры getWorldPoint.

const worldPoint = camera.getWorldPoint(pointer.x, pointer.y);

Затем вычисляется новый уровень зума, основываясь на значении deltaY (скорость прокрутки колёсика). Значение ограничивается функцией Phaser.Math.Clamp.

const newZoom = camera.zoom - camera.zoom * 0.001 * deltaY;
camera.zoom = Phaser.Math.Clamp(newZoom, 0.25, 2);

После изменения зума необходимо обновить матрицу камеры вызовом camera.preRender(). Это нужно, чтобы следующий вызов getWorldPoint вернул координаты с учётом нового масштаба.

camera.preRender();
const newWorldPoint = camera.getWorldPoint(pointer.x, pointer.y);

Финальный шаг — сдвинуть камеру так, чтобы та самая точка в мире, на которую указывал курсор, снова оказалась под ним после зума.

camera.scrollX -= newWorldPoint.x - worldPoint.x;
camera.scrollY -= newWorldPoint.y - worldPoint.y;

Конфигурация игры и особенности

Конфигурация игры стандартна, но с двумя важными настройками.

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

Ключевой параметр здесь — pixelArt: true. Он включает специальный режим рендеринга для пиксельной графики, который отключает сглаживание при масштабировании. Это гарантирует, что наши тайлы размером 32x32 будут отображаться чёткими при любом уровне зума камеры.

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

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