Please note, this is a STATIC archive of website developer.mozilla.org from 03 Nov 2016, cach3.com does not collect or store any user information, there is no "phishing" involved.

Revision 1109271 of Desktop gamepad controls

  • Revision slug: Games/Techniques/Control_mechanisms/Desktop_with_gamepad
  • Revision title: Implementing input controls for desktop with Gamepad API
  • Revision id: 1109271
  • Created:
  • Creator: chrisdavidmills
  • Is current revision? No
  • Comment Games/Techniques/Input_controls/Desktop_with_gamepad Games/Techniques/Control_mechanisms/Desktop_with_gamepad

Revision Content

Now when we have the desktop controls in place on top of the mobile controls, we can add something extra - the Gamepad API support. It brings console-like experience to the web games. In this tutorial you'll learn how to implement the Gamepad API in a JavaScript game, so you can enjoy it using a gamepad connected to your computer.

API status, browser and hardware support

The Gamepad API documentation is still in the Working Draft status, although the browser support is already quite good - 63% global coverage. The list of supported devices is also quite extensive - getting a fairly popular gamepad like the XBox 360 or PS3 ones should be enough to play with the implementation.

GamepadAPI object

Let's move on to the coding part - we'll add Gamepad API support to the game Captain Rogers: Battle at Andromeda created with Phaser. This JavaScript code can be used in any other project no matter the framework though.

First off, we'll create a small library that will take care of handling the input for us. Here's the GamepadAPI object containing useful variables and functions:
    
var GamepadAPI = {
    active: false,
    controller: {},
    connect: function(event) {},
    disconnect: function(event) {},
    update: function() {},
    buttons: {
        layout: [],
        cache: [],
        status: [],
        pressed: function(button, state) {}
    }
    axes: {
        status: []
    }
};

The controller variable stores the information about the connected gamepad, and there's an active boolean variable we can use to know if the controller is connected or not. The connect and disconnect functions are bound to the events:

window.addEventListener("gamepadconnected", GamepadAPI.connect);
window.addEventListener("gamepaddisconnected", GamepadAPI.disconnect);

They are fired when the gamepad is connected, and disconnected respectively. The next function is update where the information about the pressed buttons and axes is updated. The buttons variable contains the layout of a given controller (for example which buttons are where, because XBox 360 layout may be different than a generic, no-name one), the cache containing the information about the buttons from the previous frame and the status containing the information from the current frame. The pressed function gets the input data and sets the infofmation about it in our object, and the axes variable stores the table with the values.

After the gamepad is connected, the information about the controller is stored in the object:

connect: function(event) {
    GamepadAPI.controller = event.gamepad;
    GamepadAPI.active = true;
},

The disconnect function removes the information from the object:

disconnect: function(event) {
    delete GamepadAPI.controller;
    GamepadAPI.active = false;
},

The update function is executed in the update loop of the game on every frame, and have the latest information on the pressed buttons:

update: function() {
  GamepadAPI.buttons.cache = [];
  for(var k=0; k<GamepadAPI.buttons.status.length; k++) {
    GamepadAPI.buttons.cache[k] = GamepadAPI.buttons.status[k];
  }
  GamepadAPI.buttons.status = [];
  var c = GamepadAPI.controller || {};
  var pressed = [];
  if(c.buttons) {
    for(var b=0,t=c.buttons.length; b<t; b++) {
      if(c.buttons[b].pressed) {
        pressed.push(GamepadAPI.buttons.layout[b]);
      }
    }
  }
  var axes = [];
  if(c.axes) {
    for(var a=0,x=c.axes.length; a<x; a++) {
      axes.push(c.axes[a].toFixed(2));
    }
  }
  GamepadAPI.axes.status = axes;
  GamepadAPI.buttons.status = pressed;
  return pressed;
},

The function above clears the buttons cache, and copy their status from the previous frame to the cache. Then the buttons status is cleared and the new information is added. The same goes for the axes information - looping through axes adds the values to the array. Received values are assigned to the proper objects and returns the pressed info for debugging purposes.

The button.pressed function detects the actual button presses and saves the information about that in the table.

pressed: function(button, hold) {
  var newPress = false;
  for(var i=0,s=GamepadAPI.buttons.status.length; i<s; i++) {
    if(GamepadAPI.buttons.status[i] == button) {
      newPress = true;
      if(!hold) {
        for(var j=0,p=GamepadAPI.buttons.cache.length; j<p; j++) {
          if(GamepadAPI.buttons.cache[j] == button) {
            newPress = false;
          }
        }
      }
    }
  }
  return newPress;
},

