О чем этот пример
В играх-головоломках или шутерах от первого лица курсор не должен выходить за пределы окна браузера. Техника 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), создав эффект бесконечной карты. Или добавьте инерцию и ускорение кораблю, чтобы движение стало более плавным и физически правдоподобным.
