Modern web browsers feature support for gaming controllers natively through the Gamepad API. Although it’s only in the W3C Draft stage, many web browsers are already implementing basic support.
While it’s powerful on its own, it was primarily designed for games and lacks the features and ease of use we thought would be helpful for building interactive UIs. In this article we’re going to discuss the capabilities of the Gamepad API and the limitations and missing features which led us to develop Controller.js.
Here’s how the Gamepad API works
A great tool for game development
This approach makes sense in the context of game development. If you’re writing a game, you’ll probably have a render loop to draw the scene each frame using requestAnimationFrame(), so checking a gamepad’s state near the beginning of that loop is a convenient and efficient way to update your game based on user input.
The signals sent from gamepads are broken into two arrays: buttons and axes. Because there are a variety of gamepads in the market, and no actual standards for their layouts, the Gamepad API makes no requirements about the length of these arrays nor the order in which buttons and axes must appear in them. The W3C Draft does define a single layout that it calls the “Standard Gamepad”, based on de facto standards that many gamepads have settled upon, which recommends which indexes the triggers, shoulder buttons, analog sticks, D-pad and other buttons should be mapped to. If a gamepad is recognized by the browser, the indexes will match up with this layout (as seen in the diagram below). Otherwise, they will not, e.g. buttons may not be the right trigger.
What the API is missing
Challenges in prototyping interfaces
What drove us to develop Controller.js were a combination of things that we either A) wanted as general features or B) thought would make interacting with gamepads more “web-like”.
- A consistent naming scheme for inputs and the ability to map non-standard layout gamepads to those names
- An event-driven system for buttons and analog sticks more consistent with other web development paradigms
- A simple way to treat analog stick movements as if they were D-pad presses (and other configurable settings)
1. Layouts and input names
One downside of the Gamepad API’s hands-off approach to buttons and axes is that you have no way to identify one beyond it’s index in an array. As a developer, you have to keep track of which input each button or axis input is supposed to map to. And that’s only if the browser recognizes that specific gamepad in the first place. If it doesn’t, the indexes are next to useless unless you know exactly which gamepad a user has connected and understand it’s layout yourself.
Controller.js tries to help the developer out in two ways. The first is assigning descriptive names to each input defined in the Standard Gamepad layout, and the second is maintaining a list of gamepads and instructions for how to map their unique layouts to those names that can be used as a fallback if that gamepad is not natively recognized by the browser.
When a gamepad is connected the library checks if it is recognized by the browser, and if so maps names to the correct inputs according to the Standard Gamepad layout. If not, it searches for a match in the Controller.layouts.js add-on and uses that layout to correctly assign names to the inputs.
One area in which Controller.js differs more fundamentally from the Gamepad API is in the treatment of analog sticks. The Gamepad API represents each as two separate axes, one for horizontal position and the other for vertical. Controller.js combines each pair of axes into a single input, an “analog stick” with an X and Y value. As a result, a typical gamepad will report two analog sticks (LEFT_ANALOG_STICK and RIGHT_ANALOG_STICK), instead of four axes.
As we mentioned earlier, the Gamepad API expects you to set up a loop to check out the state of your gamepad periodically to see when buttons are pressed or analog sticks are moved. There are benefits to this method in game development, but for interface building, it comes with a lot of unneeded overhead. With Controller.js, we wanted to introduce the concept of DOM Events to gamepads so that developers can interact with them in the same way they would a keyboard or mouse.
The library provides a few events each for buttons and analog sticks. Buttons have events for press, hold and release while analog sticks have start, hold, change and end. The library does the heavy lifting of comparing an input’s value changes over time to determine what is happening and dispatch the appropriate event in each situation. This frees you, the developer, to focus on your app.
When an event fires, it passes data back to your application. Some information—like the index of the gamepad that fired the event, the time the event fired and the name of the input—are available for both buttons and analog sticks, but each has it’s own unique properties as well.
- pressed tells you whether the button is pressed or not
- value is a number between 0 and 1 telling you how far the button has been pressed (pressure sensitive buttons such as triggers can have values between 0 and 1)
- position.x and position.y are the horizontal and vertical axis values, each from -1 to 1
- angle.degrees is the angle the stick is pressed in degrees, with 0° being right, 90° being up, and so on
- angle.radians is also the angle the stick is pressed but represented in radians, with right at 0 and left at π (~3.14)
Something we noticed with our prototype was that people would inevitably try to navigate the UI using the analog sticks instead of the D-pad. It was such a common occurence that it led us to add an option to the library that would translate analog stick inputs into D-pad button presses. We went with this method so that once you set that option, the event handlers you already have registered on the D-pad will just begin working.
This led to a more robust system of controller settings that could be set and tweaked at runtime. For example, we found it helpful to set a threshold for what value on a button constitues a “press”, or how far an analog stick needed to be moved in any direction for it to register as a D-pad press.
Building support for additional gamepads
Support is a mixed bag. First, your operating system has to recognize the gamepad. Some will work out of the box, others may have official drivers which need to be installed, and still others you can only get to work with unofficial, third party drivers or not at all.
If the OS does recognize the gamepad, it’s then up to the browser you’re using to recognize it. There are three possibilities:
- It sees the gamepad and recognizes it (Standard Gamepad layout)
- It sees the gamepad but does not recognize it (unknown layout)
- It doesn’t see the gamepad at all
With the third possibility, your gamepad is essentially invisible to the Gamepad API and, by extension, Controller.js.
The second possibility is where things can get confusing, and it’s why the Controller.layouts.js extension exists. It’s a brute-force method of making sense of the gamepads your browser sees but does not understand by creating a list of gamepads and how their inputs are mapped.
To illustrate this, let’s look at the Xbox One Controller. As of the time of writing this, Windows officially supports this gamepad. Using Edge on Windows 10, it will be visible and conform to the Standard Gamepad layout. However, there is no official driver for macOS, so we turned to one from a third party. With this installed, both Chrome and Firefox recognize the gamepad but both reported different layouts (neither of them the Standard Gamepad layout). Controller.layouts.js contains support for both of these layouts and knows which to use based on the user’s browser.
A quick-start guide to Controller.js
Let’s take a look at a few examples to get you using and understanding the basics of Controller.js quickly.
Including the Controller.js library will make a global Controller object available in your code. Before gamepads will start dispatching events, you have to tell Controller to look for them with the search() method.
Once you run this method, any gamepads you connect will become available and begin reporting events, starting with gc.controller.found, to let you know it’s been discovered.
Reacting to button events
The most important feature of the library is being able to do stuff in response to a user’s interaction.
Controller.js supports settings which can change the behavior of gamepads. They can be applied either to individual gamepads, or globally to all gamepads.
A particularly useful setting is useAnalogAsDpad, which makes one or both analog sticks fire D-pad button events when they’re pressed beyond a threshold in any direction.