It loops through pressed buttons and if the button we're looking for is pressed, then the corresponding boolean variable is set to true. If we want to check if the button is not held already (so it's a new press), then looping through the cached states from the previous frame does the job - if the button was already pressed, then ignore the new press and set it to false.

Implementation

We now know how the GamepadAPI object looks like and what variables and functions it contain, so let's learn how all this is actually used in the game. To indicate that the gamepad controller is active we can show the user a custom text on the game's main menu screen.

The textGamepad object holds the text saying a gamepad has been connected, and is hidden by default. Here's the code we've prepared in the create function that is executed once when the new state is created:

create() {
    // ...
    var message = 'Gamepad connected! Press Y for controls';
    var textGamepad = this.add.text(message, ...);
    textGamepad.visible = false;
}

Then, in the update function which is executed every frame we can wait till the controller is actually connected, so the proper text can be shown. Then, in the update function we can keep the track of the information about pressed buttons by using Gamepad.update() method, and then react to the given information:

update: function() {
    // ...
    if(GamepadAPI.active) {
        if(!this.textGamepad.visible) {
            this.textGamepad.visible = true;
        }
        GamepadAPI.update();
        if(GamepadAPI.buttons.pressed('Start')) {
            // start the game
        }
        if(GamepadAPI.buttons.pressed('X')) {
            // turn on/off the sounds
        }
        if(GamepadAPI.buttons.pressed('Y','hold')) {
            if(!this.screenGamepadHelp.visible) {
                this.screenGamepadHelp.visible = true;
            }
        }
        else {
            if(this.screenGamepadHelp.visible) {
                this.screenGamepadHelp.visible = false;
            }
        }
    }
}

When pressing the Start button the relevant function will be called to begin the game, and the same approach is for turning the audio on and off. There's an option to see screenGamepadHelp which holds the image with all the button controls explained - if the Y button is pressed and held, the help is visible, and when it is released the help diappears.

---IMG_BUTTONS_EXPLAINED---

On-screen instructions

There is an introductory text when you start the game, that shows you available controls - we are already detecting if the game is launched on desktop or mobile to show relevant message, but we can go even further.

create() {
    // ...
    if(this.game.device.desktop) {
        if(GamepadAPI.active) {
            moveText = 'DPad or left Stick\nto move';
            shootText = 'A to shoot,\nY for controls';
        }
        else {
            moveText = 'Arrow keys\nor WASD to move';
            shootText = 'X or Space\nto shoot';
        }
    }
    else {
        moveText = 'Tap and hold to move';
        shootText = 'Tap to shoot';
    }
}

When on desktop, we can check if the controller is active and show the gamepad controls - if not, then the keyboard controls will be shown.

It depends on the platform and available devices, and can adjust to the current configuration. That way the player sees only the information he currently needs.

Gameplay controls

We can offer even more flexibility to the player by giving him main and alternative gamepad movement controls:

if(GamepadAPI.buttons.pressed('DPad-Up','hold')) {
    // move player up
}
else if(GamepadAPI.buttons.pressed('DPad-Down','hold')) {
    // move player down
}
if(GamepadAPI.buttons.pressed('DPad-Left','hold')) {
    // move player left
}
if(GamepadAPI.buttons.pressed('DPad-Right','hold')) {
    // move player right
}
if(GamepadAPI.axes.status && GamepadAPI.axes.status[0]) {
    if(GamepadAPI.axes.status[0] > 0.5) {
        // move player up
    }
    else if(GamepadAPI.axes.status[0] < -0.5) {
        // move player down
    }
    if(GamepadAPI.axes.status[1] > 0.5) {
        // move player left
    }
    else if(GamepadAPI.axes.status[1] < -0.5) {
        // move player right
    }
}

He can move the ship on the screen by using the DPad buttons, or the left stick axes.

Have you noticed that the current value of the axes is evaluated against 0.5? It's because axes are having floating point values while buttons are booleans. After a certain threshold is reached we can assume the input is done deliberately by the user and can act accordingly.

Now, shooting with a gamepad: remember to press and hold the A button to spawn a new bullet, and everything else will be handled by the game:

if(GamepadAPI.buttons.pressed('A','hold')) {
    this.spawnBullet();
}

Showing the screen with all the controls looks exactly the same as in the main menu:

