Gamepad API를 활용한 장애물 넘기기

Marcin Wichary
Marcin Wichary

소개

초보자는 어드벤처 게임을 위한 키보드, 과일 자르기를 위한 소중한 멀티 터치 감각의 손가락, 마이클 잭슨처럼 춤을 추는 척할 수 있는 멋진 최신형 모션 센서를 계속 사용하세요. (뉴스플래시: 할 수 없습니다.) 하지만 당신은 다릅니다. 괜찮으신가요? 프로의 경지에요. 게임은 손에 쥔 게임패드로 시작하고 끝납니다.

하지만 잠깐만요. 웹 앱에서 게임패드를 지원하려면 어떻게 해야 하나요? 더 이상은 그럴 필요가 없습니다. 새로운 Gamepad API를 사용하면 JavaScript를 사용하여 컴퓨터에 연결된 게임패드 컨트롤러의 상태를 읽을 수 있습니다. 이 기능은 아직 출시된 지 얼마 되지 않아서 지난주에야 Chrome 21에 적용되었으며, Firefox에서도 곧 지원될 예정입니다 (현재 특별 빌드에서 사용 가능).

다행히 좋은 타이밍이었습니다. 최근 2012년 장애물 경주 Google 두들에 이 기술을 사용할 수 있었습니다. 이 도움말에서는 Doodle에 Gamepad API를 추가한 방법과 그 과정에서 얻은 정보를 간단히 설명합니다.

2012년 장애물 경주 Google 기념일 로고
2012년 장애물 Google 기념일 로고

게임패드 테스터

대화형 낙서는 일회성이지만 내부적으로는 상당히 복잡한 경향이 있습니다. 설명을 더 쉽게 하기 위해 드 doodle의 게임패드 코드를 가져와 간단한 게임패드 테스터를 만들었습니다. 이를 사용하여 USB 게임패드가 제대로 작동하는지 확인하고 내부 작동 방식을 살펴볼 수 있습니다.

현재 어떤 브라우저에서 지원되나요?

브라우저 지원

  • Chrome: 21.
  • Edge: 12.
  • Firefox: 29
  • Safari: 10.1

소스

어떤 게임패드를 사용할 수 있나요?

일반적으로 시스템에서 기본적으로 지원하는 최신 게임패드는 모두 작동합니다. PC용 비브랜드 USB 컨트롤러부터 도구를 통해 Mac에 연결된 PlayStation 2 게임패드, Chrome OS 노트북과 페어링된 블루투스 컨트롤러에 이르기까지 다양한 게임패드를 테스트했습니다.

게임패드
게임패드

다음은 '엄마, 제가 직장에서 하는 일이 정말 이거예요'라는 드로잉을 테스트하는 데 사용한 컨트롤러 사진입니다. 컨트롤러가 작동하지 않거나 컨트롤이 잘못 매핑된 경우 Chrome 또는 Firefox에 버그를 신고하세요 . (각 브라우저의 최신 버전에서 테스트하여 이미 수정되지 않았는지 확인하세요.)

Gamepad API 기능 감지<

Chrome에서 간단하게 수행할 수 있습니다.

var gamepadSupportAvailable = !!navigator.webkitGetGamepads || !!navigator.webkitGamepads;

아직 Firefox에서는 이를 감지할 수 없는 것 같습니다. 모든 것이 이벤트 기반이며 모든 이벤트 핸들러가 창에 연결되어야 하므로 이벤트 핸들러를 감지하는 일반적인 기법이 작동하지 않습니다.

하지만 일시적인 문제일 뿐입니다. 멋진 Modernizr에서 이미 Gamepad API에 관해 알려주므로 현재 및 향후 모든 감지 요구사항에 이를 사용하는 것이 좋습니다.

var gamepadSupportAvailable = Modernizr.gamepads;

연결된 게임패드 확인하기

게임패드를 연결해도 사용자가 먼저 버튼을 누르지 않으면 어떤 식으로도 표시되지 않습니다. 이는 지문 식별을 방지하기 위한 조치이지만 사용자 환경에 약간의 문제를 일으킬 수 있습니다. 사용자가 컨트롤러를 연결했는지 알 수 없으므로 사용자에게 버튼을 누르라고 요청하거나 게임패드 관련 안내를 제공할 수 없습니다.

