Skip to content

Penanganan Input Game

Penanganan input yang baik dapat menentukan keberhasilan sebuah game. Kontrol yang lambat, input yang tidak terdeteksi, atau pemetaan tombol yang membingungkan akan membuat pemain frustrasi bahkan sebelum mereka melihat konten game Anda. Tutorial ini menunjukkan cara menangani keyboard, mouse, sentuhan, dan gamepad secara terpadu.

Coba terlebih dahulu:

Pola Status Input

Pendekatan sederhana adalah merespons event secara langsung: ketika pengguna menekan tombol, lakukan sesuatu. Pendekatan ini menimbulkan masalah. Event dipicu pada waktu yang tidak dapat diprediksi, sedangkan game loop Anda berjalan dengan laju tetap. Jika Anda menggerakkan pemain di dalam handler keydown, kecepatan gerakan akan bergantung pada laju pengulangan tombol yang berbeda-beda di setiap sistem operasi.

Sebagai gantinya, pertahankan objek status input yang dibaca oleh game loop Anda:

js
const input = {
  left: false,
  right: false,
  up: false,
  down: false,
  action: false,
  pointer: { x: 0, y: 0, down: false },
}

Handler event menetapkan flag tersebut. Game loop membacanya. Pemisahan ini menyederhanakan semuanya: logika pembaruan Anda tidak perlu mengetahui apakah input berasal dari keyboard, sentuhan, atau gamepad.

Keyboard

Gunakan e.code, bukan e.key, karena properti tersebut mengacu pada posisi fisik tombol. WASD akan berfungsi dengan benar pada tata letak non-QWERTY.

js
const keyMap = {
  ArrowLeft: 'left',
  ArrowRight: 'right',
  ArrowUp: 'up',
  ArrowDown: 'down',
  KeyA: 'left',
  KeyD: 'right',
  KeyW: 'up',
  KeyS: 'down',
  Space: 'action',
}

window.addEventListener('keydown', (e) => {
  const action = keyMap[e.code]
  if (action) {
    input[action] = true
    e.preventDefault()
  }
})

window.addEventListener('keyup', (e) => {
  const action = keyMap[e.code]
  if (action) input[action] = false
})

e.preventDefault() mencegah tombol panah menggulir halaman. Panggil metode ini hanya untuk tombol yang Anda tangani.

Mouse

Koordinat mouse menggunakan ruang layar. Anda perlu mengonversinya ke ruang canvas:

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

canvas.addEventListener('mousemove', (e) => {
  const rect = canvas.getBoundingClientRect()
  input.pointer.x = e.clientX - rect.left
  input.pointer.y = e.clientY - rect.top
})

canvas.addEventListener('mousedown', () => {
  input.pointer.down = true
  input.action = true
})

canvas.addEventListener('mouseup', () => {
  input.pointer.down = false
  input.action = false
})

Jika canvas Anda diskalakan (lihat tutorial canvas responsif), Anda perlu membaginya dengan faktor skala untuk mendapatkan koordinat game.

Sentuhan

Sentuhan bekerja serupa dengan mouse, tetapi Anda memerlukan e.preventDefault() untuk mencegah browser menggulir atau memperbesar halaman:

js
canvas.addEventListener('touchstart', (e) => {
  e.preventDefault()
  const touch = e.touches[0]
  const rect = canvas.getBoundingClientRect()
  input.pointer.x = touch.clientX - rect.left
  input.pointer.y = touch.clientY - rect.top
  input.pointer.down = true
  input.action = true
}, { passive: false })

canvas.addEventListener('touchmove', (e) => {
  e.preventDefault()
  const touch = e.touches[0]
  const rect = canvas.getBoundingClientRect()
  input.pointer.x = touch.clientX - rect.left
  input.pointer.y = touch.clientY - rect.top
}, { passive: false })

canvas.addEventListener('touchend', () => {
  input.pointer.down = false
  input.action = false
})

{ passive: false } diperlukan karena browser modern secara default menggunakan listener sentuhan pasif demi performa. Tanpanya, preventDefault tidak akan berfungsi.

D-Pad Virtual untuk Perangkat Seluler

Untuk game yang memerlukan kontrol arah di perangkat seluler, tambahkan tombol di layar:

js
function createVirtualDpad(container) {
  const dpad = document.createElement('div')
  dpad.className = 'virtual-dpad'
  dpad.innerHTML = `
    <button data-dir="up">↑</button>
    <button data-dir="left">←</button>
    <button data-dir="right">→</button>
    <button data-dir="down">↓</button>
  `
  
  dpad.querySelectorAll('button').forEach(btn => {
    const dir = btn.dataset.dir
    btn.addEventListener('touchstart', (e) => {
      e.preventDefault()
      input[dir] = true
    })
    btn.addEventListener('touchend', () => input[dir] = false)
  })
  
  container.appendChild(dpad)
}

