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

마르신 위차리
Marcin Wichary

소개

초보자가 어드벤처 게임을 할 때는 키보드, 과일 자르기를 위한 소중한 만능 손가락 끝, 마이클 잭슨처럼 춤을 출 수 있을 척을 하는 참신한 모션 센서를 사용해 보세요. (Newsflash: 불가능합니다.) 하지만 여러분은 다릅니다. 나아졌어. 프로의 경지에요. 게임은 손에 든 게임패드를 두고 시작하고 끝납니다.

하지만 잠시만요. 웹 앱에서 게임패드를 지원하려고 하는데 좋지 않은가요? 이제 더 이상 그렇지 않습니다. 새로운 Gamepad API를 사용하면 자바스크립트를 사용하여 컴퓨터에 연결된 게임패드 컨트롤러의 상태를 읽을 수 있습니다. 최근 출시된 이 기능은 지난 주에 Chrome 21에서 사용할 수 있을 뿐 아니라 Firefox에서도 곧 지원될 예정입니다 (현재 특별 빌드로 사용 가능).

최근 Hurdles 2012 Google 기념일 로고에서 이 기념일 로고를 사용할 수 있는 기회를 얻었기 때문에, 매우 좋은 기회였습니다. 이 글에서는 Gamepad API를 기념일 로고에 추가한 방법과 이 과정에서 배운 내용을 간략하게 설명합니다.

허들 2012 Google 기념일 로고
Hurdles 2012 Google 기념일 로고

게임패드 테스터

임시적인 대화형 기념일 로고는 내부적으로 꽤 복잡한 경향이 있습니다. 내용을 더 쉽게 보여줄 수 있도록 기념일 로고에서 게임패드 코드를 가져와 간단한 게임패드 테스터를 구성했습니다. 이 대시보드에서 USB 게임패드가 제대로 작동하는지 확인할 수 있으며, 작동 방식을 확인할 수도 있습니다.

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

브라우저 지원

  • 21
  • 12
  • 29
  • 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[]을 대체했습니다. 함수 호출은 Chrome 22 이상에서 작동하지만 2012년 8월 초 현재 Chrome 21에서도 이 배열에 액세스해야 합니다. 앞으로는 API를 사용하기 위한 권장 방법이 함수 호출이 될 것이며, 설치된 모든 Chrome 브라우저로 서서히 확대될 것입니다.)

사양에서 조금 더 구현된 부분에서는 상황이 변경될 때 이벤트를 실행하는 대신 연결된 게임패드의 상태를 지속적으로 확인하고 필요한 경우 이전 버전과 비교해야 합니다. 우리는 requestAnimationFrame()을 사용하여 가장 효율적이고 배터리 친화적인 방식으로 폴링을 설정했습니다. 기념일 로고의 경우 이미 애니메이션을 지원하는 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];

조금 더 똑똑해지고 싶거나 두 명 이상의 플레이어를 동시에 지원하려면 코드 몇 줄을 더 추가하여 더 복잡한 시나리오 (두 개 이상의 게임패드가 연결되고 일부는 중간에 연결이 끊어지는 등)에 반응해야 합니다. 이 문제를 해결하는 한 가지 방법은 테스터의 소스 코드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 (완전히 오른쪽 또는 아래)까지 이동합니다.

아날로그 vs. 불연성

겉보기에는 모든 버튼이 아날로그 버튼일 수 있습니다. 예를 들어 숄더 버튼처럼 일반적인 버튼입니다. 따라서 임곗값을 1.00과 직설적으로 비교하는 것보다는 임곗값을 설정하는 것이 좋습니다 (아날로그 버튼이 약간 더러워지면 어떻게 될까요? 1.00에 도달하지 못할 수도 있음). 기념일 로고에서는 다음과 같은 방식으로 작동합니다.

gamepad.ANALOGUE_BUTTON_THRESHOLD = .5;

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

같은 방법으로 아날로그 스틱을 디지털 조이스틱으로 전환할 수 있습니다. 물론입니다. 항상 디지털 패드 (D패드)가 있지만 게임패드에는 없을 수도 있습니다. 이 문제를 처리하는 코드는 다음과 같습니다.

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 기념일 로고 같은 경우에는? 프레임마다 버튼을 확인해야 하는 이유가 무엇인지 궁금할 수 있습니다. 키보드 또는 마우스 위/아래와 같은 이벤트를 가져올 수 없는 이유는 무엇인가요?

다행히도 가능합니다. 나쁜 소식은 다가올 미래입니다. 사양에 나와 있지만 아직 브라우저에서 구현되지 않았습니다.

폴링

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

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. 기념일 로고는 버튼 3개(달리기 버튼 2개와 점프용 버튼 1개)만 있으면 되지만, 게임패드에는 버튼이 더 많을 수 있습니다. 따라서 우리는 알려진 16개의 버튼과 2개의 알려진 스틱을 3가지 논리적 기능에 가장 합리적인 방식으로 매핑하여 사람들이 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. 우리는 게임패드 입력을 낙서에 끼워 넣는 대신에 낙서에 고정하는 데까지 왔습니다. 폴링 루프는 실제로 필요한 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);

그러면 끝입니다.

도움말 및 유용한 정보

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

향후 나아갈 방향

조금 까다롭기는 하지만 이미 많은 재미를 선사하고 있는 이 새로운 API를 조명하는 데 도움이 되기를 바랍니다.

API에서 누락된 부분 (예: 이벤트)과 광범위한 브라우저 지원 외에도 럼블 컨트롤, 내장 자이로스코프 액세스 등 다양한 기능이 지원될 예정입니다. Chrome에 버그를 신고하고, 제대로 작동하지 않거나 전혀 작동하지 않는 버그를 발견하면 Chrome에 버그를 신고해 주세요.

그 전에 Hurdles 2012 기념일 로고를 사용해 보고 게임패드에서 얼마나 더 재미있었는지 확인해 보세요. 아, 10.7초보다 더 잘할 수 있다고 하셨나요? 준비하세요.

추가 자료