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.