Atur gaya tombol-tombol ini agar cukup besar untuk ditekan dengan ibu jari (setidaknya 44x44 piksel), lalu posisikan di sudut bawah.

Gamepad

Berbeda dari keyboard dan mouse, status gamepad tidak digerakkan oleh event. Anda harus melakukan polling pada setiap frame:

js
function pollGamepads() {
  const gamepads = navigator.getGamepads()
  const gp = gamepads[0]
  
  if (gp) {
    // D-pad or left stick
    input.left = gp.buttons[14]?.pressed || gp.axes[0] < -0.5
    input.right = gp.buttons[15]?.pressed || gp.axes[0] > 0.5
    input.up = gp.buttons[12]?.pressed || gp.axes[1] < -0.5
    input.down = gp.buttons[13]?.pressed || gp.axes[1] > 0.5
    
    // A button (standard mapping)
    input.action = gp.buttons[0]?.pressed
  }
}

Panggil pollGamepads() pada awal fungsi pembaruan Anda. Ambang batas 0,5 pada stik analog menciptakan zona mati agar gerakan kecil pada stik tidak terdeteksi sebagai input.

Anda dapat mendeteksi saat gamepad tersambung dan terputus:

js
window.addEventListener('gamepadconnected', (e) => {
  console.log('Gamepad connected:', e.gamepad.id)
})

window.addEventListener('gamepaddisconnected', (e) => {
  console.log('Gamepad disconnected:', e.gamepad.id)
})

Menggunakan Input dalam Game Loop Anda

Sekarang fungsi pembaruan Anda cukup membaca status input:

js
function update(dt) {
  pollGamepads()
  
  if (input.left) player.x -= player.speed * dt
  if (input.right) player.x += player.speed * dt
  if (input.up) player.y -= player.speed * dt
  if (input.down) player.y += player.speed * dt
  
  if (input.action && player.canShoot) {
    player.shoot()
  }
}

Cara ini berfungsi terlepas dari apakah input berasal dari keyboard, sentuhan, atau gamepad. Logika game tidak perlu mengetahuinya.

Baru Ditekan vs Ditahan

Terkadang Anda ingin mendeteksi saat tombol baru saja ditekan pada frame ini, bukan apakah tombol tersebut sedang ditahan. Melompat adalah contoh yang tepat: Anda menginginkan satu lompatan untuk setiap penekanan, bukan lompatan terus-menerus selama tombol ditahan.

js
const inputPrev = { ...input }

function wasJustPressed(action) {
  return input[action] && !inputPrev[action]
}

function endFrame() {
  Object.assign(inputPrev, input)
}

Panggil endFrame() pada akhir loop pembaruan Anda. Kemudian gunakan wasJustPressed('action'), bukan input.action, untuk tindakan yang hanya dijalankan sekali.

Kehilangan Fokus

Saat jendela browser kehilangan fokus, event keyboard berhenti dipicu. Jika pemain sedang menahan tombol, statusnya akan terus aktif:

js
window.addEventListener('blur', () => {
  input.left = input.right = input.up = input.down = input.action = false
})

Tanpa penanganan ini, pemain akan terus bergerak ketika pengguna beralih jendela dengan alt-tab.

Praktik Terbaik

Dukung WASD dan tombol panah karena setiap pemain memiliki preferensi. Izinkan pemetaan ulang tombol dan simpan konfigurasinya di localStorage. Sediakan kontrol sentuh pada perangkat seluler. Uji dengan gamepad sungguhan karena kontroler Xbox, PlayStation, dan generik memiliki perilaku yang sedikit berbeda. Dan selalu tangani hilangnya fokus dengan baik.

Referensi Lainnya

Pembahasan mendalam Gamepad API membahas fitur gamepad tingkat lanjut seperti getaran dan dukungan beberapa pemain.

Game web yang ramah perangkat seluler membahas input sentuh secara lebih mendetail.

Pointer Lock untuk game FPS menunjukkan cara menangkap mouse untuk kontrol orang pertama.

Game loop Canvas 2D menunjukkan pola status input yang diintegrasikan ke dalam game loop lengkap.

Dasar-dasar multipemain WebSocket membahas sinkronisasi input di antara pemain dalam jaringan.

Referensi Eksternal