FPS games के लिए Pointer Lock
Pointer Lock API mouse cursor को कैप्चर कर लेता है, जिससे smooth FPS-style camera controls मिलते हैं। पहले-व्यक्ति और तीसरे-व्यक्ति वाले shooters के लिए यह बहुत जरूरी है।
1) Pointer lock का अनुरोध करना
Pointer lock के लिए एक user gesture चाहिए होता है:
const canvas = document.getElementById('game')
canvas.addEventListener('click', () => {
canvas.requestPointerLock()
})Raw mouse input (acceleration बंद करना)
डिफ़ॉल्ट रूप से browser operating system का mouse acceleration movementX/movementY पर लगाता है, इसलिए एक तेज़ झटका और उतनी ही दूरी पर एक धीमा drag अलग-अलग camera rotation पैदा करते हैं। FPS aim के लिए आपको आमतौर पर इसका उल्टा चाहिए: एक ही physical distance हमेशा camera को उतना ही घुमाए। requestPointerLock() को unadjustedMovement: true पास करके raw, बिना-acceleration वाला movement मांगें:
canvas.addEventListener('click', async () => {
try {
await canvas.requestPointerLock({ unadjustedMovement: true })
} catch (err) {
// unadjustedMovement यहां समर्थित नहीं है; OS-adjusted deltas पर वापस जाएं
await canvas.requestPointerLock()
}
})आधुनिक requestPointerLock() एक Promise लौटाता है जो success पर resolve होता है और failure पर reject होता है, इसीलिए ऊपर वाले call को try/catch में लपेटा गया है। Chrome और Edge version 88 से unadjustedMovement का समर्थन करते आ रहे हैं, और Safari इसे 18.4 (मार्च 2025) से समर्थित करता है, यह वही release है जिसने requestPointerLock को Promise लौटाने के लिए ठीक किया। पुराने browsers जो Promise नहीं लौटाते वे भी यहां काम करते हैं क्योंकि await एक non-Promise value को सह लेता है और अनजान option को बस अनदेखा कर दिया जाता है, इसलिए साधारण requestPointerLock() fallback को हमेशा रखें।
2) Lock state का पता लगाना
document.addEventListener('pointerlockchange', () => {
if (document.pointerLockElement === canvas) {
console.log('Pointer locked')
game.mouseLocked = true
} else {
console.log('Pointer unlocked')
game.mouseLocked = false
}
})
document.addEventListener('pointerlockerror', () => {
console.error('Pointer lock failed')
})3) Mouse movement पढ़ना
जब locked हो, तब movementX और movementY का इस्तेमाल करें:
document.addEventListener('mousemove', (e) => {
if (document.pointerLockElement !== canvas) return
const sensitivity = 0.002
camera.yaw -= e.movementX * sensitivity
camera.pitch -= e.movementY * sensitivity
// Clamp pitch to prevent flipping
camera.pitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, camera.pitch))
})4) FPS camera class
class FPSCamera {
constructor() {
this.position = { x: 0, y: 1.7, z: 0 } // Eye height
this.yaw = 0 // Left/right rotation
this.pitch = 0 // Up/down rotation
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()
// Move on XZ plane only
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) WASD movement
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) पूरा FPS controller
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)
// Horizontal movement
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
// Jumping
if (this.input.jump && this.onGround) {
this.velocity.y = this.jumpSpeed
this.onGround = false
}
// Gravity
if (!this.onGround) {
this.velocity.y += this.gravity * dt
}
// Apply velocity
this.camera.position.x += this.velocity.x * dt
this.camera.position.y += this.velocity.y * dt
this.camera.position.z += this.velocity.z * dt
// Simple ground collision
if (this.camera.position.y < 1.7) {
this.camera.position.y = 1.7
this.velocity.y = 0
this.onGround = true
}
}
}7) Crosshair
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
// Top
ctx.beginPath()
ctx.moveTo(cx, cy - gap)
ctx.lineTo(cx, cy - gap - size)
ctx.stroke()
// Bottom
ctx.beginPath()
ctx.moveTo(cx, cy + gap)
ctx.lineTo(cx, cy + gap + size)
ctx.stroke()
// Left
ctx.beginPath()
ctx.moveTo(cx - gap, cy)
ctx.lineTo(cx - gap - size, cy)
ctx.stroke()
// Right
ctx.beginPath()
ctx.moveTo(cx + gap, cy)
ctx.lineTo(cx + gap + size, cy)
ctx.stroke()
}8) Mouse buttons से shooting
document.addEventListener('mousedown', (e) => {
if (!document.pointerLockElement) return
if (e.button === 0) {
// Left click - primary fire
weapon.fire()
} else if (e.button === 2) {
// Right click - aim down sights
weapon.aimDownSights(true)
}
})
document.addEventListener('mouseup', (e) => {
if (e.button === 2) {
weapon.aimDownSights(false)
}
})
// Prevent context menu
canvas.addEventListener('contextmenu', (e) => e.preventDefault())9) Sensitivity settings
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())
}
}
// Apply in mouse handler
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) Exit prompt
Pointer lock से बाहर निकलने के लिए निर्देश दिखाएं:
function showLockUI() {
const ui = document.getElementById('lock-ui')
if (document.pointerLockElement) {
ui.innerHTML = '<p>Press ESC to unlock mouse</p>'
ui.style.opacity = '0.5'
setTimeout(() => ui.style.opacity = '0', 2000)
} else {
ui.innerHTML = '<p>Click to play</p>'
ui.style.opacity = '1'
}
}
document.addEventListener('pointerlockchange', showLockUI)Exit prompt के बारे में एक बात जान लें: जब player pointer lock छोड़ने के लिए ESC दबाता है, तो browser दोबारा lock करने से पहले एक नया user gesture (एक click) मांगता है, इसीलिए 'Click to play' overlay मौजूद होता है। लेकिन अगर आपका अपना code document.exitPointerLock() को call करता है (उदाहरण के लिए in-game menu खोलने के लिए), तो बाद में दोबारा lock करने के लिए किसी नए gesture की जरूरत नहीं होती, इसलिए menu बंद होने पर आप programmatically दोबारा lock कर सकते हैं। बार-बार ESC दबाना browser को यह संकेत भी दे सकता है कि किसी और सोचे-समझे user action तक दोबारा lock करने से इनकार कर दे, इसलिए pointerlockchange पर auto-relock करने की कोशिश न करें।
Iframe से जुड़ी बातें
Iframes में pointer lock के लिए allow="pointer-lock" attribute चाहिए होता है:
<iframe
src="game.html"
allow="pointer-lock; fullscreen"
></iframe>देखें कि pointer lock उपलब्ध है या नहीं:
if (!document.pointerLockElement && !('requestPointerLock' in canvas)) {
showMessage('Mouse capture not available')
}संबंधित
- Game input handling
- Gamepad API
- WebGL fundamentals
- WebXR basics — पहले-व्यक्ति अनुभवों के लिए VR controls
- WebGPU getting started — FPS games के लिए आधुनिक GPU rendering
External Resources
- MDN: Pointer Lock API — पूरा API reference
- MDN: MouseEvent.movementX — mouse deltas पढ़ना