이 장애물을 넘기면 (죄송합니다…) 더 많은 일이 기다리고 있습니다.

폴링

Chrome의 API 구현은 현재 시스템에 연결된 모든 게임패드의 목록과 함께 현재 상태 (버튼 + 스틱)를 가져오는 데 사용할 수 있는 함수인 navigator.webkitGetGamepads()를 노출합니다. 연결된 첫 번째 게임패드는 배열의 첫 번째 항목으로 반환되고, 이후 연결된 게임패드는 배열의 다음 항목으로 반환됩니다.

(이 함수 호출은 최근에 직접 액세스할 수 있는 배열인 navigator.webkitGamepads[]를 대체했습니다. 2012년 8월 초 현재 Chrome 21에서는 이 배열에 계속 액세스해야 하지만 함수 호출은 Chrome 22 이상에서 작동합니다. 앞으로는 함수 호출이 API를 사용하는 권장 방법이며 설치된 모든 Chrome 브라우저에 점진적으로 적용될 예정입니다.)

지금까지 구현된 사양의 일부를 사용하려면 상황이 변경될 때 이벤트를 실행하는 대신 연결된 게임패드의 상태를 지속적으로 확인하고 필요한 경우 이전 상태와 비교해야 합니다. requestAnimationFrame()을 사용하여 가장 효율적이고 배터리 친화적인 방식으로 폴링을 설정했습니다. Doodle의 경우 애니메이션을 지원하는 requestAnimationFrame() 루프가 이미 있지만 완전히 별개의 두 번째 루프를 만들었습니다. 코딩이 더 간단하고 성능에 영향을 미치지 않기 때문입니다.

다음은 테스터의 코드입니다.

/**
 * Starts a polling loop to check for gamepad state.
 */
startPolling: function() {
    // Don't accidentally start a second loop, man.
    if (!gamepadSupport.ticking) {
    gamepadSupport.ticking = true;
    gamepadSupport.tick();
    }
},

/**
 * Stops a polling loop by setting a flag which will prevent the next
 * requestAnimationFrame() from being scheduled.
 */
stopPolling: function() {
    gamepadSupport.ticking = false;
},

/**
 * A function called with each requestAnimationFrame(). Polls the gamepad
 * status and schedules another poll.
 */
tick: function() {
    gamepadSupport.pollStatus();
    gamepadSupport.scheduleNextTick();
},

scheduleNextTick: function() {
    // Only schedule the next frame if we haven't decided to stop via
    // stopPolling() before.
    if (gamepadSupport.ticking) {
    if (window.requestAnimationFrame) {
        window.requestAnimationFrame(gamepadSupport.tick);
    } else if (window.mozRequestAnimationFrame) {
        window.mozRequestAnimationFrame(gamepadSupport.tick);
    } else if (window.webkitRequestAnimationFrame) {
        window.webkitRequestAnimationFrame(gamepadSupport.tick);
    }
    // Note lack of setTimeout since all the browsers that support
    // Gamepad API are already supporting requestAnimationFrame().
    }
},

/**
 * Checks for the gamepad status. Monitors the necessary data and notices
 * the differences from previous state (buttons for Chrome/Firefox,
 * new connects/disconnects for Chrome). If differences are noticed, asks
 * to update the display accordingly. Should run as close to 60 frames per
 * second as possible.
 */
pollStatus: function() {
    // (Code goes here.)
},

게임패드 하나만 신경 쓰는 경우 다음과 같이 간단하게 데이터를 가져올 수 있습니다.

var gamepad = navigator.webkitGetGamepads && navigator.webkitGetGamepads()[0];

좀 더 영리하게 하거나 두 명 이상의 플레이어를 동시에 지원하려면 더 복잡한 시나리오 (2개 이상의 게임패드가 연결되어 있고 그중 일부가 중간에 연결 해제되는 등)에 반응하는 코드 몇 줄을 더 추가해야 합니다. 이 문제를 해결하는 한 가지 방법은 테스터 소스 코드pollGamepads() 함수를 살펴보는 것입니다.

이벤트

Firefox는 Gamepad API 사양에 설명된 더 나은 대체 방법을 사용합니다. 폴링을 요청하는 대신 게임패드가 연결되거나(더 정확하게는 연결되고 버튼을 눌러 '발표'됨) 연결 해제될 때마다 실행되는 두 가지 이벤트(MozGamepadConnectedMozGamepadDisconnected)를 노출합니다. 향후 상태를 계속 반영할 게임패드 객체가 이벤트 객체의 .gamepad 매개변수로 전달됩니다.

