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