運用 Gamepad API 突破瓶頸

馬爾辛.維恰裡 (Marcin Wichary)
Marcin Wichary

引言

新奇玩家除了用鍵盤玩冒險遊戲,還將珍貴的多點觸控式水果取向,配上精美的全新動作感應器,假裝自己能像麥可傑克森一樣跳舞(最新消息:他們無法辦到)。但你截然不同。你真厲害!您是專業人員。對自己來說,在開始和結束比賽之前,你會先拿到遊戲手把。

等一下。想在網頁應用程式中支援遊戲手把嗎?然而今非昔比。全新的 Gamepad API 是救援手段的基石,讓您能夠使用 JavaScript 讀取與電腦連接的任何遊戲手把控制器狀態。從媒體評比到來,這項功能在上週才推出,而且只在 Chrome 21 版推出,而 Firefox 也持續提供更完善的支援 (目前正處於特殊版本中)。

從這裡開始,就是我們相當寶貴的時間,最近我們有機會在 2012 年 Hurdles 2012 Google Doodle 中使用。本文會簡單說明我們如何在 Doodle 中加入 Gamepad API,以及我們在過程中學到的內容。

2012 年跨欄 Google Doodle
2012 年世界盃 Google Doodle

遊戲手把測試人員

相對而言,互動式 Doodle 作品的創作內容往往相當複雜。為了方便示範,我們從 Doodle 中拿到遊戲手把程式碼,再設計出一個簡單的遊戲手把測試程式。你可以透過它查看 USB 遊戲手把是否正常運作,還能查看引擎蓋子的裝扮。

目前哪些瀏覽器支援本功能?

瀏覽器支援

  • 21
  • 12
  • 29
  • 10.1

資料來源

可以使用哪些遊戲搖桿?

一般來說,只要是系統原生支援的新式遊戲手把應該都能正常運作。我們測試各種遊戲控制器在電腦上使用非品牌 USB 控制器,並透過連接器與 Mac 連接的 PlayStation 2 遊戲手把,與藍牙控制器與 Chrome OS 筆記本配對。

遊戲搖桿
遊戲搖桿

這是我們用來測試 Doodle 的遙控器相片:「是的,媽媽,我就是在工作。」如果您的控制器無法正常運作,或是對應設定有誤,請回報 ChromeFirefox 的錯誤。(請使用每個瀏覽器最新的版本進行測試,確認該版本是否尚未修正)。

可偵測 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(),以最有效、最省電的方式設定輪詢作業。雖然我們已使用 requestAnimationFrame() 迴圈支援動畫,但在這次的 Doodle 版本中,我們建立第二個完全獨立的迴圈,比較容易編寫程式碼,也不會影響效能。

以下是測試人員提供的程式碼:

/**
 * 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 個按鈕和四個軸對應至:

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 個按鈕和四個軸可保證,其中部分按鈕可供明確定義。

按鈕的值可以從 0.0 (未按下) 到 1.0 (完全按下)。軸從 -1.0 (完全向左或向上) 到 0.0 (中間) 到 1.0 (完全向右或向下)。

類比或離散?

當然,每個按鈕都有類比按鈕,這對於肩膀按鈕而言很常見。因此,最好設定門檻,而不是只比較 1.00 這個門檻 (如果類比按鈕似乎有點髒?可能永遠不會達到 1.00)。在我們的 Doodle 中,我們這麼做的方式如下:

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 年 Hurdles 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. 這款 Doodle 遊戲只需要三個按鈕,兩個按鈕用於跑步和跳躍,但遊戲手把多可能有多種。因此,我們以最合理的方式,將十六個已知的按鈕和兩個已知的棒子對應到這三種邏輯功能,讓使用者可以運用以下方式執行:交替 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 回報錯誤

在此之前,不妨先試試 2012 年 Hurdles 2012 Doodle,看看這款遊戲有多有趣。對了,你剛才說過你會做得更好,超過 10.7 秒嗎?放馬過來吧!

延伸閱讀