Pointer Lock para jogos FPS
A API Pointer Lock captura o cursor do mouse, permitindo controles suaves de câmera no estilo FPS. Essencial para jogos de tiro em primeira e terceira pessoa.
1) Solicitando o bloqueio do ponteiro
O bloqueio do ponteiro exige uma interação do usuário:
const canvas = document.getElementById('game')
canvas.addEventListener('click', () => {
canvas.requestPointerLock()
})Entrada bruta do mouse (desativar aceleração)
Por padrão, o navegador aplica a aceleração do mouse do sistema operacional a movementX/movementY. Assim, um movimento rápido e um arrasto lento pela mesma distância produzem rotações diferentes da câmera. Para mirar em jogos FPS, geralmente você quer o contrário: a mesma distância física deve sempre girar a câmera na mesma proporção. Solicite movimentos brutos e sem aceleração passando unadjustedMovement: true para requestPointerLock():
canvas.addEventListener('click', async () => {
try {
await canvas.requestPointerLock({ unadjustedMovement: true })
} catch (err) {
// unadjustedMovement não é compatível aqui; use deltas ajustados pelo sistema operacional
await canvas.requestPointerLock()
}
})A versão moderna de requestPointerLock() retorna uma Promise que é resolvida em caso de sucesso e rejeitada em caso de falha, por isso a chamada acima está envolvida em try/catch. Chrome e Edge oferecem suporte a unadjustedMovement desde a versão 88, e o Safari desde a versão 18.4 (março de 2025), que também corrigiu requestPointerLock para retornar uma Promise. Navegadores mais antigos que não retornam uma Promise ainda funcionam aqui, pois await aceita um valor que não seja uma Promise e a opção desconhecida é simplesmente ignorada. Portanto, sempre mantenha a chamada simples a requestPointerLock() como alternativa.
2) Detectando o estado do bloqueio
document.addEventListener('pointerlockchange', () => {
if (document.pointerLockElement === canvas) {
console.log('Ponteiro bloqueado')
game.mouseLocked = true
} else {
console.log('Ponteiro desbloqueado')
game.mouseLocked = false
}
})
document.addEventListener('pointerlockerror', () => {
console.error('Falha ao bloquear o ponteiro')
})3) Lendo o movimento do mouse
Quando o ponteiro estiver bloqueado, use movementX e movementY:
document.addEventListener('mousemove', (e) => {
if (document.pointerLockElement !== canvas) return
const sensitivity = 0.002
camera.yaw -= e.movementX * sensitivity
camera.pitch -= e.movementY * sensitivity
// Limite a inclinação para evitar que a câmera vire de cabeça para baixo
camera.pitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, camera.pitch))
})4) Classe de câmera FPS
class FPSCamera {
constructor() {
this.position = { x: 0, y: 1.7, z: 0 } // Altura dos olhos
this.yaw = 0 // Rotação para esquerda/direita
this.pitch = 0 // Rotação para cima/baixo
this.sensitivity = 0.002
}
handleMouseMove(e) {
this.yaw -= e.movementX * this.sensitivity
this.pitch -= e.movementY * this.sensitivity
this.pitch = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, this.pitch))
}
getForward() {
return {
x: Math.sin(this.yaw) * Math.cos(this.pitch),
y: Math.sin(this.pitch),
z: Math.cos(this.yaw) * Math.cos(this.pitch),
}
}
getRight() {
return {
x: Math.cos(this.yaw),
y: 0,
z: -Math.sin(this.yaw),
}
}
move(forward, right, dt, speed = 5) {
const fwd = this.getForward()
const rgt = this.getRight()
// Mova apenas no plano XZ
this.position.x += (fwd.x * forward + rgt.x * right) * speed * dt
this.position.z += (fwd.z * forward + rgt.z * right) * speed * dt
}
getViewMatrix() {
const forward = this.getForward()
const target = {
x: this.position.x + forward.x,
y: this.position.y + forward.y,
z: this.position.z + forward.z,
}
return lookAt(this.position, target, { x: 0, y: 1, z: 0 })
}
}5) Movimento com WASD
const input = {
forward: false,
backward: false,
left: false,
right: false,
jump: false,
}
window.addEventListener('keydown', (e) => {
switch (e.code) {
case 'KeyW': input.forward = true; break
case 'KeyS': input.backward = true; break
case 'KeyA': input.left = true; break
case 'KeyD': input.right = true; break
case 'Space': input.jump = true; break
}
})
window.addEventListener('keyup', (e) => {
switch (e.code) {
case 'KeyW': input.forward = false; break
case 'KeyS': input.backward = false; break
case 'KeyA': input.left = false; break
case 'KeyD': input.right = false; break
case 'Space': input.jump = false; break
}
})
function update(dt) {
const moveForward = (input.forward ? 1 : 0) - (input.backward ? 1 : 0)
const moveRight = (input.right ? 1 : 0) - (input.left ? 1 : 0)
camera.move(moveForward, moveRight, dt)
}6) Controlador FPS completo
class FPSController {
constructor(canvas) {
this.canvas = canvas
this.camera = new FPSCamera()
this.locked = false
this.speed = 5
this.sprintMultiplier = 1.5
this.input = {
forward: false, backward: false,
left: false, right: false,
jump: false, sprint: false,
}
this.velocity = { x: 0, y: 0, z: 0 }
this.onGround = true
this.gravity = -20
this.jumpSpeed = 8
this.bindEvents()
}
bindEvents() {
this.canvas.addEventListener('click', () => {
this.canvas.requestPointerLock()
})
document.addEventListener('pointerlockchange', () => {
this.locked = document.pointerLockElement === this.canvas
})
document.addEventListener('mousemove', (e) => {
if (this.locked) {
this.camera.handleMouseMove(e)
}
})
window.addEventListener('keydown', (e) => this.handleKey(e.code, true))
window.addEventListener('keyup', (e) => this.handleKey(e.code, false))
}
handleKey(code, pressed) {
switch (code) {
case 'KeyW': this.input.forward = pressed; break
case 'KeyS': this.input.backward = pressed; break
case 'KeyA': this.input.left = pressed; break
case 'KeyD': this.input.right = pressed; break
case 'Space': this.input.jump = pressed; break
case 'ShiftLeft': this.input.sprint = pressed; break
}
}
update(dt) {
if (!this.locked) return
const speed = this.speed * (this.input.sprint ? this.sprintMultiplier : 1)
// Movimento horizontal
const moveForward = (this.input.forward ? 1 : 0) - (this.input.backward ? 1 : 0)
const moveRight = (this.input.right ? 1 : 0) - (this.input.left ? 1 : 0)
const forward = this.camera.getForward()
const right = this.camera.getRight()
this.velocity.x = (forward.x * moveForward + right.x * moveRight) * speed
this.velocity.z = (forward.z * moveForward + right.z * moveRight) * speed
// Salto
if (this.input.jump && this.onGround) {
this.velocity.y = this.jumpSpeed
this.onGround = false
}
// Gravidade
if (!this.onGround) {
this.velocity.y += this.gravity * dt
}
// Aplique a velocidade
this.camera.position.x += this.velocity.x * dt
this.camera.position.y += this.velocity.y * dt
this.camera.position.z += this.velocity.z * dt
// Colisão simples com o chão
if (this.camera.position.y < 1.7) {
this.camera.position.y = 1.7
this.velocity.y = 0
this.onGround = true
}
}
}7) Mira
function drawCrosshair(ctx) {
const cx = ctx.canvas.width / 2
const cy = ctx.canvas.height / 2
const size = 10
const gap = 4
ctx.strokeStyle = '#fff'
ctx.lineWidth = 2
// Superior
ctx.beginPath()
ctx.moveTo(cx, cy - gap)
ctx.lineTo(cx, cy - gap - size)
ctx.stroke()
// Inferior
ctx.beginPath()
ctx.moveTo(cx, cy + gap)
ctx.lineTo(cx, cy + gap + size)
ctx.stroke()
// Esquerda
ctx.beginPath()
ctx.moveTo(cx - gap, cy)
ctx.lineTo(cx - gap - size, cy)
ctx.stroke()
// Direita
ctx.beginPath()
ctx.moveTo(cx + gap, cy)
ctx.lineTo(cx + gap + size, cy)
ctx.stroke()
}8) Atirando com os botões do mouse
document.addEventListener('mousedown', (e) => {
if (!document.pointerLockElement) return
if (e.button === 0) {
// Botão esquerdo — disparo primário
weapon.fire()
} else if (e.button === 2) {
// Botão direito — mirar pela mira da arma
weapon.aimDownSights(true)
}
})
document.addEventListener('mouseup', (e) => {
if (e.button === 2) {
weapon.aimDownSights(false)
}
})
// Impeça a exibição do menu de contexto
canvas.addEventListener('contextmenu', (e) => e.preventDefault())9) Configurações de sensibilidade
class Settings {
constructor() {
this.mouseSensitivity = parseFloat(localStorage.getItem('sensitivity') || '1.0')
this.invertY = localStorage.getItem('invertY') === 'true'
}
save() {
localStorage.setItem('sensitivity', this.mouseSensitivity.toString())
localStorage.setItem('invertY', this.invertY.toString())
}
}
// Aplique no manipulador do mouse
document.addEventListener('mousemove', (e) => {
if (!locked) return
const sens = settings.mouseSensitivity * 0.002
camera.yaw -= e.movementX * sens
camera.pitch -= e.movementY * sens * (settings.invertY ? -1 : 1)
})10) Aviso de saída
Mostre instruções para sair do bloqueio do ponteiro:
function showLockUI() {
const ui = document.getElementById('lock-ui')
if (document.pointerLockElement) {
ui.innerHTML = '<p>Pressione ESC para liberar o mouse</p>'
ui.style.opacity = '0.5'
setTimeout(() => ui.style.opacity = '0', 2000)
} else {
ui.innerHTML = '<p>Clique para jogar</p>'
ui.style.opacity = '1'
}
}
document.addEventListener('pointerlockchange', showLockUI)Um detalhe importante sobre o aviso de saída: quando o jogador pressiona ESC para sair do bloqueio do ponteiro, o navegador exige uma nova interação do usuário (um clique) antes de permitir outro bloqueio. É por isso que a sobreposição “Clique para jogar” existe. Porém, se o próprio código chamar document.exitPointerLock() (por exemplo, para abrir um menu dentro do jogo), nenhuma nova interação será necessária para bloquear novamente depois. Assim, você poderá reativar o bloqueio por código quando o menu for fechado. Pressionar ESC repetidamente também pode fazer o navegador recusar novas tentativas de bloqueio até que o usuário realize uma ação mais deliberada. Portanto, não tente bloquear o ponteiro automaticamente em pointerlockchange.
Considerações sobre iframes
O bloqueio do ponteiro em iframes exige o atributo allow="pointer-lock":
<iframe
src="game.html"
allow="pointer-lock; fullscreen"
></iframe>Verifique se o bloqueio do ponteiro está disponível:
if (!document.pointerLockElement && !('requestPointerLock' in canvas)) {
showMessage('A captura do mouse não está disponível')
}Conteúdo relacionado
- Tratamento de entrada em jogos
- API Gamepad
- Fundamentos de WebGL
- Conceitos básicos de WebXR — controles de RV para experiências em primeira pessoa
- Primeiros passos com WebGPU — renderização moderna por GPU para jogos FPS
Recursos externos
- MDN: API Pointer Lock — referência completa da API
- MDN: MouseEvent.movementX — leitura dos deltas do mouse