Skip to content

FPS oyunları için Pointer Lock ​

Pointer Lock API, fare imlecini yakalayarak akıcı FPS tarzı kamera kontrollerine olanak tanır. Birinci ve üçüncü şahıs nişancı oyunları için vazgeçilmezdir.

1) Pointer lock isteme ​

Pointer lock için bir kullanıcı etkileşimi gerekir:

js
const canvas = document.getElementById('game')

canvas.addEventListener('click', () => {
  canvas.requestPointerLock()
})

Ham fare girdisi (ivmeyi devre dışı bırakma) ​

Tarayıcı varsayılan olarak işletim sisteminin fare ivmesini movementX/movementY değerlerine uygular; bu nedenle aynı mesafede yapılan hızlı bir hareket ile yavaş bir sürükleme, kamerayı farklı miktarlarda döndürür. FPS nişan alma için genellikle bunun tersini istersiniz: aynı fiziksel mesafe, kamerayı her zaman aynı miktarda döndürmelidir. requestPointerLock() işlevine unadjustedMovement: true geçirerek ham ve ivmesiz hareket isteyin:

js
canvas.addEventListener('click', async () => {
  try {
    await canvas.requestPointerLock({ unadjustedMovement: true })
  } catch (err) {
    // unadjustedMovement unsupported here; fall back to OS-adjusted deltas
    await canvas.requestPointerLock()
  }
})

Modern requestPointerLock(), başarı durumunda çözümlenen ve başarısızlık durumunda reddedilen bir Promise döndürür; bu nedenle yukarıdaki çağrı try/catch içine alınmıştır. Chrome ve Edge, 88. sürümden beri unadjustedMovement seçeneğini destekler; Safari ise requestPointerLock işlevinin Promise döndürmesini da düzelten 18.4 sürümünden (Mart 2025) beri desteklemektedir. Promise döndürmeyen eski tarayıcılar da burada çalışır; çünkü await, Promise olmayan bir değeri kabul eder ve bilinmeyen seçenek yok sayılır. Bu nedenle sade requestPointerLock() yedeğini her zaman koruyun.

2) Kilit durumunu algılama ​

js
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) Fare hareketini okuma ​

Kilitliyken movementX ve movementY değerlerini kullanın:

js
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 kamera sınıfı ​

js
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 hareketi ​

js
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) Tam FPS kontrolcüsü ​

js
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) Nişangâh ​

js
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) Fare düğmeleriyle ateş etme ​

js
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) Hassasiyet ayarları ​

js
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) Çıkış istemi ​

Pointer lock'tan çıkma talimatlarını gösterin:

js
function showLockUI() {
  const ui = document.getElementById('lock-ui')
  
  if (document.pointerLockElement) {
    ui.innerHTML = '<p>Farenin kilidini açmak için ESC tuşuna basın</p>'
    ui.style.opacity = '0.5'
    setTimeout(() => ui.style.opacity = '0', 2000)
  } else {
    ui.innerHTML = '<p>Oynamak için tıklayın</p>'
    ui.style.opacity = '1'
  }
}

document.addEventListener('pointerlockchange', showLockUI)

Çıkış istemi hakkında bilinmesi gereken bir nokta vardır: Oyuncu pointer lock'tan çıkmak için ESC tuşuna bastığında tarayıcı, yeniden kilitleme yapılabilmesi için yeni bir kullanıcı etkileşimi (tıklama) ister. “Oynamak için tıklayın” katmanının bulunma nedeni budur. Ancak kendi kodunuz document.exitPointerLock() işlevini çağırırsa (örneğin oyun içi bir menü açmak için), sonrasında yeniden kilitleme yapmak için yeni bir etkileşim gerekmez. Böylece menü kapandığında kilidi program aracılığıyla yeniden etkinleştirebilirsiniz. ESC tuşuna art arda basılması da tarayıcının daha bilinçli bir kullanıcı eylemi gerçekleşene kadar yeniden kilitlemeyi reddetmesine yol açabilir. Bu nedenle pointerlockchange sırasında otomatik olarak yeniden kilitlemeye çalışmayın.

Iframe ile ilgili hususlar ​

Iframe'lerde pointer lock için allow="pointer-lock" özniteliği gerekir:

html
<iframe 
  src="game.html" 
  allow="pointer-lock; fullscreen"
></iframe>

Pointer lock özelliğinin kullanılabilir olup olmadığını kontrol edin:

js
if (!document.pointerLockElement && !('requestPointerLock' in canvas)) {
  showMessage('Fare yakalama kullanılamıyor')
}

İlgili içerikler ​

Harici Kaynaklar ​