Surmonter les obstacles avec l'API Gamepad

Marcin Wichary
Marcin Wichary

Introduction

Laissez les novices garder leur clavier pour les jeux d'aventure, leurs précieux doigts aux multiples touchers pour couper les fruits et leurs nouveaux capteurs de mouvement pour faire semblant de danser comme Michael Jackson. (Flash: non.) Mais tu es différent. Tu vas mieux. Tu es un pro. Pour vous, les jeux commencent et se terminent avec une manette de jeu à portée de main.

Mais attendez. N'avez-vous pas de chance si vous souhaitez prendre en charge une manette de jeu dans votre application Web ? Plus vraiment. La toute nouvelle API Gamepad vous vient en aide : elle vous permet d'utiliser JavaScript pour lire l'état de n'importe quelle manette de jeu connectée à votre ordinateur. Elle est tellement récente qu'elle n'est disponible que dans Chrome 21 la semaine dernière. Elle est également sur le point d'être compatible avec Firefox (actuellement disponible dans une version spéciale).

Le moment s'est avéré très intéressant, car nous avons récemment eu l'occasion de l'utiliser dans le doodle Google Hurdles 2012. Cet article explique brièvement comment nous avons ajouté l'API Gamepad au doodle et ce que nous avons appris au cours du processus.

Haies doodle 2012 Google
Doodle Google Hurdles 2012

Testeur de manette de jeu

Éphémères comme ils sont, les doodles interactifs ont tendance à être assez complexes en arrière-plan. Pour illustrer plus facilement ce dont nous parlons, nous avons pris le code de la manette de jeu du doodle et avons mis au point un simple testeur de manette de jeu. Vous pouvez l'utiliser pour vérifier que votre manette de jeu USB fonctionne correctement, mais aussi regarder sous le capot pour en savoir plus.

Quels sont les navigateurs compatibles avec cette fonctionnalité actuellement ?

Navigateurs pris en charge

  • 21
  • 12
  • 29
  • 10.1

Source

Quelles manettes de jeu peuvent être utilisées ?

En règle générale, toute manette de jeu moderne compatible avec votre système de manière native devrait fonctionner. Nous avons testé plusieurs manettes de jeu, qu'il s'agisse de manettes USB d'autres marques sur un PC, de manettes de jeu PlayStation 2 connectées via un dongle à un Mac, en passant par les manettes Bluetooth associées à un ordinateur portable Chrome OS.

Manettes de jeu
Manettes de jeu

Photo de certaines des télécommandes que nous avons utilisées 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 correctement mappées, veuillez signaler un bug dans Chrome ou Firefox . (Veuillez tester la version la plus récente de chaque navigateur pour vous assurer que le problème n'est pas déjà résolu.)

Détecter l'API Gamepad

Facile dans Chrome:

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

Il semble impossible de détecter cela dans Firefox pour le moment. Tout est basé sur des événements et tous les gestionnaires d'événements doivent être rattachés à une fenêtre, ce qui empêche une technique classique de détection des gestionnaires d'événements de fonctionner.

Mais nous sommes sûrs que c'est temporaire. L'incroyable Modernizr vous parle déjà de l'API Gamepad. Nous vous recommandons donc ceci pour tous vos besoins de détection actuels et futurs:

var gamepadSupportAvailable = Modernizr.gamepads;

En savoir plus sur les manettes de jeu connectées

Même si vous connectez la manette de jeu, elle ne se manifeste en aucune façon, sauf si l'utilisateur appuie d'abord sur l'un de ses boutons. Cela permet d'éviter le fingerprinting, bien que cela représente un véritable défi pour l'expérience utilisateur: vous ne pouvez pas demander à l'utilisateur d'appuyer sur le bouton ou de fournir des instructions spécifiques à la manette, car vous ne savez pas s'il a connecté sa manette.

Une fois que vous aurez surmonté cet obstacle (désolé...), ce n'est pas tout !

Sondage

L'implémentation de l'API dans 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 connectée est renvoyée en tant que première entrée du tableau, et ainsi de suite.

(Cet appel de fonction vient de remplacer un tableau auquel vous pouviez accéder directement : navigator.webkitGamepads[]. Depuis le début du mois d'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 versions ultérieures. À l'avenir, l'appel de fonction sera la méthode recommandée pour utiliser l'API. Il sera progressivement appliqué à tous les navigateurs Chrome installés.)

La partie de la spécification mise en œuvre jusqu'ici nécessite de vérifier en permanence l'état des manettes de jeu connectées (et de le comparer à la précédente si nécessaire), au lieu de déclencher des événements lorsque les choses changent. Nous avons utilisé requestAnimationFrame() pour configurer le sondage de la manière la plus efficace et la plus économe en énergie. Pour notre doodle, bien que nous ayons déjà une boucle requestAnimationFrame() pour prendre en charge les animations, nous en avons créé une autre complètement distincte. Elle était plus simple à coder et ne devrait en aucun cas affecter les performances.

Voici le code envoyé par le 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 n'avez besoin que d'une seule manette de jeu, voici comment obtenir ses données:

var gamepad = navigator.webkitGetGamepads && navigator.webkitGetGamepads()[0];

Si vous souhaitez être un peu plus intelligent ou prendre en charge plusieurs joueurs en même temps, vous devez 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éconnectent en cours de route, etc.). Vous pouvez consulter le code source de notre testeur, la fonction pollGamepads(), pour voir une approche permettant de résoudre ce problème.

Événements