테스터 소스 코드에서

/**
 * React to the gamepad being connected. Today, this will only be executed
 * on Firefox.
 */
onGamepadConnect: function(event) {
    // Add the new gamepad on the list of gamepads to look after.
    gamepadSupport.gamepads.push(event.gamepad);

    // Start the polling loop to monitor button changes.
    gamepadSupport.startPolling();

    // Ask the tester to update the screen to show more gamepads.
    tester.updateGamepads(gamepadSupport.gamepads);
},

요약

결국 두 가지 접근 방식을 모두 지원하는 테스터의 초기화 함수는 다음과 같습니다.

/**
 * Initialize support for Gamepad API.
 */
init: function() {
    // As of writing, it seems impossible to detect Gamepad API support
    // in Firefox, hence we need to hardcode it in the third clause.
    // (The preceding two clauses are for Chrome.)
    var gamepadSupportAvailable = !!navigator.webkitGetGamepads ||
        !!navigator.webkitGamepads ||
        (navigator.userAgent.indexOf('Firefox/') != -1);

    if (!gamepadSupportAvailable) {
    // It doesn't seem Gamepad API is available – show a message telling
    // the visitor about it.
    tester.showNotSupported();
    } else {
    // Firefox supports the connect/disconnect event, so we attach event
    // handlers to those.
    window.addEventListener('MozGamepadConnected',
                            gamepadSupport.onGamepadConnect, false);
    window.addEventListener('MozGamepadDisconnected',
                            gamepadSupport.onGamepadDisconnect, false);

    // Since Chrome only supports polling, we initiate polling loop straight
    // away. For Firefox, we will only do it if we get a connect event.
    if (!!navigator.webkitGamepads || !!navigator.webkitGetGamepads) {
        gamepadSupport.startPolling();
    }
    }
},

게임패드 정보

시스템에 연결된 모든 게임패드는 다음과 같이 보이는 객체로 표시됩니다.

id: "PLAYSTATION(R)3 Controller (STANDARD GAMEPAD Vendor: 054c Product: 0268)"
index: 1
timestamp: 18395424738498
buttons: Array[8]
    0: 0
    1: 0
    2: 1
    3: 0
    4: 0
    5: 0
    6: 0.03291
    7: 0
axes: Array[4]
    0: -0.01176
    1: 0.01961
    2: -0.00392
    3: -0.01176

기본 정보

상단의 몇 가지 필드는 간단한 메타데이터입니다.

  • id: 게임패드에 대한 텍스트 설명
  • index: 하나의 컴퓨터에 연결된 여러 게임패드를 구분하는 데 유용한 정수입니다.
  • timestamp: 버튼/축 상태가 마지막으로 업데이트된 타임스탬프입니다 (현재 Chrome에서만 지원됨).

버튼 및 스틱

오늘날의 게임패드는 할아버지가 잘못된 성에서 공주를 구하기 위해 사용했을 법한 게임패드와는 다릅니다. 일반적으로 두 개의 아날로그 스틱 외에도 16개 이상의 별도의 버튼 (일부는 개별 버튼, 일부는 아날로그 버튼)이 있습니다. Gamepad API는 운영체제에서 보고하는 모든 버튼과 아날로그 스틱에 관해 알려줍니다.

게임패드 객체에서 현재 상태를 가져오면 .buttons[]를 통해 버튼에 액세스하고 .axes[] 배열을 통해 스틱에 액세스할 수 있습니다. 다음은 각 값에 해당하는 내용을 시각적으로 요약한 내용입니다.

게임패드 다이어그램
게임패드 다이어그램

사양은 브라우저에 처음 16개 버튼과 4개 축을 다음에 매핑하도록 요청합니다.

gamepad.BUTTONS = {
    FACE_1: 0, // Face (main) buttons
    FACE_2: 1,
    FACE_3: 2,
    FACE_4: 3,
    LEFT_SHOULDER: 4, // Top shoulder buttons
    RIGHT_SHOULDER: 5,
    LEFT_SHOULDER_BOTTOM: 6, // Bottom shoulder buttons
    RIGHT_SHOULDER_BOTTOM: 7,
    SELECT: 8,
    START: 9,
    LEFT_ANALOGUE_STICK: 10, // Analogue sticks (if depressible)
    RIGHT_ANALOGUE_STICK: 11,
    PAD_TOP: 12, // Directional (discrete) pad
    PAD_BOTTOM: 13,
    PAD_LEFT: 14,
    PAD_RIGHT: 15
};

