Surmonter les obstacles avec l'API Gamepad

Marcin Wichary
Marcin Wichary

Introduction

Laissez les novices garder leurs claviers pour les jeux d'aventure, leurs précieux doigts multipoint pour couper des fruits et leurs capteurs de mouvement dernier cri pour faire semblant de danser comme Michael Jackson. (Info: Ce n'est pas possible.) Mais vous êtes différent. Tu es mieux. Vous êtes un(e) pro. Pour vous, les jeux commencent et se terminent avec une manette de jeu dans les mains.

Mais attendez. N'est-il pas impossible d'utiliser une manette de jeu dans votre application Web ? Plus vraiment. La toute nouvelle API Gamepad vient à la rescousse. Elle vous permet d'utiliser JavaScript pour lire l'état de n'importe quel contrôleur de manette de jeu connecté à votre ordinateur. Cette fonctionnalité est si récente qu'elle n'est arrivée dans Chrome 21 que la semaine dernière. Elle est également sur le point d'être prise en charge dans Firefox (actuellement disponible dans une version spéciale).

C'était un bon timing, car nous avons récemment pu l'utiliser dans le dessin Google pour les obstacles de 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.

Doodle Google 2012 pour les obstacles
Doodle Google 2012 sur les obstacles

Testeur de manette de jeu

Bien qu'éphémères, les doodles interactifs sont généralement assez complexes en interne. Pour vous montrer plus facilement de quoi nous parlons, nous avons repris le code de la manette du doodle et créé un simple testeur de manette. Vous pouvez l'utiliser pour vérifier que votre manette de jeu USB fonctionne correctement, et aussi pour examiner en détail son fonctionnement.

Quels navigateurs sont compatibles avec cette fonctionnalité aujourd'hui ?

Navigateurs pris en charge

  • Chrome: 21.
  • Edge: 12.
  • Firefox: 29.
  • Safari: 10.1.

Source

Quelles manettes de jeu peuvent être utilisées ?

En général, toute manette de jeu moderne compatible nativement avec votre système devrait fonctionner. Nous avons testé différentes manettes, de manettes USB génériques sur un PC à des manettes PlayStation 2 connectées à un Mac via un dongle, en passant par des manettes Bluetooth associées à un ordinateur portable ChromeOS.

Manettes de jeu
Manettes de jeu