Firefox utilise une méthode alternative mieux décrite dans les spécifications 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 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 de manette de jeu qui continuera de refléter son état futur est transmis en tant que paramètre .gamepad de l'objet événement.

Dans le 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, compatible avec les deux approches, ressemble à ceci:

/**
 * 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

Chaque manette connectée au système sera représentée par un objet qui ressemble à 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 jeu
  • index: entier utile pour distinguer les différentes manettes de jeu connectées à un ordinateur.
  • timestamp: code temporel de la dernière mise à jour de l'état du bouton ou des axes (actuellement disponible uniquement dans Chrome).

Boutons et bâtons

Les manettes de jeu actuelles ne sont pas exactement ce que votre grand-père aurait utilisé pour sauver la princesse dans le mauvais château. Elles disposent généralement d'au moins 16 boutons distincts (certains discrets, d'autres analogiques), en plus de deux sticks analogiques. L'API Gamepad vous indique tous les boutons et sticks analogiques indiqués par le système d'exploitation.

Une fois que vous avez obtenu l'état actuel dans l'objet de manette de jeu, vous pouvez accéder aux boutons via .buttons[] et aux sticks via des tableaux .axes[]. Voici un résumé visuel de leur correspondance:

Schéma de la manette de jeu
Schéma de la manette de jeu

La spécification demande au navigateur de mapper les seize premiers boutons et quatre axes sur les éléments suivants:

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. Notez toutefois que ni seize boutons ni quatre axes ne sont pas garantis. Certains d'entre eux risquent simplement de ne pas être définis.

La valeur des boutons peut être comprise entre 0.0 (si vous n'avez pas appuyé sur le bouton) et 1.0 (en cas d'appui complet). Les axes vont de -1.0 (complètement à gauche ou en haut) à 0.0 (centre) à 1.0 (complètement à droite ou en bas).

Analogique ou discret ?

En apparence, chaque bouton pourrait être analogue (ce qui est assez courant pour les boutons des angles supérieurs, par exemple). Par conséquent, il est préférable de définir un seuil plutôt que de le comparer directement à 1,00 (que se passe-t-il si un bouton analogique est légèrement sale ? Il est possible qu'elle n'atteigne jamais 1,00. Dans notre doodle, nous utilisons la méthode suivante:

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é numérique, mais votre manette de jeu n'en a peut-être pas. Voici notre code permettant de 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 du stick

Événements

Dans certains cas, comme dans 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 dois-je vérifier la présence de boutons à chaque image ? Pourquoi ne puis-je pas obtenir d'événements comme pour un clavier ou une souris Haut/Bas ?

La bonne nouvelle, c'est que vous pouvez le faire. La mauvaise nouvelle, c'est le futur. Elle figure dans les spécifications, mais elle n'est encore implémentée dans aucun navigateur.

Sondage

En attendant, pour vous éloigner, 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 Hurdles 2012

Étant donné que sans manette de jeu, le mode de saisie préféré de notre doodle aujourd'hui est le clavier, nous avons décidé que la manette de jeu l'émulait plutôt fidèlement. Cela s'est traduit par trois décisions:

  1. Le doodle n'a besoin que de trois boutons, deux pour courir et un pour sauter. Il est probable que la manette de jeu en propose beaucoup plus. Nous avons donc associé les 16 boutons connus et les deux sticks connus à ces trois fonctions logiques de la manière qui nous a semblé la plus logique, afin que les utilisateurs puissent courir en alternant les boutons A/B, en alternant les boutons des angles supérieurs, en appuyant sur les touches gauche/droite du pavé directionnel ou en balançant violemment l'un ou l'autre des sticks vers la gauche ou vers la droite (certains seront, bien sûr, plus efficaces que les 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),
    
  2. Nous avons traité chaque entrée analogique comme une entrée discrète, à l'aide des fonctions de seuil décrites précédemment.

  3. 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 de touche et de raccourci 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 !

À noter

  • N'oubliez pas que la manette de jeu n'est pas du tout visible dans votre navigateur tant que vous n'avez pas appuyé sur un bouton.
  • Si vous testez la manette dans plusieurs navigateurs simultanément, notez qu'un seul d'entre eux peut détecter la manette. Si vous ne recevez aucun événement, assurez-vous de fermer les autres pages susceptibles de l'utiliser. De plus, d'après notre expérience, un navigateur peut parfois "se tenir sur" la manette de jeu, même si vous fermez l'onglet ou que vous le quittez. Redémarrer le système est parfois le seul moyen de résoudre les problèmes.
  • Comme toujours, utilisez Chrome Canary et des équivalents pour les autres navigateurs afin de vous assurer de bénéficier d'une assistance optimale. Ensuite, agissez en conséquence si vous constatez que d'anciennes versions se comportent différemment.

Prochaines étapes

Nous espérons que ces précisions vous aideront à mieux comprendre cette nouvelle API, qui est un peu incertaine, mais déjà très amusante.

En plus des éléments manquants dans l'API (par exemple, les événements) et de la prise en charge plus large des navigateurs, nous espérons voir à terme des options comme le contrôle du rumble, l'accès aux gyroscopes intégrés, etc. Et une meilleure prise en charge de différents types de manettes de jeu : signalez un bug dans Chrome et/ou signalez un bug avec Firefox si vous en trouvez une qui fonctionne mal ou pas du tout.

Mais avant cela, n'hésitez pas à tester notre doodle Hurdles 2012 et à vous amuser sur la manette. Oh, est-ce que vous venez de dire que vous pourriez faire mieux que 10,7 secondes ? Allez-y !

Documentation complémentaire