gamepad.AXES = {
    LEFT_ANALOGUE_HOR: 0,
    LEFT_ANALOGUE_VERT: 1,
    RIGHT_ANALOGUE_HOR: 2,
    RIGHT_ANALOGUE_VERT: 3
};

추가 버튼과 축이 위에 있는 버튼과 축에 추가됩니다. 16개 버튼이나 4개 축이 보장되지는 않습니다. 일부 버튼이나 축이 정의되지 않을 수 있습니다.

버튼은 0.0 (누르지 않음)에서 1.0 (완전히 누름)까지의 값을 사용할 수 있습니다. 축은 -1.0 (완전히 왼쪽 또는 위)에서 0.0 (중앙)을 거쳐 1.0 (완전히 오른쪽 또는 아래)까지 이어집니다.

아날로그 또는 이산형인가요?

모든 버튼이 아날로그 버튼일 수 있습니다. 예를 들어 숄더 버튼의 경우 이는 다소 일반적입니다. 따라서 1.00과 단순히 비교하는 대신 기준점을 설정하는 것이 가장 좋습니다. 아날로그 버튼이 약간 더러워지면 어떻게 될까요? 1.00에 도달하지 않을 수도 있습니다. YouTube에서는 다음과 같이 처리합니다.

gamepad.ANALOGUE_BUTTON_THRESHOLD = .5;

gamepad.buttonPressed_ = function(pad, buttonId) {
    return pad.buttons[buttonId] &&
            (pad.buttons[buttonId] > gamepad.ANALOGUE_BUTTON_THRESHOLD);
};

아날로그 스틱을 디지털 조이스틱으로 전환하는 방법도 동일합니다. 물론 디지털 패드 (d-pad)는 항상 있지만 게임패드에는 디지털 패드가 없을 수도 있습니다. 다음은 이를 처리하는 코드입니다.

gamepad.AXIS_THRESHOLD = .75;

gamepad.stickMoved_ = function(pad, axisId, negativeDirection) {
    if (typeof pad.axes[axisId] == 'undefined') {
    return false;
    } else if (negativeDirection) {
    return pad.axes[axisId] < -gamepad.AXIS_THRESHOLD;
    } else {
    return pad.axes[axisId] > gamepad.AXIS_THRESHOLD;
    }
};

버튼 누르기 및 스틱 움직임

이벤트

비행 시뮬레이터 게임과 같이 스틱 위치나 버튼 누르기를 지속적으로 확인하고 이에 반응하는 것이 더 적합한 경우도 있지만 2012년 장애물 드 doodle과 같은 경우에는 어떨까요? 프레임마다 버튼을 확인해야 하는 이유는 무엇일까요? 키보드나 마우스 위/아래와 같은 이벤트를 가져올 수 없는 이유는 무엇인가요?

좋은 소식은 가능합니다. 나쁜 소식은 향후입니다. 사양에 있지만 아직 어떤 브라우저에도 구현되지 않았습니다.

폴링

그동안은 현재 상태와 이전 상태를 비교하고 차이가 있는 경우 함수를 호출하는 방법을 사용할 수 있습니다. 예를 들면 다음과 같습니다.

if (buttonPressed(pad, 0) != buttonPressed(oldPad, 0)) {
    buttonEvent(0, buttonPressed(pad, 0) ? 'down' : 'up');
}
for (var i in gamepadSupport.gamepads) {
    var gamepad = gamepadSupport.gamepads[i];

    // Don't do anything if the current timestamp is the same as previous
    // one, which means that the state of the gamepad hasn't changed.
    // This is only supported by Chrome right now, so the first check
    // makes sure we're not doing anything if the timestamps are empty
    // or undefined.
    if (gamepadSupport.prevTimestamps[i] &&
        (gamepad.timestamp == gamepadSupport.prevTimestamps[i])) {
    continue;
    }
    gamepadSupport.prevTimestamps[i] = gamepad.timestamp;

    gamepadSupport.updateDisplay(i);
}

