Introduction
Laissons les novices utiliser leur clavier pour les jeux d'aventure, leurs précieux doigts pour couper des fruits et leurs nouveaux capteurs de mouvement pour faire semblant de danser comme Michael Jackson. (Spoiler : ils ne le peuvent pas.) Mais vous êtes différent. Vous êtes meilleur. Vous êtes un(e) pro. Pour vous, les jeux commencent et se terminent avec une manette entre les mains.
Mais attendez. N'est-il pas impossible de prendre en charge une manette de jeu dans votre application Web ? Plus maintenant. La toute nouvelle API Gamepad vient à la rescousse, vous permettant d'utiliser JavaScript pour lire l'état de n'importe quelle manette de jeu connectée à votre ordinateur. Cette fonctionnalité est tellement récente qu'elle n'est disponible que depuis la semaine dernière dans Chrome 21. Elle est également sur le point d'être compatible avec Firefox (actuellement disponible dans une version spéciale).
Le timing était parfait, car nous avons pu l'utiliser récemment dans le doodle Google Course de haies 2012. Cet article explique brièvement comment nous avons ajouté l'API Gamepad au doodle et ce que nous avons appris au cours de ce processus.
Testeur de manette de jeu
Bien qu'ils soient éphémères, les doodles interactifs sont généralement assez complexes en coulisses. Pour illustrer plus facilement notre propos, nous avons pris le code de la manette de jeu du doodle et avons créé un testeur de manette de jeu simple. Vous pouvez l'utiliser pour vérifier si votre manette de jeu USB fonctionne correctement, et examiner comment elle est conçue.
Quels navigateurs sont compatibles aujourd'hui ?
Quelles manettes de jeu puis-je utiliser ?
En général, toute manette de jeu moderne compatible de manière native avec votre système devrait fonctionner. Nous avons testé différentes manettes de jeu, des manettes USB sans marque sur un PC aux manettes Bluetooth associées à un ordinateur portable Chrome OS, en passant par les manettes PlayStation 2 connectées via un dongle à un Mac.
Voici une photo de certains des contrôleurs que nous avons utilisés pour tester notre doodle. "Oui, maman, c'est vraiment ce que je fais au travail." Si votre manette ne fonctionne pas ou si les commandes ne sont pas mappées correctement, veuillez signaler un bug à Chrome ou Firefox . (Veuillez tester la toute dernière version de chaque navigateur pour vous assurer que le problème n'a pas déjà été résolu.)
Détection de la compatibilité avec l'API Gamepad<
Dans Chrome :
var gamepadSupportAvailable = !!navigator.webkitGetGamepads || !!navigator.webkitGamepads;
Il ne semble pas encore possible de détecter cela dans Firefox. Tout est basé sur les événements, et tous les gestionnaires d'événements doivent être associés à la fenêtre, ce qui empêche le fonctionnement d'une technique classique de détection des gestionnaires d'événements.
Mais nous sommes sûrs que ce n'est que temporaire. L'excellent Modernizr vous informe déjà sur l'API Gamepad. Nous vous le recommandons donc pour tous vos besoins de détection actuels et futurs :
var gamepadSupportAvailable = Modernizr.gamepads;
Recherche de manettes de jeu connectées…
Même si vous connectez la manette de jeu, elle ne se manifestera d'aucune manière, sauf si l'utilisateur appuie d'abord sur l'un de ses boutons. Cela permet d'éviter l'empreinte numérique, mais cela s'avère un peu difficile pour l'expérience utilisateur : vous ne pouvez pas demander à l'utilisateur d'appuyer sur le bouton ni lui fournir des instructions spécifiques à la manette de jeu, car vous ne savez pas s'il a connecté sa manette.
Une fois que vous aurez franchi cet obstacle (désolé…), d'autres vous attendent.
Sondages
L'implémentation de l'API par Chrome expose une fonction, navigator.webkitGetGamepads(), que vous pouvez utiliser pour obtenir la liste de toutes les manettes de jeu actuellement branchées au système, ainsi que leur état actuel (boutons et sticks). La première manette de jeu connectée sera renvoyée en tant que première entrée du tableau, et ainsi de suite.
(Cet appel de fonction a récemment remplacé un tableau auquel vous pouviez accéder directement : navigator.webkitGamepads[]. Depuis début août 2012, l'accès à ce tableau est toujours nécessaire dans Chrome 21, tandis que l'appel de fonction fonctionne dans Chrome 22 et les versions ultérieures. À l'avenir, l'appel de fonction sera la méthode recommandée pour utiliser l'API, et il sera progressivement déployé sur tous les navigateurs Chrome installés.)
La partie de la spécification implémentée jusqu'à présent nécessite de vérifier en continu l'état des manettes de jeu connectées (et de le comparer à l'état précédent si nécessaire), au lieu de déclencher des événements lorsque des changements se produisent. Nous avons utilisé requestAnimationFrame() pour configurer l'interrogation de la manière la plus efficace et la plus économe en batterie. Pour notre doodle, même si nous disposons déjà d'une boucle requestAnimationFrame() pour prendre en charge les animations, nous en avons créé une deuxième complètement distincte. C'était plus simple à coder et cela ne devrait pas affecter les performances.
Voici le code du testeur :
/**
* 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.)
},
Si vous ne vous intéressez qu'à une seule manette de jeu, vous pouvez obtenir ses données de la manière suivante :
var gamepad = navigator.webkitGetGamepads && navigator.webkitGetGamepads()[0];
Si vous souhaitez être un peu plus astucieux ou prendre en charge plusieurs joueurs simultanément, vous devrez ajouter quelques lignes de code supplémentaires pour réagir à des scénarios plus complexes (deux manettes de jeu ou plus connectées, certaines se déconnectant en cours de route, etc.). Vous pouvez consulter le code source de notre testeur, la fonction pollGamepads(), pour découvrir une approche permettant de résoudre ce problème.
Événements
Firefox utilise une autre méthode, plus efficace, décrite dans la spécification de l'API Gamepad. Au lieu de vous demander d'interroger, il expose deux événements, MozGamepadConnected et MozGamepadDisconnected, qui sont déclenchés chaque fois qu'une manette de jeu est branchée (ou, plus précisément, branchée et "annoncée" en appuyant sur l'un de ses boutons) ou débranchée. L'objet manette de jeu qui continuera à refléter son état futur est transmis en tant que paramètre .gamepad de l'objet d'événement.
À partir du code source du testeur :
/**
* 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);
},
Résumé
Au final, notre fonction d'initialisation dans le testeur, qui accepte les deux approches, se présente comme suit :
/**
* 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();
}
}
},
Informations sur la manette de jeu
Chaque manette de jeu connectée au système sera représentée par un objet ressemblant à ceci :
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
Informations générales
Les premiers champs sont des métadonnées simples :
id: description textuelle de la manette de jeuindex: un entier permettant de distinguer les différentes manettes de jeu connectées à un ordinateurtimestamp: code temporel de la dernière mise à jour de l'état du bouton/des axes (actuellement compatible uniquement avec Chrome)
Boutons et joysticks
Les manettes de jeu d'aujourd'hui ne sont pas exactement celles que votre grand-père utilisait pour sauver la princesse dans le mauvais château. Elles comportent généralement au moins 16 boutons distincts (certains discrets, d'autres analogiques), en plus de deux sticks analogiques. L'API Gamepad vous informe sur tous les boutons et sticks analogiques signalés par le système d'exploitation.
Une fois que vous avez obtenu l'état actuel de l'objet de la manette de jeu, vous pouvez accéder aux boutons via les tableaux .buttons[] et aux sticks via les tableaux .axes[]. Voici un résumé visuel de ce à quoi ils correspondent :
La spécification demande au navigateur de mapper les seize premiers boutons et les quatre axes sur :
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
};
Les boutons et axes supplémentaires seront ajoutés à ceux ci-dessus. Veuillez noter que ni les seize boutons ni les quatre axes ne sont garantis. Préparez-vous à ce que certains d'entre eux ne soient tout simplement pas définis.
Les boutons peuvent prendre des valeurs comprises entre 0,0 (non enfoncé) et 1,0 (complètement enfoncé). Les axes vont de -1,0 (complètement à gauche ou en haut) à 1,0 (complètement à droite ou en bas), en passant par 0,0 (centre).
Analogique ou discret ?
En apparence, chaque bouton pourrait être analogique. C'est assez courant pour les boutons d'épaule, par exemple. Il est donc préférable de définir un seuil plutôt que de simplement le comparer à 1, 00 (que se passe-t-il si un bouton analogique est légèrement sale ? Il est possible qu'il n'atteigne jamais 1,00. Dans notre doodle, nous procédons comme suit :
gamepad.ANALOGUE_BUTTON_THRESHOLD = .5;
gamepad.buttonPressed_ = function(pad, buttonId) {
return pad.buttons[buttonId] &&
(pad.buttons[buttonId] > gamepad.ANALOGUE_BUTTON_THRESHOLD);
};
Vous pouvez faire de même pour transformer des sticks analogiques en joysticks numériques. Bien sûr, il y a toujours le pavé directionnel, mais votre manette de jeu n'en est peut-être pas équipée. Voici notre code pour gérer cela :
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;
}
};
Pressions sur les boutons et mouvements des joysticks
Événements
Dans certains cas, comme un jeu de simulation de vol, il est plus logique de vérifier et de réagir en permanence aux positions du joystick ou aux pressions sur les boutons… mais pour des choses comme le doodle Hurdles 2012 ? Vous vous demandez peut-être pourquoi vous devez vérifier les boutons à chaque frame. Pourquoi ne puis-je pas obtenir d'événements comme je le fais pour les événements de pression/relâchement du clavier ou de la souris ?
Bonne nouvelle : c'est possible. La mauvaise nouvelle, c'est que ce n'est pas pour tout de suite. Elle est incluse dans la spécification, mais n'est encore implémentée dans aucun navigateur.
Sondages
En attendant, vous pouvez comparer l'état actuel et l'état précédent, et appeler des fonctions si vous constatez une différence. Exemple :
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);
}
L'approche axée sur le clavier dans le doodle Course de haies 2012
Comme le clavier est le mode de saisie préféré du doodle d'aujourd'hui en l'absence de manette de jeu, nous avons décidé que la manette de jeu l'émulerait d'assez près. Cela impliquait trois décisions :
Le doodle n'a besoin que de trois boutons (deux pour courir et un pour sauter), mais la manette de jeu en aura probablement beaucoup plus. Nous avons donc mappé les 16 boutons et les deux sticks connus sur ces trois fonctions logiques de la manière qui nous semblait la plus logique, afin que les joueurs puissent courir en alternant les boutons A/B, en alternant les boutons d'épaule, en appuyant sur gauche/droite sur le pavé directionnel ou en balançant violemment l'un des sticks vers la gauche et la droite (certaines de ces options seront bien sûr plus efficaces que d'autres). Exemple :
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),Nous avons traité chaque entrée analogique comme une entrée discrète, en utilisant les fonctions de seuil décrites précédemment.
Nous sommes allés jusqu'à ajouter l'entrée de la manette de jeu au doodle, au lieu de l'intégrer. Notre boucle d'interrogation synthétise en fait les événements keydown et keyup nécessaires (avec un keyCode approprié) et les renvoie au 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);
Et c'est tout ce que vous avez à faire !
Conseils et astuces
- N'oubliez pas que la manette de jeu ne sera pas du tout visible dans votre navigateur tant que vous n'aurez pas appuyé sur un bouton.
- Si vous testez la manette de jeu dans différents navigateurs simultanément, notez qu'un seul d'entre eux peut détecter la manette. Si vous ne recevez aucun événement, veillez à fermer les autres pages qui pourraient l'utiliser. De plus, d'après notre expérience, il arrive qu'un navigateur "conserve" la manette de jeu même si vous fermez l'onglet ou quittez le navigateur. Le redémarrage du système est parfois le seul moyen de résoudre les problèmes.
- Comme toujours, utilisez Chrome Canary et les équivalents pour les autres navigateurs afin de vous assurer de bénéficier de la meilleure compatibilité possible, puis agissez en conséquence si vous constatez que les anciennes versions se comportent différemment.
L'avenir
Nous espérons que cela vous aidera à mieux comprendre cette nouvelle API, qui est encore un peu précaire, mais déjà très amusante.
En plus des éléments manquants de l'API (par exemple, les événements) et d'une compatibilité plus étendue avec les navigateurs, nous espérons également voir des fonctionnalités telles que le contrôle du retour haptique, l'accès aux gyroscopes intégrés, etc. De plus, nous souhaitons une meilleure compatibilité avec différents types de manettes de jeu. Veuillez signaler un bug dans Chrome et/ou signaler un bug dans Firefox si vous en trouvez une qui ne fonctionne pas correctement ou pas du tout.
Mais avant cela, essayez notre Doodle Course de haies 2012 et découvrez à quel point il est plus amusant avec une manette de jeu. Oh, tu viens de dire que tu pouvais faire mieux que 10,7 secondes ? Allez-y !