if(GamepadAPI.buttons.pressed('Y','hold')) {
    if(!this.screenGamepadHelp.visible) {
        this.screenGamepadHelp.visible = true;
    }
}
else {
    if(this.screenGamepadHelp.visible) {
        this.screenGamepadHelp.visible = false;
    }
}

If the button B is pressed, the game is then paused:

if(gamepadAPI.buttonPressed('B')) {
    this.managePause();
}

While playing, holding the A button will fire the bullets with a defined fire rate, in the meantime you can sneak peek other controls by pressing and holding the Y button, and if that's too much of the multitasking you can always pause the game instantly by pressing the B button.

Pause and game over states

We already learned how to control the whole lifecycle of the game: pausing the gameplay, restarting it or getting back to the main menu. It works smooth on mobile, then we added the keyboard controls on top of that. Adding gamepad controls to that is quite straightforward - in the update function, if the current state status is paused then the relevant actions are enabled:

if(GamepadAPI.buttons.pressed('Start')) {
    this.managePause();
}
if(GamepadAPI.buttons.pressed('Back')) {
    this.stateBack();
}

Similarly, when the gameover state status is active, then we can allow the user to restart the game instead of continuing it:

if(GamepadAPI.buttons.pressed('Start')) {
    this.stateRestart();
}
if(GamepadAPI.buttons.pressed('Back')) {
    this.stateBack();
}

When the game over screen is visible, the Start button is restarting the game while the Back button helps us get back to the main menu. The same goes for when the game is paused: the Start button is unpausing the game and the Back button does exactly the same as before.

That's it! We have successfully implemented gamepad controls in the game - try connecting any popular controller like the XBox 360 one and see for yourself how fun it is to avoid the asteroids and shoot the aliens that way.

Now we can move on and explore new, even more unconventional ways to control the HTML5 game like waving your hand in front of the laptop or screaming to the speaker.

Revision Source

<p class="summary">Now when we have the desktop controls in place on top of the mobile controls, we can add something extra - the Gamepad API support. It brings console-like experience to the web games. In this tutorial you'll learn how to implement the Gamepad API in a JavaScript game, so you can enjoy it using a gamepad connected to your computer.</p>

<h2>API status, browser and hardware support</h2>

<p>The Gamepad API documentation is still in the Working Draft status, although the browser support is already quite good - 63% global coverage. The list of supported devices is also quite extensive - getting a fairly popular gamepad like the XBox 360 or PS3 ones should be enough to play with the implementation.</p>

<h2>GamepadAPI object</h2>

<p>Let's move on to the coding part - we'll add Gamepad API support to the game Captain Rogers: Battle at Andromeda created with Phaser. This JavaScript code can be used in any other project no matter the framework though.</p>

<pre>
First off, we'll create a small library that will take care of handling the input for us. Here's the <code>GamepadAPI</code> object containing useful variables and functions:
    
var GamepadAPI = {
    active: false,
    controller: {},
    connect: function(event) {},
    disconnect: function(event) {},
    update: function() {},
    buttons: {
        layout: [],
        cache: [],
        status: [],
        pressed: function(button, state) {}
    }
    axes: {
        status: []
    }
};</pre>

<p>The controller variable stores the information about the connected gamepad, and there's an <code>active</code> boolean variable we can use to know if the controller is connected or not. The <code>connect</code> and <code>disconnect</code> functions are bound to the events:</p>

<pre>
window.addEventListener("gamepadconnected", GamepadAPI.connect);
window.addEventListener("gamepaddisconnected", GamepadAPI.disconnect);</pre>

<p>They are fired when the gamepad is connected, and disconnected respectively. The next function is <code>update</code> where the information about the pressed buttons and axes is updated. The <code>buttons</code> variable contains the <code>layout</code> of a given controller (for example which buttons are where, because XBox 360 layout may be different than a generic, no-name one), the <code>cache</code> containing the information about the buttons from the previous frame and the <code>status</code> containing the information from the current frame. The <code>pressed</code> function gets the input data and sets the infofmation about it in our object, and the <code>axes</code> variable stores the table with the values.</p>

<p>After the gamepad is connected, the information about the controller is stored in the object:</p>

<pre>
connect: function(event) {
    GamepadAPI.controller = event.gamepad;
    GamepadAPI.active = true;
},</pre>

<p>The <code>disconnect</code> function removes the information from the object:</p>

<pre>
disconnect: function(event) {
    delete GamepadAPI.controller;
    GamepadAPI.active = false;
},</pre>

<p>The <code>update</code> function is executed in the update loop of the game on every frame, and have the latest information on the pressed buttons:</p>

