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

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

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

Живой запуск

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

Исходный код


class Example extends Phaser.Scene
{
    lockText;
    sprite;

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

    create ()
    {
        this.sprite = this.add.sprite(400, 300, 'ship');

        // Pointer lock will only work after an 'engagement gesture', e.g. mousedown, keypress, etc.
        this.input.on('pointerdown', function (pointer)
        {

            this.input.mouse.requestPointerLock();

        }, 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)
            {
                this.sprite.x += pointer.movementX;
                this.sprite.y += pointer.movementY;


                // Force the sprite to stay on screen
                this.sprite.x = Phaser.Math.Wrap(this.sprite.x, 0, game.renderer.width);
                this.sprite.y = Phaser.Math.Wrap(this.sprite.y, 0, game.renderer.height);

                if (pointer.movementX > 0) { this.sprite.setRotation(0.1); }
                else if (pointer.movementX < 0) { this.sprite.setRotation(-0.1); }
                else { this.sprite.setRotation(0); }

                this.updateLockText(true);
            }
        }, this);

        // Exit pointer lock when Q is pressed. Browsers will also exit pointer lock when escape is
        // pressed.
        this.input.keyboard.on('keydown-Q', function (event)
        {
            if (this.input.mouse.locked)
            {
                this.input.mouse.releasePointerLock();
            }
        }, this);

        // Optionally, you can subscribe to the game's pointer lock change event to know when the player
        // enters/exits pointer lock. This is useful if you need to update the UI, change to a custom
        // mouse cursor, etc.
        this.input.manager.events.on('pointerlockchange', event =>
        {
            this.updateLockText(event.isPointerLocked, this.sprite.x, this.sprite.y);

        });

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

        this.updateLockText(false);
    }

    updateLockText (isLocked)
    {
        this.lockText.setText([
            isLocked ? 'The pointer is now locked!' : 'The pointer is now unlocked.',
            `Sprite is at: (${this.sprite.x},${this.sprite.y})`,
            'Press Q to release pointer lock.'
        ]);
    }
}

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

const game = new Phaser.Game(config);

Что такое Pointer Lock и зачем он нужен?

Обычно координаты указателя мыши (pointer.x и pointer.y) привязаны к абсолютной позиции на экране и обнуляются, когда курсор достигает края окна. В режиме Pointer Lock браузер "захватывает" указатель, скрывает его и предоставляет только данные об относительном перемещении (movementX и movementY). Это позволяет объекту двигаться непрерывно, что критично для: * Камеры от первого лица. * Управления космическим кораблём или танком. * Любого элемента, которому нужно бесконечное перемещение.

В Phaser работа с этой технологией ведётся через объект this.input.mouse.

Активация захвата по жесту взаимодействия

По правилам браузеров, Pointer Lock можно активировать только в ответ на явное действие пользователя — так называемый 'жест взаимодействия'. Это может быть клик (pointerdown), нажатие клавиши или касание. В нашем примере захват включается по клику в любом месте игры.

this.input.on('pointerdown', function (pointer) {
    this.input.mouse.requestPointerLock();
}, this);

Вызов requestPointerLock() отправляет браузеру запрос на захват. Пользователь может увидеть системное уведомление с запросом разрешения.

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

Когда указатель захвачен, его абсолютные координаты перестают обновляться. Вместо этого в обработчике события pointermove мы используем свойства pointer.movementX и pointer.movementY, которые показывают, на сколько пикселей сместилась мышь с предыдущего кадра.

this.input.on('pointermove', function (pointer) {
    if (this.input.mouse.locked) {
        this.sprite.x += pointer.movementX;
        this.sprite.y += pointer.movementY;
    }
}, this);

Проверка this.input.mouse.locked гарантирует, что логика перемещения сработает только в активном режиме захвата.

Улучшение примера: ограничение и визуальная обратная связь

Исходный код примера дополняет базовую логику двумя важными деталями.

1. **Удержание спрайта на экране:** Метод Phaser.Math.Wrap() заставляет корабль, вылетевший за одну границу, появиться с противоположной.

this.sprite.x = Phaser.Math.Wrap(this.sprite.x, 0, game.renderer.width);
this.sprite.y = Phaser.Math.Wrap(this.sprite.y, 0, game.renderer.height);

2. **Визуальная обратная связь:** Вращение спрайта в зависимости от направления движения по оси X делает управление более отзывчивым.

if (pointer.movementX > 0) { this.sprite.setRotation(0.1); }
else if (pointer.movementX < 0) { this.sprite.setRotation(-0.1); }
else { this.sprite.setRotation(0); }

Выход из режима захвата и отслеживание состояния

Пользователь может выйти из режима, нажав Escape (системная клавиша браузера) или Q, как реализовано в примере.

this.input.keyboard.on('keydown-Q', function (event) {
    if (this.input.mouse.locked) {
        this.input.mouse.releasePointerLock();
    }
}, this);

Для синхронизации интерфейса с состоянием захвата можно подписаться на глобальное событие pointerlockchange. Оно срабатывает при любом изменении состояния, будь то системное или программное.

this.input.manager.events.on('pointerlockchange', event => {
    this.updateLockText(event.isPointerLocked, this.sprite.x, this.sprite.y);
});

Функция updateLockText обновляет текстовую подсказку на экране, информируя пользователя о текущем режиме и позиции корабля.

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

Pointer Lock — мощный инструмент для создания иммерсивного и непрерывного управления в браузерных играх. Он снимает ограничения, накладываемые краями окна, и открывает путь к более сложным механикам. Для экспериментов попробуйте привязать движение указателя не к спрайту, а к камере (this.cameras.main.scrollX/Y), создав эффект бесконечной карты. Или добавьте инерцию и ускорение кораблю, чтобы движение стало более плавным и физически правдоподобным.