簡介
讓新手在冒險遊戲中使用鍵盤、在水果切割遊戲中使用珍貴的多點觸控手指,以及在最新的動作感應器中假裝自己能像麥可傑克森一樣跳舞。(新聞快訊:他們無法這樣做)。但你不一樣。你比較好。你是專家。您可以透過手邊的遊戲搖桿,開始和結束遊戲。
等等,您是否想在網頁應用程式中支援遊戲控制器?答案是不需要。全新的 Gamepad API 可解決這個問題,讓您使用 JavaScript 讀取電腦上任何 Gamepad 控制器的狀態。這項功能是本週才推出的 Chrome 21 新功能,目前 Firefox 也即將支援這項功能 (目前可透過特殊版本使用)。
結果我們最近在 2012 年障礙賽 Google Doodle 中,有機會使用這項功能。本文將簡要說明我們如何將 Gamepad API 新增至塗鴉,以及過程中學到的事。
遊戲手把測試人員
雖然互動式塗鴉的生命週期很短,但其背後的運作機制通常相當複雜。為了更容易說明我們要談的內容,我們從塗鴉中取得了遊戲控制器程式碼,並整合了簡單的遊戲控制器測試器。您可以使用它來檢查 USB 遊戲控制器是否正常運作,也可以查看底層細節,瞭解如何運作。
目前有哪些瀏覽器支援這項功能?
可以使用哪些遊戲控制器?
一般來說,任何系統原生支援的新型遊戲控制器都應該可以運作。我們在電腦上測試了各種非品牌 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 不會要求您執行輪詢,而是會公開兩個事件 (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)
按鈕和搖桿
現今的遊戲控制器與你祖父可能用來拯救公主 (在錯誤的城堡中) 的遊戲控制器不太一樣,因為它們通常至少有十六個獨立按鈕 (有些是獨立按鈕,有些是類比按鈕),以及兩個類比搖桿。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 中以鍵盤為優先的做法
由於沒有遊戲控制器,今天的塗鴉遊戲偏好的輸入方式是鍵盤,因此我們決定讓遊戲控制器模擬鍵盤的操作方式。這代表做出三項決定:
塗鴉遊戲只需要三個按鈕 (兩個用於跑步,一個用於跳躍),但遊戲搖桿可能會有更多按鈕。因此,我們以我們認為最合理的方式,將所有已知的 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),
我們使用先前所述的閾值函式,將每個類比輸入都視為離散輸入。
我們將遊戲控制器輸入內容綁定到塗鴉上,而不是將其內建於其中。我們的輪詢迴圈實際上會合成必要的 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 秒更快嗎?拿來。