2012년 장애물 기념일 로고의 키보드 우선 접근 방식

게임패드가 없으면 오늘의 두들의 기본 입력 방법이 키보드이므로 게임패드가 키보드를 매우 근접하게 에뮬레이션하도록 결정했습니다. 이는 다음과 같은 세 가지 결정을 의미합니다.

  1. 두들에는 달리기용 버튼 2개와 점프용 버튼 1개 등 3개의 버튼만 필요하지만 게임패드에는 더 많은 버튼이 있을 수 있습니다. 따라서 알려진 16개 버튼과 2개의 스틱을 모두 세 가지 논리적 함수에 가장 적절하다고 생각되는 방식으로 매핑하여 사용자가 A/B 버튼을 번갈아 누르거나, 어깨 버튼을 번갈아 누르거나, D-패드에서 왼쪽/오른쪽을 누르거나, 스틱을 왼쪽과 오른쪽으로 세게 흔들면 달릴 수 있도록 했습니다. 물론 그중 일부는 다른 것보다 더 효율적일 것입니다. 예를 들면 다음과 같습니다.

    newState[gamepad.STATES.LEFT] =
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.PAD_LEFT) ||
        gamepad.stickMoved_(pad, gamepad.AXES.LEFT_ANALOGUE_HOR, true) ||
        gamepad.stickMoved_(pad, gamepad.AXES.RIGHT_ANALOGUE_HOR, true),
    
    newState[gamepad.STATES.PRIMARY_BUTTON] =
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.FACE_1) ||
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.LEFT_SHOULDER) ||
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.LEFT_SHOULDER_BOTTOM) ||
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.SELECT) ||
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.START) ||
        gamepad.buttonPressed_(pad, gamepad.BUTTONS.LEFT_ANALOGUE_STICK),
    
  2. 앞서 설명한 임곗값 함수를 사용하여 각 아날로그 입력을 불연속 입력으로 취급했습니다.

  3. 게임패드 입력을 베이킹하는 대신 드 doodle에 고정했습니다. 폴링 루프는 실제로 필요한 keydown 및 keyup 이벤트를 적절한 keyCode와 함께 합성하여 DOM으로 다시 전송합니다.

    // Create and dispatch a corresponding key event.
    var event = document.createEvent('Event');
    var eventName = down ? 'keydown' : 'keyup';
    event.initEvent(eventName, true, true);
    event.keyCode = gamepad.stateToKeyCodeMap_[state];
    gamepad.containerElement_.dispatchEvent(event);

이제 완료되었습니다.

도움말 및 유용한 정보

  • 버튼을 누르기 전에는 게임패드가 브라우저에 전혀 표시되지 않습니다.
  • 여러 브라우저에서 동시에 게임패드를 테스트하는 경우 그중 하나만 컨트롤러를 감지할 수 있습니다. 이벤트가 수신되지 않으면 이를 사용 중일 수 있는 다른 페이지를 닫으세요. 또한 경험에 비추어 볼 때 탭을 닫거나 브라우저 자체를 종료해도 브라우저가 게임패드를 '유지'하는 경우가 있습니다. 시스템을 다시 시작하는 것이 문제를 해결하는 유일한 방법일 때도 있습니다.
  • 항상 Chrome Canary 및 다른 브라우저의 상응하는 버전을 사용하여 최상의 지원을 받으세요. 그런 다음 이전 버전의 동작이 다르다면 적절하게 조치를 취하세요.

향후 나아갈 방향

이 도움말이 아직은 불안정하지만 이미 재미있는 이 새로운 API를 이해하는 데 도움이 되기를 바랍니다.

누락된 API (예: 이벤트)와 더 광범위한 브라우저 지원 외에도 궁극적으로 럼블 제어, 내장 자이로스코프 액세스 등의 기능을 지원하고자 합니다. 또한 다양한 유형의 게임패드에 대한 지원도 확대하고자 합니다. 잘못 작동하거나 전혀 작동하지 않는 게임패드를 발견하면 Chrome에 버그를 신고하거나 Firefox에 버그를 신고해 주세요.

하지만 그 전에 Hurdles 2012 Doodle을 플레이해 보고 게임패드에서 얼마나 더 재미있는지 확인해 보세요. 10.7초보다 더 빨리 할 수 있다고 하셨나요? 한판 붙어볼까요?

추가 자료