Mozilla's getting a new look. What do you think? http://www.surveygizmo.com/s3/3076876/Mozilla-Brand-Identity-Survey-MDN

Desktop gamepad controls

Now we'll look at adding something extra — support for gamepad controls, via the Gamepad API. It brings a console-like experience to your web games.

API status, browser and hardware support

The Gamepad API is still in Working Draft status, although browser support is already quite good — around 63% global coverage, according to canuse.com. The list of supported devices is also quite extensive — most popular gamepads (e.g. XBox 360 or PS3) should be suitable for web implementations.

GamepadAPI object

Let's move on to the coding part — we'll add Gamepad API support to the Captain Rogers: Battle at Andromeda game we created with Phaser. This is pure JavaScript code however, so can be used in any other project no matter what framework was used.

First off, we'll create a small library that will take care of handling the input for us. Here's the GamepadAPI object, which contains 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 following 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(), which updates the information about the pressed buttons and axes.

The buttons variable contains the layout of a given controller (for example which buttons are where, because an XBox 360 layout may be different to a generic 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 information 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, so it contains 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 copies their status from the previous frame to the cache. Next, the button 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 them 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 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 we ignore the new press and set it to false.

Implementation

We now know what 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 some 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;
}

In the update() function, which is executed every frame, we can wait until the controller is actually connected, so the proper text can be shown. Then we can keep the track of the information about pressed buttons by using the Gamepad.update() method, and 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 used for turning the audio on and off. There's an option wired up to show screenGamepadHelp, which holds an image with all the button controls explained — if the Y button is pressed and held, the help becomes visible; when it is released the help diappears.

---IMG_BUTTONS_EXPLAINED---

On-screen instructions

When the game is started, some introductory text is shown that shows you available controls — we are already detecting if the game is launched on desktop or mobile then showing a relevant message for the device, but we can go even further, to allow for the presence of a gamepad:

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.

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

They can now 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.

For the shooting controls, we used the A button — when it is held down, a new bullet is spawned, and everything else is 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 B button is pressed, the game is paused:

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

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 and desktop, and adding gamepad controls is just as straightforward — in the update() function, we check to see if the current state status is paused — if so, 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 restarts 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 unpauses the game and the Back button goes back, just like before.

Summary

That's it! We have successfully implemented gamepad controls in our 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 with a gamepad.

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 into your microphone.

 

Document Tags and Contributors

 Contributors to this page: chrisdavidmills, end3r
 Last updated by: chrisdavidmills,