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

В играх от первого лица или 3D-редакторах часто требуется вращать камеру или объекты плавным, непрерывным движением мыши, не ограниченным краями экрана. Механизм Pointer Lock (захват указателя) браузера позволяет это реализовать. В этой статье мы разберем, как использовать Pointer Lock API вместе с Phaser для создания интерактивного вращения 3D-модели, где движение курсора напрямую управляет углами поворота, а сам курсор остается на экране. Это практический пример для добавления профессионального управления в ваши проекты.

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

Живой запуск

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

Исходный код


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

var game = new Phaser.Game(config);
var text;
var mesh;
var graphics;
var cursor;

function preload ()
{
        this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    // Source: https://www.youmagine.com/designs/low-poly-pikachu
    this.load.obj('pikachu', 'assets/obj/pikachu.obj');

    // Source: Font Awesome
    this.load.image('cursor-rotate', 'assets/sprites/cursor-rotate.png');
}

function create ()
{
    graphics = this.add.graphics();

    mesh = graphics.createMesh('pikachu', 0, 0.3, 8);
    mesh.rotation.x = Phaser.Math.DegToRad(180);
    mesh.thickness = 2;
    mesh.setFillColor(0xffda1f);
    mesh.setStrokeColor(0x6b5900);
    graphics.fillMesh(mesh);
    graphics.strokeMesh(mesh);

    cursor = this.add.sprite(0, 0, 'cursor-rotate');

    // Pointer lock will only work after an 'engagement gesture', e.g. mousedown, keypress, etc.
    game.canvas.addEventListener('mousedown', function () {
        game.input.mouse.requestPointerLock();
        cursor.x = this.input.x;
        cursor.y = this.input.y;
    }.bind(this));

    // When locked, you will have to use the movementX and movementY properties of the pointer
    // (since a locked cursor's xy position does not update)
    this.input.on('pointermove', function (pointer) {
        if (this.input.mouse.locked)
        {
            mesh.rotation.x += pointer.movementY * 0.01;
            mesh.rotation.y += pointer.movementX * 0.01;

            graphics.clear();
            graphics.fillMesh(mesh);
            graphics.strokeMesh(mesh);
            updateLockText();

            cursor.x += pointer.movementX;
            cursor.y += pointer.movementY;

            // Force the cursor to stay on screen by wrapping around at the edges
            cursor.x = Phaser.Math.Wrap(cursor.x, 0, game.renderer.width);
            cursor.y = Phaser.Math.Wrap(cursor.y, 0, game.renderer.height);

            updateLockText(true);
        }
    }, this);

    game.input.on('POINTER_LOCK_CHANGE_EVENT', function (event) {
        updateLockText(event.isPointerLocked);
    }, 0, this);

    text = this.add.text(16, 16, '', { fontSize: '20px', fill: '#ffffff' });
    updateLockText(false);
}

function updateLockText (isLocked)
{
    var xRotation = Phaser.Math.Wrap(mesh.rotation.x * 180 / Math.PI, 0, 360).toFixed(1);
    var yRotation = Phaser.Math.Wrap(mesh.rotation.y * 180 / Math.PI, 0, 360).toFixed(1);
    var zRotation = Phaser.Math.Wrap(mesh.rotation.z * 180 / Math.PI, 0, 360).toFixed(1);
    text.setText([
        isLocked ? 'Move cursor to rotate.' : 'Click to edit rotation.',
        'Current rotation: (' + xRotation + ', ' + yRotation + ', ' + zRotation + ')'
    ]);
}

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

Первым делом настраивается базовая сцена Phaser и загружаются необходимые ресурсы. Нам понадобится 3D-модель в формате .obj и спрайт для визуального курсора.

function preload ()
{
    this.load.setBaseURL('https://raw.githubusercontent.com/phaserjs/examples/master/public/');
    this.load.obj('pikachu', 'assets/obj/pikachu.obj');
    this.load.image('cursor-rotate', 'assets/sprites/cursor-rotate.png');
}

В функции create создается графика. Объект graphics используется для работы с 3D-мешами. Метод createMesh загружает ранее загруженную модель 'pikachu', задает ее позицию, масштаб и создает объект меша. Затем меш поворачивается по оси X, задается толщина линий и цвета заливки и обводки. После настройки меш отрисовывается на экране с помощью fillMesh и strokeMesh. Также добавляется спрайт курсора.

