引言
新奇玩家除了用鍵盤玩冒險遊戲,還將珍貴的多點觸控式水果取向,配上精美的全新動作感應器,假裝自己能像麥可傑克森一樣跳舞(最新消息:他們無法辦到)。但你截然不同。你真厲害!您是專業人員。對自己來說,在開始和結束比賽之前,你會先拿到遊戲手把。
等一下。想在網頁應用程式中支援遊戲手把嗎?然而今非昔比。全新的 Gamepad API 是救援手段的基石,讓您能夠使用 JavaScript 讀取與電腦連接的任何遊戲手把控制器狀態。從媒體評比到來,這項功能在上週才推出,而且只在 Chrome 21 版推出,而 Firefox 也持續提供更完善的支援 (目前正處於特殊版本中)。
從這裡開始,就是我們相當寶貴的時間,最近我們有機會在 2012 年 Hurdles 2012 Google Doodle 中使用。本文會簡單說明我們如何在 Doodle 中加入 Gamepad API,以及我們在過程中學到的內容。
遊戲手把測試人員
相對而言,互動式 Doodle 作品的創作內容往往相當複雜。為了方便示範,我們從 Doodle 中拿到遊戲手把程式碼,再設計出一個簡單的遊戲手把測試程式。你可以透過它查看 USB 遊戲手把是否正常運作,還能查看引擎蓋子的裝扮。
目前哪些瀏覽器支援本功能?
可以使用哪些遊戲搖桿?
一般來說,只要是系統原生支援的新式遊戲手把應該都能正常運作。我們測試各種遊戲控制器在電腦上使用非品牌 USB 控制器,並透過連接器與 Mac 連接的 PlayStation 2 遊戲手把,與藍牙控制器與 Chrome OS 筆記本配對。
這是我們用來測試 Doodle 的遙控器相片:「是的,媽媽,我就是在工作。」如果您的控制器無法正常運作,或是對應設定有誤,請回報 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(),以最有效、最省電的方式設定輪詢作業。雖然我們已使用 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 規格中更妥善地描述的替代方法。它會顯示 MozGamepadConnected
和 MozGamepadDisconnected
這兩個事件,不會在遊戲鍵盤插入 (或按下任何按鈕以更精確地插入並「通知」) 或未插電時觸發,而不會要求您輪詢。將繼續反映未來狀態的遊戲手把物件會傳遞做為事件物件的 .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 年世界盃足球賽中,以鍵盤為主的方法
由於沒有遊戲手把,今天我們慣用的輸入方式就是鍵盤,因此我們決定讓遊戲手把更接近。這意味著有三項決定:
這款 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),
我們使用前述的閾值函式,將每個類比輸入內容視為離散輸入內容。
我們甚至將遊戲手把輸入內容整合到 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 秒嗎?放馬過來吧!