<pre>
update: function() {
  GamepadAPI.buttons.cache = [];
  for(var k=0; k&lt;GamepadAPI.buttons.status.length; k++) {
    GamepadAPI.buttons.cache[k] = GamepadAPI.buttons.status[k];
  }
  GamepadAPI.buttons.status = [];
  var c = GamepadAPI.controller || {};
  var pressed = [];
  if(c.buttons) {
    for(var b=0,t=c.buttons.length; b&lt;t; b++) {
      if(c.buttons[b].pressed) {
        pressed.push(GamepadAPI.buttons.layout[b]);
      }
    }
  }
  var axes = [];
  if(c.axes) {
    for(var a=0,x=c.axes.length; a&lt;x; a++) {
      axes.push(c.axes[a].toFixed(2));
    }
  }
  GamepadAPI.axes.status = axes;
  GamepadAPI.buttons.status = pressed;
  return pressed;
},</pre>

<p>The function above clears the buttons cache, and copy their status from the previous frame to the cache. Then the buttons status is cleared and the new information is added. The same goes for the axes information - looping through axes adds the values to the array. Received values are assigned to the proper objects and returns the pressed info for debugging purposes.</p>

<p>The <code>button.pressed</code> function detects the actual button presses and saves the information about that in the table.</p>

<pre>
pressed: function(button, hold) {
  var newPress = false;
  for(var i=0,s=GamepadAPI.buttons.status.length; i&lt;s; i++) {
    if(GamepadAPI.buttons.status[i] == button) {
      newPress = true;
      if(!hold) {
        for(var j=0,p=GamepadAPI.buttons.cache.length; j&lt;p; j++) {
          if(GamepadAPI.buttons.cache[j] == button) {
            newPress = false;
          }
        }
      }
    }
  }
  return newPress;
},</pre>

<p>It loops through pressed buttons and if the button we're looking for is pressed, then the corresponding boolean variable is set to <code>true</code>. If we want to check if the button is not held already (so it's a new press), then looping through the cached states from the previous frame does the job - if the button was already pressed, then ignore the new press and set it to false.</p>

<h2>Implementation</h2>

<p>We now know how the <code>GamepadAPI</code> object looks like and what variables and functions it contain, so let's learn how all this is actually used in the game. To indicate that the gamepad controller is active we can show the user a custom text on the game's main menu screen.</p>

<p>The textGamepad object holds the text saying a gamepad has been connected, and is hidden by default. Here's the code we've prepared in the create function that is executed once when the new state is created:</p>

<pre>
create() {
    // ...
    var message = 'Gamepad connected! Press Y for controls';
    var textGamepad = this.add.text(message, ...);
    textGamepad.visible = false;
}</pre>

<p>Then, in the update function which is executed every frame we can wait till the controller is actually connected, so the proper text can be shown. Then, in the update function we can keep the track of the information about pressed buttons by using <code>Gamepad.update()</code> method, and then react to the given information:</p>

<pre>
update: function() {
    // ...
    if(GamepadAPI.active) {
        if(!this.textGamepad.visible) {
            this.textGamepad.visible = true;
        }
        GamepadAPI.update();
        if(GamepadAPI.buttons.pressed('Start')) {
            // start the game
        }
        if(GamepadAPI.buttons.pressed('X')) {
            // turn on/off the sounds
        }
        if(GamepadAPI.buttons.pressed('Y','hold')) {
            if(!this.screenGamepadHelp.visible) {
                this.screenGamepadHelp.visible = true;
            }
        }
        else {
            if(this.screenGamepadHelp.visible) {
                this.screenGamepadHelp.visible = false;
            }
        }
    }
}</pre>

<p>When pressing the <code>Start</code> button the relevant function will be called to begin the game, and the same approach is for turning the audio on and off. There's an option to see <code>screenGamepadHelp</code> which holds the image with all the button controls explained - if the <code>Y</code> button is pressed and held, the help is visible, and when it is released the help diappears.</p>

<p>---IMG_BUTTONS_EXPLAINED---</p>

<h2>On-screen instructions</h2>

<p>There is an introductory text when you start the game, that shows you available controls - we are already detecting if the game is launched on desktop or mobile to show relevant message, but we can go even further.</p>

