As web-based gaming gets more popular, one of the biggest sticking points for players is input control. While my first FPS games were purely mouse- and keyboard-based, I've now got so much more used to a proper console controller that I'd rather use it for everything, including web-based games.
Luckily, the HTML5 Gamepad API exists to allow web developers programmatic access to game controllers. Unluckily, although this API has been around for a long time, it is only now, slowly, moving into the most recent versions of desktop browsers. It languished for a long time in one build of Firefox (not one build higher, no—one nightly build) and was problematic in Chrome. Now it is—well—not perfect, but slightly less problematic and actually pretty easy to use.
In this article, I'll discuss the various features of the API, how to get it working in both Firefox and Chrome, and show a real (if simple) game and how easy it is to add gamepad support to it.
The Basics
The Gamepad API comprises the following features:
- The ability to listen for
connect
anddisconnect
events. - The ability to recognize multiple gamepads. (In theory, you could plug in as many gamepads as you have USB ports.)
- The ability to inspect these gamepads and recognize how many axes they have (joysticks), how many buttons they have (have you played a modern game console lately?), and what state each of these individual items are in.
Let's begin by discussing how you can detect support for a gamepad at a high level.
Both Firefox and Chrome support a method on navigator
, getGamepads()
, that returns an array of all connected gamepad devices. We can use this as a simple method of detecting whether the Gamepad API is present. Here is a simple function for that check:
function canGame() { return "getGamepads" in navigator; }
So far so good. Now for the funky part. The Gamepad API has support for events that detect when a gamepad is connected and disconnected. But what happens if the user already has a gamepad connected to their laptop when they hit your page? Normally, the web page will wait for the user to do something, anything really, with the actual gamepad. This means we have to provide some type of message to the user that lets them know that they need to "wake up" support for the gamepad if it is connected. You could tell them to hit any button or move a stick.
To make things even more interesting, this particular check does not seem to be required when you reload the page. You'll find that once you've used the Gamepad API on a page and then reloaded it, the page recognizes this fact and automatically considers it connected.
But wait—it gets better. Chrome doesn't support the connected (or disconnected) events at this time. The typical work around for this (and the one demonstrated in the good MDN docs for the API) is to set up a poll and see whether a gamepad "shows up" in the list of connected devices.
Confusing? Let's start off with an example supporting Firefox only:
<!DOCTYPE html><html><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"><title></title><meta name="description" content=""><meta name="viewport" content="width=device-width"><script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script></head><body><div id="gamepadPrompt"></div><script> function canGame() { return "getGamepads" in navigator; } $(document).ready(function() { if(canGame()) { var prompt = "To begin using your gamepad, connect it and press any button!"; $("#gamepadPrompt").text(prompt); $(window).on("gamepadconnected", function() { $("#gamepadPrompt").html("Gamepad connected!"); console.log("connection event"); }); $(window).on("gamepaddisconnected", function() { console.log("disconnection event"); $("#gamepadPrompt").text(prompt); }); } }); </script></body></html>
In the example above, we begin by checking to see whether the browser supports the Gamepad API. If it does, we first update a div with instructions for the user, and then begin listening immediately to both the connect
and disconnect
events.
If you run this with Firefox and connect your gamepad, you should then have to also hit a button, at which point the event is fired and you're ready to go.
Again, though, in my testing, when I reload the page, the connection
event is immediate. This does create a slight "flicker" effect that may be undeseriable. You could actually use an interval to set the directions for something like 250ms after the DOM has loaded and only prompt if a connection didn't occur in the meantime. I decided to keep things simple for this tutorial.
Our code works for Firefox, but now let's add Chrome support:
<!DOCTYPE html><html><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"><title></title><meta name="description" content=""><meta name="viewport" content="width=device-width"><script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script></head><body><div id="gamepadPrompt"></div><script> var hasGP = false; function canGame() { return "getGamepads" in navigator; } $(document).ready(function() { if(canGame()) { var prompt = "To begin using your gamepad, connect it and press any button!"; $("#gamepadPrompt").text(prompt); $(window).on("gamepadconnected", function() { hasGP = true; $("#gamepadPrompt").html("Gamepad connected!"); console.log("connection event"); }); $(window).on("gamepaddisconnected", function() { console.log("disconnection event"); $("#gamepadPrompt").text(prompt); }); //setup an interval for Chrome var checkGP = window.setInterval(function() { if(navigator.getGamepads()[0]) { if(!hasGP) $(window).trigger("gamepadconnected"); window.clearInterval(checkGP); } }, 500); } }); </script></body></html>
The code is a bit more complex now, but not terribly so. Load the demo in Chrome and see what happens.
Note that we've got a new global variable, hasGP
, that we'll use as a general flag for having a gamepad connected. As before, we have two event listeners, but now we've got a new interval set up to check to see whether a gamepad exists. This is the first time you've seen getGamepads
in action, and we'll describe it a bit more in the next section, but for now know that it just returns an array, and if the first item exists, we can use that as a way of knowing that a gamepad is connected.
We use jQuery to fire off the same event Firefox would have received, and then clear the interval. Notice that this same interval will fire once in Firefox as well, which is slightly wasteful, but honestly I thought it was a waste of time adding in additional support to sniff Chrome versus Firefox. One small call like this wasted in Firefox should not matter at all.
Now that we've got a connected gamepad, let's work with it!
The Gamepad Object
To give you an idea of just how old I am - here is the state of the art joystick I used for my first gaming system.
Nice - simple - and it hurt like hell after an hour of playing. Modern consoles have much more complex gamepads. Consider the PS4 controller:
This controller has two sticks, a directional pad, four main buttons, four more on the back, a Share and Options button, a PS button, some funky touch control thing, a speaker, and a light. It also probably has a flux capaciter and a kitchen sink.
Luckily, we've got access to this beast via the Gamepad object. Properties include:
id
: This is the name of the controller. Don't expect something friendly from this. My DualShock 4 was reported as54c-5c4-Wireless Controller
in Firefox, whereas Chrome called the same controllerWireless Controller (STANDARD GAMEPAD Vendor: 054c Product: 05c4)
.index
: Since the Gamepad API supports multiple controllers, this one lets you determine which numbered controller it is. It could be used to identify player one, two, and so on.mapping
: Mapping isn't something we're going to cover here, but essentially this is something the browser can do to help map your particular controller to a "standard" controller setup. If you've played multiple consoles you know they have some similarities in terms of control, and the API tries to "mash" your controller into a standard. You don't have to worry about this for now, but if you want more details, check the mapping section of the API docs.connected
: A Boolean indicating whether the controller is still connected.buttons
: An array of button values. Each button is an instance of GamepadButton. Note that theGamepadButton
object supports both a simple Boolean property (pressed
) as well as avalue
property for analog buttons.axes
: An array of values representing the different sticks on the gamepad. Given a gamepad with three sticks, you will have an array of six items, where each stick is represented by two array values. The first in the pair represents X, or left/right movement, while the second represents Y, up/down movement. In all cases the value will range from-1
to1
: for left/right values,-1
is left and1
is right; for up/down values,-1
is up and1
is down. According to the API, the array is sorted according to "importance", so in theory, you can focus onaxes[0]
andaxes[1]
for most gaming needs. To make things more interesting, using my DualShock 4, Firefox reported three axes (which makes sense—see the picture above), but Chrome reported two. It seems as if the d-pad stick is reported in Firefox as an axis, but no data seems to come out of it. In Chrome, the d-pad showed up as additional buttons, and was correctly read.timestamp
: Finally, this value is a timestamp representing the last time the hardware was checked. In theory, this is probably not something you would use.
Okay, so that's lot to digest. In the example below, we've simply added a interval to get, and inspect, the first gamepad, and print out the ID and then the buttons and axes:
<!DOCTYPE html><html><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"><title></title><meta name="description" content=""><meta name="viewport" content="width=device-width"><script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script></head><body><div id="gamepadPrompt"></div><div id="gamepadDisplay"></div><script> var hasGP = false; var repGP; function canGame() { return "getGamepads" in navigator; } function reportOnGamepad() { var gp = navigator.getGamepads()[0]; var html = ""; html += "id: "+gp.id+"<br/>"; for(var i=0;i<gp.buttons.length;i++) { html+= "Button "+(i+1)+": "; if(gp.buttons[i].pressed) html+= " pressed"; html+= "<br/>"; } for(var i=0;i<gp.axes.length; i+=2) { html+= "Stick "+(Math.ceil(i/2)+1)+": "+gp.axes[i]+","+gp.axes[i+1]+"<br/>"; } $("#gamepadDisplay").html(html); } $(document).ready(function() { if(canGame()) { var prompt = "To begin using your gamepad, connect it and press any button!"; $("#gamepadPrompt").text(prompt); $(window).on("gamepadconnected", function() { hasGP = true; $("#gamepadPrompt").html("Gamepad connected!"); console.log("connection event"); repGP = window.setInterval(reportOnGamepad,100); }); $(window).on("gamepaddisconnected", function() { console.log("disconnection event"); $("#gamepadPrompt").text(prompt); window.clearInterval(repGP); }); //setup an interval for Chrome var checkGP = window.setInterval(function() { console.log('checkGP'); if(navigator.getGamepads()[0]) { if(!hasGP) $(window).trigger("gamepadconnected"); window.clearInterval(checkGP); } }, 500); } }); </script></body></html>
You can try the demo in either Chrome or Firefox.
I assume this is all pretty self explanatory; the only real difficult part was handling the axes. I loop over the array and count by twos to represent both the left/right, up/down values at once. If you open this up in Firefox and connect a DualShock, you may see something like this.
As you can see, Button 2 was pressed when I took my screenshot. (In case you're curious, that was the X button.) Note the sticks; my gamepad was sitting on my laptop and those values were constantly fluctuating. Not in a way that would imply the values were bad, per se—if I picked up the game pad and pushed all the way in one direction, I saw the right value. But I believe what I was seeing was just how sensitive the controller is to the environment. Or maybe gremlins.
Here is an example of how Chrome displays it:
I was, again, holding the X button—but notice how the button index is different here. As you can tell, you're going to need to do a bit of... massaging if you want to use this API for a game. I'd imagine you could check both Buttons 1 and 2 for "fire" and follow up with a good deal of testing.
Putting It All Together
So, how about a real demo? Like most coders who started their life playing video games, I dreamed of being a hotshot video game creator when I grew up. It turns out that math gets real hard after calculus, and apparently this "web" stuff has a future, so while that future didn't pan out for me, I'd still like to imagine that one day I could turn these web standards skills into a playable game. Until that day, what I've got today is a pretty lame canvas-based version of pong. Single-player pong. As I said, lame.
The game simply renders a paddle and a ball, and gives you keyboard control over the ball. Every time you miss the ball, the score goes up. Which makes sense for golf rather than pong, I suppose, but let's not worry too much about it. The code can be found in game1.html and you can play the demo in your browser.
I won't go through all the code here, but let's look at a few snippets. First, here is the main loop function which handles all animation details:
function loop() { draw.clear(); ball.move(); ball.draw(); paddle.draw(); paddle.move(); draw.text("Score: "+score, 10, 20, 20); }
The paddle is driven by the keyboard using two simple event handlers:
$(window).keydown(function(e) { switch (e.keyCode) { case 37: input.left = true; break; case 39: input.right = true; break; } }); $(window).keyup(function(e) { switch (e.keyCode) { case 37: input.left = false; break; case 39: input.right = false; break; } });
The input
variable is a global variable that is picked up by a paddle object move
method:
this.move = function() { if(input.left) { this.x -= this.speed; if(this.x < 0) this.x=0; } if(input.right) { this.x += this.speed; if((this.x+this.w) > canvas.width) this.x=canvas.width-this.w; } }
Again, nothing too complex here. Here is a screenshot of the game in action. (I know—I shouldn't quit my day job.)
So, how do we add gamepad support? Luckily, we've got the code done for us already. In the previous demo, we did everything required to check for and notice updates to the code. We can take that code and simply append it to the game's existing code.
Since it is (virtually) the same, I won't repeat it (though the full listing is available if you want it), but I will share the modified code run every 100ms once a gamepad is detected:
function checkGamepad() { var gp = navigator.getGamepads()[0]; var axeLF = gp.axes[0]; if(axeLF < -0.5) { input.left = true; input.right = false; } else if(axeLF > 0.5) { input.left = false; input.right = true; } else { input.left = false; input.right = false; } }
Again, you can try the demo in either browser.
As with the previous example, we've assumed that we only care about one gamepad. Since our game only has a paddle and it only moves horizontally, we can get by by only checking the very first axis. Remember, according to the API this should be the "most important" one, and in my testing it was the left stick, which is pretty standard for games.
Since our game uses a global variable, input
, to represent left and right movement, all I have to do is modify that value based on the axis value. Now, notice that I didn't simply check for "less than zero" and "greater than zero". Why? If you remember from the earlier demo, the gamepad was very sensitive, and would often report values even when I didn't think I had actually moved the stick. Using a boundary value of .5
gives the control a bit more stability. (And obviously this is the type of thing you would need to tweak to see what "feels" right.)
All in all, I added roughly 25 lines of code to my game to add gamepad support. That rocks.
Game On!
Hopefully you've seen that, while there are definitely some idiosyncrasies, the Gamepad API now has support in two major browsers, and it's something I think developers really should start considering for their games.
Resources
Here's a few additional resources to help you learn more about the Gamepad API.
- Using the Gamepad API
- Gamepad Specification
- Gamepad.js - A Javascript library to enable using gamepads and joysticks in the browser.
- Gamepad Controls for HTML5 Games
- Wii U's Version (totally different from the specification - good job, Nintendo!)
References
- Preview image credit: Video Game Controller designed by Uriel Sosa from the Noun Project