運用 Gamepad API 突破瓶頸

Marcin Wichary
Marcin Wichary

簡介

讓新手在冒險遊戲中使用鍵盤、在水果切割遊戲中使用珍貴的多點觸控手指,以及在最新的動作感應器中假裝自己能像麥可傑克森一樣跳舞。(新聞快訊:他們無法這樣做)。但你不一樣。你比較好。你是專家。您可以透過手邊的遊戲搖桿,開始和結束遊戲。

等等,您是否想在網頁應用程式中支援遊戲控制器?答案是不需要。全新的 Gamepad API 可解決這個問題,讓您使用 JavaScript 讀取電腦上任何 Gamepad 控制器的狀態。這項功能是本週才推出的 Chrome 21 新功能,目前 Firefox 也即將支援這項功能 (目前可透過特殊版本使用)。

結果我們最近在 2012 年障礙賽 Google Doodle 中,有機會使用這項功能。本文將簡要說明我們如何將 Gamepad API 新增至塗鴉,以及過程中學到的事。

2012 年 Google Doodle 的跨欄賽
2012 年 Google Doodle 的障礙

遊戲手把測試人員

雖然互動式塗鴉的生命週期很短,但其背後的運作機制通常相當複雜。為了更容易說明我們要談的內容,我們從塗鴉中取得了遊戲控制器程式碼,並整合了簡單的遊戲控制器測試器。您可以使用它來檢查 USB 遊戲控制器是否正常運作,也可以查看底層細節,瞭解如何運作。

目前有哪些瀏覽器支援這項功能?

瀏覽器支援

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

資料來源

可以使用哪些遊戲控制器?

一般來說,任何系統原生支援的新型遊戲控制器都應該可以運作。我們在電腦上測試了各種非品牌 USB 控制器,包括透過轉接器連接至 Mac 的 PlayStation 2 遊戲控制器,以及與 ChromeOS 筆記型電腦配對的藍牙控制器。

遊戲手把
遊戲控制器

這是我們用來測試塗鴉的控制器相片。如果控制器無法運作,或控制項對應不正確,請向 Chrome 回報錯誤向 Firefox 回報錯誤。(請使用各瀏覽器的最新版本進行測試,確認問題是否已修正)。

功能偵測 Gamepad API<

在 Chrome 中輕鬆完成:

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

目前似乎無法在 Firefox 中偵測這項問題,因為所有內容都是以事件為基礎,且所有事件處理常式都必須附加至視窗,這會導致偵測事件處理常式的典型技巧無法運作。

但我們相信這只是暫時性的。超棒的 Modernizr 已說明 Gamepad API,因此我們建議您在目前和未來的所有偵測需求中使用這個工具:

var gamepadSupportAvailable = Modernizr.gamepads;

瞭解已連結的遊戲控制器

即使您已連接遊戲控制器,除非使用者先按下任何按鈕,否則遊戲控制器不會以任何方式顯示。這麼做可防止指紋辨識,但對使用者體驗來說,這有點困難:您無法要求使用者按下按鈕,也無法提供特定遊戲控制器的指示,因為您不知道使用者是否已連接控制器。

不過,解決這個問題後,還有更多工作要做 (抱歉…)。

意見調查

Chrome 的 API 實作會公開一個函式 navigator.webkitGetGamepads(),您可以使用這個函式取得目前插入系統的所有遊戲控制器清單,以及這些遊戲控制器的目前狀態 (按鈕 + 搖桿)。系統會將第一個連線的遊戲控制器傳回為陣列中的第 1 個項目,依此類推。

(這個函式呼叫最近取代了您可以直接存取的陣列 – navigator.webkitGamepads[]。截至 2012 年 8 月初,在 Chrome 21 中仍需存取這個陣列,但函式呼叫在 Chrome 22 以上版本中有效。日後,我們建議使用函式呼叫來使用 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 規格中所述的替代方法,這項方法更為理想。Firefox 不會要求您執行輪詢,而是會公開兩個事件 (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)

按鈕和搖桿

現今的遊戲控制器與你祖父可能用來拯救公主 (在錯誤的城堡中) 的遊戲控制器不太一樣,因為它們通常至少有十六個獨立按鈕 (有些是獨立按鈕,有些是類比按鈕),以及兩個類比搖桿。Gamepad API 會告知您作業系統回報的所有按鈕和類比搖桿。

取得 Gamepad 物件中的目前狀態後,您可以透過 .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
};

系統會將額外的按鈕和軸附加到上述按鈕和軸。請注意,系統不保證會提供十六個按鈕或四個軸線,因此請做好準備,以便處理其中部分未定義的按鈕或軸線。

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

模擬或離散?

理論上,每個按鈕都可以是類比按鈕,例如肩側按鈕。因此,建議您設定閾值,而非直接將其與 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-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 年障礙賽」塗鴉遊戲來說,您可能會想:為什麼我需要在每個影格中檢查按鈕?為什麼我無法取得與鍵盤或滑鼠上/下操作相同的事件?

好消息是,您可以這麼做。壞消息是,這項功能已納入規格,但尚未在任何瀏覽器中實作。

意見調查

在此同時,您可以比較目前和先前的狀態,並在發現差異時呼叫函式。例如:

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 年障礙賽 Doodle 中以鍵盤為優先的做法

由於沒有遊戲控制器,今天的塗鴉遊戲偏好的輸入方式是鍵盤,因此我們決定讓遊戲控制器模擬鍵盤的操作方式。這代表做出三項決定:

  1. 塗鴉遊戲只需要三個按鈕 (兩個用於跑步,一個用於跳躍),但遊戲搖桿可能會有更多按鈕。因此,我們以我們認為最合理的方式,將所有已知的 16 個按鈕和 2 個搖桿對應至這三個邏輯功能,讓使用者可以透過以下方式執行:交替按下 A/B 按鈕、交替按下肩鈕、按下方向鍵的左/右鍵,或快速左右擺動任一搖桿 (當然,有些方式會比其他方式更有效率)。例如:

    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);

就是這麼簡單!

提示與秘訣

  • 請注意,在按下按鈕之前,瀏覽器中不會顯示遊戲控制器。
  • 如果您同時在不同瀏覽器中測試遊戲控制器,請注意,只有其中一個瀏覽器可以感應控制器。如果您沒有收到任何事件,請務必關閉可能使用該事件的其他網頁。此外,根據我們的經驗,即使關閉分頁或退出瀏覽器,瀏覽器有時仍可能「保留」控制器。有時,重新啟動系統是解決問題的唯一方法。
  • 如往常一樣,請使用 Chrome Canary 和其他瀏覽器的等效版本,確保您能獲得最佳支援。如果您發現舊版的行為有所不同,請採取適當行動。

未來

希望這篇文章能讓您對這項新 API 有更深入的瞭解。雖然這項 API 仍有待改進之處,但已經非常好玩了。

除了缺少的 API 部分 (例如事件) 和更廣泛的瀏覽器支援之外,我們也希望最終能看到震動控制、存取內建陀螺儀等功能。此外,我們也希望能支援更多類型的遊戲控制器。如果您發現某個遊戲控制器無法正常運作或完全無法運作,請向 Chrome 回報錯誤和/或向 Firefox 回報錯誤

不過,在開始玩之前,先來玩玩看 2012 年障礙賽塗鴉,體驗一下遊戲搖桿的樂趣。哦,你剛剛說可以跑得比 10.7 秒更快嗎?拿來。

延伸閱讀