웹 Gamepad API: DirectInput, XInput, 그리고 매핑의 혼란
Gamepad API가 주는 것
브라우저의 Gamepad API는 연결된 컨트롤러를 navigator.getGamepads()로 노출하며, Gamepad 객체의 배열을 반환합니다. 각 객체에는 buttons 배열 — 각 항목은 pressed, touched, 0에서 1까지의 아날로그 value를 가진 객체 — 과, 아날로그 스틱 위치를 담는 axes 배열이 있고, 각 값은 -1에서 1까지입니다. gamepadconnected와 gamepaddisconnected 이벤트가 있지만, 버튼과 축의 상태는 결코 당신에게 푸시되지 않습니다: 직접 폴링해야 합니다.
실제로 이것은 requestAnimationFrame 루프 안에서 프레임마다 한 번 게임패드를 읽고, 이번 프레임을 지난 프레임과 비교해 방금 눌리거나 떼어진 버튼을 감지하는 것을 뜻합니다. 얻는 객체는 얼어붙은 스냅샷이라, 새 값을 위해 프레임마다 getGamepads()를 다시 호출합니다. 이 폴링 모델은 키보드나 포인터 이벤트에서 온 개발자가 가장 먼저 놀라는 점입니다.
'표준 매핑'
명세는 표준 매핑(standard mapping)이라 불리는 정준 레이아웃을 정의합니다. 컨트롤러가 그것에 맞으면 gamepad.mapping은 문자열 standard가 되고, 인덱스는 고정되고 예측 가능합니다: buttons[0]은 아래쪽 페이스 버튼(A / 크로스), buttons[1]은 오른쪽(B / 서클)이며, 범퍼·트리거·방향키·스틱 클릭이 모두 정의된 위치에 있고, axes[0]부터 axes[3]이 좌우 스틱을 담습니다. 17개의 버튼과 4개의 축이 알려진 순서로 있습니다.
gamepad.mapping이 빈 문자열이면 컨트롤러가 표준 레이아웃에 맞지 않은 것이며, 그 인덱스 중 무엇도 보장되지 않습니다. 같은 물리 버튼이 다른 인덱스에 올 수 있고, 트리거가 축으로 나타날 수 있으며, 방향키가 네 개의 버튼이 아니라 단일 햇 축일 수 있습니다. 어떤 인덱스를 믿기 전에 mapping 속성이 가장 먼저 확인해야 할 가장 중요한 필드입니다.
DirectInput 대 XInput: 혼란의 뿌리
Windows에서 게임이 컨트롤러와 대화하는 방법은 두 가지입니다. XInput은 Microsoft의 현대적 API로, Xbox 컨트롤러와 모든 Xbox 호환 패드가 따르는 레이아웃을 중심으로 만들어졌습니다. 그 레이아웃이 고정이라, 브라우저는 XInput 장치를 표준 매핑에 깔끔하게 대응시킬 수 있습니다 — 그래서 Xbox 컨트롤러가 웹 게임에서 '그냥 작동'합니다.
DirectInput은 모든 HID(휴먼 인터페이스 장치) 컨트롤러를 위한 더 오래되고 범용적인 API입니다: 많은 서드파티 패드, 아케이드 스틱, 비행 스틱, 레이싱 휠, 그리고 오래되거나 무명의 컨트롤러. DirectInput은 고정된 버튼 순서를 강제하지 않으므로, 브라우저에는 대응시킬 정준 레이아웃이 없고 빈 mapping을 보고합니다. 이 단 하나의 차이가 '브라우저에서 버튼이 뒤죽박죽'이라는 거의 모든 보고의 출처입니다.
왜 같은 버튼이 다른 인덱스를 보고하는가
DirectInput 패드에서는 어떤 물리 컨트롤이 '버튼 1'인지를 제조사가 정하므로, 당신의 A 버튼이 buttons[2]로 올 수 있고, 트리거가 버튼이 아니라 axes 배열 안에 나타날 수 있으며, 방향키가 네 개의 분리된 버튼이 아니라 시점(POV) 햇 — 여덟 방향을 가진 하나의 축 — 으로 보고될 수 있습니다.
이런 장치를 쓸 수 있게 하려고 각 브라우저는 자기만의 재매핑 휴리스틱을 적용하며, 항상 일치하지는 않습니다: 같은 DirectInput 컨트롤러가 Chrome과 Firefox에서 다른 인덱스를 낼 수 있습니다. XInput은 Microsoft가 레이아웃을 고정했기에 문제 전체를 비켜가며, 그래서 'Xbox 스타일 컨트롤러를 쓰라'가 웹 게임에 가장 믿을 만한 조언입니다.
코드에서 다루는 법
견고한 패턴은 먼저 gamepad.mapping을 확인하는 것입니다. standard와 같으면 이름 붙인 인덱스를 안전하게 쓸 수 있습니다. 비어 있으면 아무것도 하드코딩하지 말고 — 플레이어에게 각 액션을 누르게 해 그 장치의 레이아웃을 배우는 재매핑 화면을 제공하고, 결과를 gamepad.id를 키로 저장해 컨트롤러마다 유지되게 하세요.
매핑과 무관하게 중요한 디테일이 두 가지 더 있습니다. 아날로그 스틱은 정확히 0에서 쉬는 일이 드무므로, 작은 데드존(흔히 약 0.1)을 적용하고 그 안의 모든 것은 입력 없음으로 취급하세요. 안 그러면 드리프트가 캐릭터를 저절로 움직입니다. 그리고 API가 폴링 기반이므로, 프레임마다 게임패드를 새로 읽고 이전 프레임과 차이를 내어 연속 상태를 이산적인 '방금 눌림' 이벤트로 바꾸세요. 이 사이트의 게임패드 테스터는 당신의 특정 장치가 보고하는 원시 버튼 인덱스와 축 값을 보여줍니다 — 표준 매핑인지 보는 가장 빠른 방법입니다.
요점
인덱스를 믿기 전에 항상 mapping을 확인하세요. standard는 명세 레이아웃, 빈 값은 장치 특정을 뜻합니다.
웹 게임에는 XInput / Xbox 스타일 컨트롤러를 우선하세요 — 표준 레이아웃에 대응되지만 DirectInput 패드는 그렇지 않습니다.
비표준 패드에 인덱스를 하드코딩하지 마세요. 재매핑 UI를 제공하고 결과를 gamepad.id별로 저장하세요.
축에 데드존을 적용하세요 — 쉬고 있는 스틱이 움직임으로 등록되지 않도록.
프레임마다 폴링하고 차이를 내세요 — 버튼과 축에 대해 Gamepad API는 이벤트가 아니라 상태를 줍니다.