Voici une photo de certaines manettes 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 mappées correctement, veuillez signaler un bug dans Chrome ou Firefox . (Veuillez effectuer le test dans la dernière version de chaque navigateur pour vous assurer que le problème n'est pas déjà résolu.)

Fonctionnalité de détection de l'API Gamepad<

C'est facile dans Chrome:

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

Il ne semble pas possible de le détecter dans Firefox pour le moment. Tout est basé sur des événements, et tous les gestionnaires d'événements doivent être associés à la fenêtre, ce qui empêche une technique typique de détection des gestionnaires d'événements de fonctionner.

Mais nous sommes sûrs que ce problème est temporaire. L'excellent Modernizr vous informe déjà de l'API Gamepad. Nous vous recommandons donc de l'utiliser 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 manière, sauf si l'utilisateur appuie d'abord sur l'un de ses boutons. Cela permet d'éviter l'empreinte digitale, mais cela peut s'avérer un peu difficile pour l'expérience utilisateur: vous ne pouvez pas demander à l'utilisateur d'appuyer sur le bouton ni lui fournir d'instructions spécifiques à la manette, car vous ne savez pas s'il a connecté sa manette.

Une fois que vous aurez franchi cette étape (désolé…), d'autres défis vous attendent.

Sondage

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 connectées au système, ainsi que leur état actuel (boutons et sticks). Le premier contrôleur connecté sera renvoyé en tant que première entrée du tableau, et ainsi de suite.

(Cet appel de fonction vient récemment remplacer un tableau auquel vous pouviez accéder directement : navigator.webkitGamepads[]. 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 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 implémentée jusqu'à présent vous oblige à vérifier en permanence l'état des manettes de jeu connectées (et à le comparer à l'état précédent si nécessaire), au lieu de déclencher des événements en cas de changement. Nous nous sommes appuyés sur requestAnimationFrame() pour configurer le sondage de la manière la plus efficace et la plus économe en batterie. Pour notre doodle, même si nous disposions déjà d'une boucle requestAnimationFrame() pour prendre en charge les animations, nous en avons créé une deuxième complètement distincte. Il était plus simple à coder et ne devrait pas affecter les performances de quelque manière que ce soit.

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, il peut être aussi simple d'obtenir ses données que:

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

Si vous souhaitez être un peu plus malin 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 ou plus connectées, certaines d'entre elles 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, meilleure, décrite dans la spécification de l'API Gamepad. Au lieu de vous demander d'effectuer une requête, il expose deux événements (MozGamepadConnected et MozGamepadDisconnected) qui sont déclenchés chaque fois qu'un contrôleur de jeu est branché (ou, plus précisément, branché et "annoncé" en appuyant sur l'un de ses boutons) ou débranché. L'objet manette de jeu qui continuera de refléter son état futur est transmis en tant que paramètre .gamepad de l'objet é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 prend en charge 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 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
  • index: entier permettant de distinguer les différents gamepads connectés à un ordinateur
  • timestamp: code temporel de la dernière mise à jour de l'état du bouton/des axes (actuellement, cette valeur n'est acceptée que dans Chrome)

Boutons et joysticks

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

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

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:

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 que 16 boutons et quatre axes ne sont pas garantis. Certains d'entre eux peuvent simplement être non 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) à 0,0 (centre) et à 1,0 (complètement à droite ou en bas).

Analogique ou discret ?

En théorie, tous les boutons peuvent être analogiques. C'est assez courant pour les boutons de commande, 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 les sticks analogiques en joysticks numériques. Bien sûr, il y a toujours le pavé directionnel, mais il est possible que votre manette de jeu n'en dispose pas. Voici notre code pour y parvenir:

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;
    }
};

Appuyer sur les boutons et mouvements des sticks

Événements

Dans certains cas, comme dans un jeu de simulation de vol, il est plus logique de vérifier en continu la position des manettes ou les pressions sur les boutons et d'y réagir. Mais pour des éléments comme le doodle de 2012 sur les obstacles ? Vous vous demandez peut-être pourquoi vous devez vérifier la présence de boutons à chaque frame. Pourquoi ne puis-je pas obtenir d'événements comme je le fais pour les touches de clavier ou de souris "haut"/"bas" ?

La bonne nouvelle est que vous pouvez. La mauvaise nouvelle est que cela ne sera pas possible à l'avenir. Il figure dans les spécifications, mais n'est pas encore implémenté dans un navigateur.

Sondage

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 clavier d'abord dans le doodle de 2012 sur les obstacles

Étant donné que, sans manette de jeu, le clavier est le mode de saisie privilégié pour notre doodle du jour, nous avons décidé de l'émuler de manière assez proche. Cela impliquait trois décisions:

  1. 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 seize boutons et deux sticks connus sur ces trois fonctions logiques de la manière qui nous a semblé la plus logique, afin que les utilisateurs puissent courir en appuyant alternativement sur les boutons A/B, sur les boutons des épaules, sur les boutons de direction gauche/droite ou en balançant violemment l'un des sticks de gauche à droite (certains de ces mouvements 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),
    
  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'à fixer l'entrée de la manette de jeu sur le 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 code de touche 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 !

Conseils et astuces

  • N'oubliez pas que la manette de jeu n'est pas du tout visible dans votre navigateur avant qu'un bouton ne soit enfoncé.
  • 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, assurez-vous de 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 lui-même. Redémarrer le système est parfois le seul moyen de résoudre le problème.
  • Comme toujours, utilisez Chrome Canary et les équivalents pour les autres navigateurs pour bénéficier d'une assistance optimale, puis prenez les mesures appropriées si vous constatez que les versions antérieures se comportent différemment.

Prochaines étapes

Nous espérons que ces informations vous aideront à mieux comprendre cette nouvelle API, 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 large avec les navigateurs, nous espérons également proposer des fonctionnalités comme le contrôle de la vibration, l'accès aux gyroscopes intégrés, etc., et une compatibilité plus large 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, allez jouer avec notre doodle 2012 sur les obstacles et découvrez à quel point c'est plus amusant avec la manette de jeu. Vous venez de dire que vous pouviez faire mieux que 10, 7 secondes ? Allez-y !

Documentation complémentaire