<pre>
create() {
    // ...
    if(this.game.device.desktop) {
        if(GamepadAPI.active) {
            moveText = 'DPad or left Stick\nto move';
            shootText = 'A to shoot,\nY for controls';
        }
        else {
            moveText = 'Arrow keys\nor WASD to move';
            shootText = 'X or Space\nto shoot';
        }
    }
    else {
        moveText = 'Tap and hold to move';
        shootText = 'Tap to shoot';
    }
}</pre>

<p>When on desktop, we can check if the controller is active and show the gamepad controls - if not, then the keyboard controls will be shown.</p>

<p>It depends on the platform and available devices, and can adjust to the current configuration. That way the player sees only the information he currently needs.</p>

<h2>Gameplay controls</h2>

<p>We can offer even more flexibility to the player by giving him main and alternative gamepad movement controls:</p>

<pre>
if(GamepadAPI.buttons.pressed('DPad-Up','hold')) {
    // move player up
}
else if(GamepadAPI.buttons.pressed('DPad-Down','hold')) {
    // move player down
}
if(GamepadAPI.buttons.pressed('DPad-Left','hold')) {
    // move player left
}
if(GamepadAPI.buttons.pressed('DPad-Right','hold')) {
    // move player right
}
if(GamepadAPI.axes.status &amp;&amp; GamepadAPI.axes.status[0]) {
    if(GamepadAPI.axes.status[0] &gt; 0.5) {
        // move player up
    }
    else if(GamepadAPI.axes.status[0] &lt; -0.5) {
        // move player down
    }
    if(GamepadAPI.axes.status[1] &gt; 0.5) {
        // move player left
    }
    else if(GamepadAPI.axes.status[1] &lt; -0.5) {
        // move player right
    }
}</pre>

<p>He can move the ship on the screen by using the <code>DPad</code> buttons, or the left stick axes.</p>

<p>Have you noticed that the current value of the axes is evaluated against <code>0.5</code>? It's because axes are having floating point values while buttons are booleans. After a certain threshold is reached we can assume the input is done deliberately by the user and can act accordingly.</p>

<p>Now, shooting with a gamepad: remember to press and hold the <code>A</code> button to spawn a new bullet, and everything else will be handled by the game:</p>

<pre>
if(GamepadAPI.buttons.pressed('A','hold')) {
    this.spawnBullet();
}</pre>

<p>Showing the screen with all the controls looks exactly the same as in the main menu:</p>

<pre>
if(GamepadAPI.buttons.pressed('Y','hold')) {
    if(!this.screenGamepadHelp.visible) {
        this.screenGamepadHelp.visible = true;
    }
}
else {
    if(this.screenGamepadHelp.visible) {
        this.screenGamepadHelp.visible = false;
    }
}</pre>

<p>If the button <code>B</code> is pressed, the game is then paused:</p>

<pre>
if(gamepadAPI.buttonPressed('B')) {
    this.managePause();
}</pre>

<p>While playing, holding the <code>A</code> button will fire the bullets with a defined fire rate, in the meantime you can sneak peek other controls by pressing and holding the <code>Y</code> button, and if that's too much of the multitasking you can always pause the game instantly by pressing the <code>B</code> button.</p>

<h2>Pause and game over states</h2>

<p>We already learned how to control the whole lifecycle of the game: pausing the gameplay, restarting it or getting back to the main menu. It works smooth on mobile, then we added the keyboard controls on top of that. Adding gamepad controls to that is quite straightforward - in the <code>update</code> function, if the current state status is <code>paused</code> then the relevant actions are enabled:</p>

<pre>
if(GamepadAPI.buttons.pressed('Start')) {
    this.managePause();
}
if(GamepadAPI.buttons.pressed('Back')) {
    this.stateBack();
}</pre>

<p>Similarly, when the <code>gameover</code> state status is active, then we can allow the user to restart the game instead of continuing it:</p>

<pre>
if(GamepadAPI.buttons.pressed('Start')) {
    this.stateRestart();
}
if(GamepadAPI.buttons.pressed('Back')) {
    this.stateBack();
}</pre>

<p>When the game over screen is visible, the <code>Start</code> button is restarting the game while the <code>Back</code> button helps us get back to the main menu. The same goes for when the game is paused: the <code>Start</code> button is unpausing the game and the <code>Back</code> button does exactly the same as before.</p>

<p>That's it! We have successfully implemented gamepad controls in the game - try connecting any popular controller like the XBox 360 one and see for yourself how fun it is to avoid the asteroids and shoot the aliens that way.</p>

<p>Now we can move on and explore new, even more unconventional ways to control the HTML5 game like waving your hand in front of the laptop or screaming to the speaker.</p>
Revert to this revision