简介
让新手们继续用键盘玩冒险游戏,用他们宝贵的多点触控指尖切水果,用他们新奇时髦的移动传感器假装自己能像迈克尔·杰克逊一样跳舞。(最新消息:他们无法做到这一点。)但你不一样。你更优秀。您真是个行家。对你来说,游戏始于手握游戏手柄,也终于手握游戏手柄。
但等等。如果您想在 Web 应用中支持游戏手柄,是不是就没戏了?现在不需要了。全新的 Gamepad API 应运而生,让您可以使用 JavaScript 读取连接到计算机的任何游戏手柄控制器的状态。它非常新,上周才在 Chrome 21 中推出,并且即将获得 Firefox 的支持(目前在特殊 build 中提供)。
事实证明,这个时机非常好,因为我们最近有机会在 2012 年跨栏 Google 涂鸦中使用了它。本文将简要介绍我们如何将 Gamepad API 添加到 Doodle 中,以及在此过程中学到的知识。
游戏手柄测试工具
虽然互动式涂鸦转瞬即逝,但其背后的技术往往相当复杂。为了更轻松地演示我们所讨论的内容,我们从涂鸦中提取了游戏手柄代码,并组合了一个简单的游戏手柄测试器。您可以使用它来查看 USB 游戏手柄是否正常工作,还可以查看其内部机制,了解其工作原理。
目前有哪些浏览器支持该功能?
可以使用哪些游戏手柄?
一般来说,只要是系统原生支持的现代游戏手柄,应该都可以使用。我们测试了各种游戏手柄,包括通过 PC 连接的杂牌 USB 控制器、通过加密狗连接到 Mac 的 PlayStation 2 游戏手柄,以及与 ChromeOS 笔记本配对的蓝牙控制器。
这张照片展示了我们用来测试涂鸦的一些控制器 -“是的,妈妈,这就是我的工作。”如果您的控制器无法正常工作,或者控制器的映射不正确,请针对 Chrome 或 Firefox 提交 bug。(请在每个浏览器的最新版本中进行测试,以确保问题尚未得到解决。)
检测 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,并且该 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 规范中描述的另一种更好的方法。它不会要求您进行轮询,而是公开两个事件: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 个按钮和 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(完全向右或向下)。
模拟还是离散?
表面上,每个按钮都可以是模拟按钮,例如,肩部按钮通常就是模拟按钮。因此,最好设置一个阈值,而不是直接与 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);
};
您也可以通过同样的方式将模拟摇杆转换为数字摇杆。当然,您始终可以使用数字方向键(方向键),但您的游戏手柄可能没有方向键。以下是处理该情况的代码:
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 的缺失部分(例如事件)和更广泛的浏览器支持之外,我们还希望最终能看到震动控制、内置陀螺仪访问等功能。此外,我们还希望看到对不同类型游戏手柄的更多支持 - 如果您发现某个游戏手柄无法正常工作或根本无法工作,请针对 Chrome 提交 bug 和/或针对 Firefox 提交 bug。
不过在此之前,不妨先去玩玩我们的跨栏 2012 涂鸦,看看用游戏手柄玩有多好玩。哦,你刚才说你能跑进 10.7 秒?来吧!