О чем этот пример
В играх от первого лица или 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-сцене.