function create ()
{
    graphics = this.add.graphics();
    mesh = graphics.createMesh('pikachu', 0, 0.3, 8);
    mesh.rotation.x = Phaser.Math.DegToRad(180);
    mesh.thickness = 2;
    mesh.setFillColor(0xffda1f);
    mesh.setStrokeColor(0x6b5900);
    graphics.fillMesh(mesh);
    graphics.strokeMesh(mesh);
    cursor = this.add.sprite(0, 0, 'cursor-rotate');
}

Активация Pointer Lock по жесту

Браузеры требуют жеста взаимодействия (например, клика мыши) для активации Pointer Lock. Это сделано в целях безопасности и UX. Мы добавляем слушатель события mousedown непосредственно на canvas игры.

game.canvas.addEventListener('mousedown', function () {
    game.input.mouse.requestPointerLock();
    cursor.x = this.input.x;
    cursor.y = this.input.y;
}.bind(this));

При клике вызывается метод requestPointerLock() объекта game.input.mouse. Это запрашивает у браузера захват курсора. Одновременно мы позиционируем спрайт нашего курсора в текущие координаты мыши this.input.x и this.input.y.

Важно: после активации Pointer Lock, свойство pointer.x/`yперестает обновляться. Для отслеживания движения мыши необходимо использовать свойстваmovementXиmovementY`.

Обработка движения мыши при захвате

Основная логика вращения находится в обработчике события pointermove. Мы проверяем, активен ли захват курсора (this.input.mouse.locked). Если да, то используем movementX и movementY для изменения вращения меша.

this.input.on('pointermove', function (pointer) {
    if (this.input.mouse.locked)
    {
        mesh.rotation.x += pointer.movementY * 0.01;
        mesh.rotation.y += pointer.movementX * 0.01;
        graphics.clear();
        graphics.fillMesh(mesh);
        graphics.strokeMesh(mesh);
        updateLockText();
        cursor.x += pointer.movementX;
        cursor.y += pointer.movementY;
        cursor.x = Phaser.Math.Wrap(cursor.x, 0, game.renderer.width);
        cursor.y = Phaser.Math.Wrap(cursor.y, 0, game.renderer.height);
        updateLockText(true);
    }
}, this);

Изменение mesh.rotation.x на movementY (и наоборот) создает интуитивное управление: движение мыши вверх-вниз вращает объект по оси X, а влево-вправо — по оси Y. После обновления вращения мы полностью очищаем graphics и перерисовываем меш с новыми параметрами. Спрайт курсора также перемещается на величину movementX/Y. Функция Phaser.Math.Wrap гарантирует, что курсор, уходя за границу экрана, появляется с противоположной стороны, создавая эффект бесконечного пространства для мыши.

Отслеживание состояния Pointer Lock и UI

Phaser генерирует специальное событие POINTER_LOCK_CHANGE_EVENT при изменении состояния захвата. Мы подписываемся на него, чтобы обновлять текстовую подсказку для пользователя.

game.input.on('POINTER_LOCK_CHANGE_EVENT', function (event) {
    updateLockText(event.isPointerLocked);
}, 0, this);

Функция updateLockText выполняет две задачи: показывает инструкцию в зависимости от состояния и выводит текущие углы вращения модели в градусах.

function updateLockText (isLocked)
{
    var xRotation = Phaser.Math.Wrap(mesh.rotation.x * 180 / Math.PI, 0, 360).toFixed(1);
    var yRotation = Phaser.Math.Wrap(mesh.rotation.y * 180 / Math.PI, 0, 360).toFixed(1);
    var zRotation = Phaser.Math.Wrap(mesh.rotation.z * 180 / Math.PI, 0, 360).toFixed(1);
    text.setText([
        isLocked ? 'Move cursor to rotate.' : 'Click to edit rotation.',
        'Current rotation: (' + xRotation + ', ' + yRotation + ', ' + zRotation + ')'
    ]);
}

Здесь Phaser.Math.Wrap используется повторно, чтобы углы вращения всегда отображались в диапазоне от 0 до 360 градусов, что удобно для восприятия.

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

Использование Pointer Lock API в Phaser открывает возможности для создания продвинутого управления в 3D-сценах. Вы научились запрашивать захват курсора, обрабатывать его движение через movementX/Y и применять это для плавного вращения объектов. Для экспериментов попробуйте: 1. Изменить множитель 0.01 для управления чувствительностью вращения. 2. Добавить вращение по оси Z при нажатии клавиши-модификатора (например, Shift). 3. Реализовать инерцию вращения: продолжать вращать объект после остановки мыши, плавно замедляя его. 4. Использовать эту технику не для объекта, а для камеры в 3D-сцене.