DEV Community

Cover image for Playing with the Gamepad API
Alvaro Montoro
Alvaro Montoro

Posted on • Originally published at alvaromontoro.com

Playing with the Gamepad API

I am not a gamer. Maybe I was at once in my life, but not anymore. Even then, I was not that great of a player. But two things happened that encouraged me to start using gamepads on my computer. The first was the nostalgia of playing 90s/early 2000s video games, which drove me to purchase a set with a Raspberry Pi, a case, and a few controllers. Building a RetroPie became a personal project.

The second one was an afternoon of "boredom." I wanted to develop, but I was running out of ideas. So, I decided to explore something new. I navigated to the Web APIs page on MDN, and something caught my eye on the letter g: Gamepad API.

Curiosity killed the cat...

A standard API to access and control gamepads? And supported by all major browsers even when listed as "experimental?" Unexpected... but interesting. It caught my attention and my curiosity.

I had a computer, a game controller, JavaScript knowledge, and a few spare hours... What did I have to lose?

After reading the first page, it looked simple: it only had a handful of interfaces, events, and methods to play with. Nothing that I couldn't handle... or so I thought. I jumped to the MDN tutorial and simplified the first code example a little:

window.addEventListener("gamepadconnected", function() {
  console.log("Gamepad Connected");
});
Enter fullscreen mode Exit fullscreen mode

A bit of theory: The gamepadconnected event is triggered when a gamepad is connected to the browser. Similarly, there is a gamepaddisconnected event that is triggered when a gamepad is disconnected. And with this paragraph, you've learned about the only two events available on the API. Now back to our story.

The page loaded on the browser, and I giddily plugged the RetroPie controller into my computer and...

Nothing.

Unplugged the gamepad. Replugged it, and...

Nothing.

Unplugged. Replugged.

Nothing.

When I was going to return the gamepad to its rightful position by the console and move on to something else, I pressed some of the buttons, and something happened. A message showed up on the console:

Gamepad Connected
Enter fullscreen mode Exit fullscreen mode

I reloaded the page, pressed a button, and the "Gamepad Connected" message popped up again on the console. I learned the first lesson of many with the Gamepad API: not all controllers connect to the browser as soon as they are connected to the computer. Most of them only activate after pressing a button or moving the joysticks.

Getting started: what's supported?

Now that I knew Chrome, the browser I was using, supported the Gamepad API, my next step was to test it with different browser operating systems. I tried on a Mac and a Windows machine, different browsers, and the same browser on different OSs, and for an experimental API, it is widely supported. It even runs on Edge on Windows!

Table with logos and versions: Chrome 25+, Edge 12+, Firefox 29+, Opera 24+, Safari 10.1+

Some features may even be available in previous versions. Still, all the ones mentioned in this article need the browsers shown in the table above (except the vibration, for which support is inconsistent, as we'll see in a little bit).

The next thought was: which controllers would work with it? I had tested with a knockoff gamepad that came with the RetroPie, but I had controllers for PS1, PS2, PS3, and Xbox One. (In hindsight, I have too many consoles around for someone who claims not to be a gamer.) Would the original console controllers work, too?

Short answer: Yes.

Long answer: Some do, some don't. For example, I didn't have problems with the PlayStation controllers (independently of the version) or with the Nintendo Switch controllers. Some friends tested their Wii controllers with a demo page, and they worked like a charm, too. Xbox controllers were a different story. It may be because they need more power; it may be that the versions that we tested were not correct. But we were unable to make any of them work.

...This is interesting, considering all the knockoff gamepads worked great, albeit with some caveats I'll explain later.

The Gamepad interface

The next step was expanding the example and exploring the Gamepad interface. Knowing that the gamepadconnected event passes the connected gamepad information as part of the parameter to the callback function. I logged that object so I could see its content:

window.addEventListener("gamepadconnected", function(e) {
  console.log("Gamepad Connected");
  console.log(e.gamepad);
});
Enter fullscreen mode Exit fullscreen mode

I was expecting something that matched the definition of the Gamepad interface:

interface Gamepad {
  id: String,
  index: Long,
  connected: Boolean,
  timestamp: Timestamp,
  mapping: enum("standard", ""),
  axes: Array<double>
  buttons: Array<GamepadButton>
}
Enter fullscreen mode Exit fullscreen mode

But the result came up with some additional information that looked promising:

{
  id: "USB gamepad            (Vendor: 081f Product: e401)",
  index: 0,
  connected: true,
  timestamp: "2007.0849999901839"
  mapping: "",
  vibrationActuator: null,
  axes: [-0.003921568393707275, -0.003921568393707275],
  buttons: [
    {pressed: false, touched: false, value: 0},
    {pressed: true, touched: true, value: 1},
    {pressed: false, touched: false, value: 0},
    {pressed: false, touched: false, value: 0},
    {pressed: false, touched: false, value: 0},
    {pressed: false, touched: false, value: 0},
    {pressed: false, touched: false, value: 0},
    {pressed: false, touched: false, value: 0},
    {pressed: false, touched: false, value: 0},
    {pressed: false, touched: false, value: 0},
  ]
}
Enter fullscreen mode Exit fullscreen mode

In which:

  • id is a string that identifies the make/model/type of controller.
  • index is the unique identifier for a gamepad assigned on connection (basically the order in which it was connected.)
  • connected indicates the status of the gamepad.
  • timestamp is a bit tricky, as it is not for the time that the gamepad was connected but the timestamp of the last time the gamepad data was updated.
  • mapping, we'll see in the next section, to specify if the button mapping is standard or not.
  • axes is an array with the values of the different axes/joysticks on the gamepad. We'll see it later.
  • buttons is an array of buttons.

There were some things that I still didn't have clear: the connected gamepad showed ten buttons and two axis, but looking at the physical device, I could see 12 buttons and no axis. It was a bit weird. Soon, I'd find out why that happened.
Meanwhile, I was familiarizing myself with the gamepad interface and was ready for the fun part.

Buttons

I could detect a gamepad being connected/disconnected and read its values and properties. But that's not practical in itself. I wanted to move on to more exciting things.

We just saw that the Gamepad object has a buttons property that contains an array of buttons. These buttons have their own interface (GamepadButton) which is an object with three read-only values:

interface GamepadButton {
  pressed: Boolean,
  touched: Boolean,
  value: Double
}
Enter fullscreen mode Exit fullscreen mode

They are more or less self-explanatory:

  • pressed indicates if the button is being pressed or not. It will be true while the button is pressed, not when it goes down.
  • touched indicates if a button is being touched. (Not all gamepads will have this feature.)
  • value is for buttons with an analog sensor. It will represent how much pressure is applied on the button: 0.0 means not pressed at all, and 1.0 means completely pressed.

The buttons are sorted in the array by order of importance as defined in the diagram below so that they can be easily mapped:

drawing of a gamepad with numbers on the buttons

Buttons and axis standard distribution as defined in the Gamepad API
 

But not all gamepads follow the same button/axis pattern. That is why it is essential to know about the button mapping.

Mapping

mapping is a property of the Gamepad interface that indicates if the browser can identify and map the controller correctly. If that's the case, the value of mapping will be "standard."

Most original controllers I tested and worked on had a standard mapping. Most of the knockoffs that I tried didn't have a standard mapping. In those cases, the developer must ensure that the pressed buttons match the user's expectations.

Trick: Sometimes (e.g. when using a PS2 controller) the browser will detect that the gamepad is standard, but the buttons will not match the standard diagram. If that happens, make sure that the controller's "Analog" functionality is activated.

Something was missing, though. I couldn't see any event being triggered when one of the buttons was pressed. Not in the documentation (where only two events are listed), nor on the gamepad object. Time to continue reading the tutorial and documentation.

Event listening vs Event querying

This was one of the things that took me a little bit longer to understand. We have already seen that there are only two events in the Gamepad API definition (gamepadconnected and gamepaddisconnected), and buttons don't have any events associated with them... So, how do the events work?

Simply put, they don't... because there aren't any events. In contrast to other APIs and elements you can associate and listen to events, the Gamepad API works differently. Without any events to listen to, developers must continuously query the gamepads to see if any changes have happened.

To achieve this, there is the getGamepads method as an extension of the Navigator interface. getGamepads will return an array with snapshots of the connected gamepads and their status:

const gamepads = navigator.getGamepads();
Enter fullscreen mode Exit fullscreen mode

Later, to support some older webkit browsers, I added a fallback to an older initialization. Either way, if the getGamepads() methods are not supported, or the gamepads were disconnected, returning an empty array is a good idea to avoid errors:

let gamepads = [];
if (navigator.getGamepads) gamepads = navigator.getGamepads();
else if (navigator.webkitGetGamepads) gamepads = navigator.webkitGetGamepads();
Enter fullscreen mode Exit fullscreen mode

I could read the status of the connected gamepads, but it was a snapshot of the status from when I called the function. I needed to be querying the status of the gamepads continuously! Instead of using something like setTimeout or setInterval that could skip animation cycles, I had to call functions within requestAnimationFrame so that it was executed every time the browser was about to repaint the screen...

...something like this:

function checkStatus() {
  // Read the status of the gamepads
  const gamepads = navigator.getGamepads();

  // Operate with the gamepad: read button values, perform actions, etc.
  // Example: log message while Start button in first gamepad is pressed
  if (gamepads[0].buttons[9].pressed) {
    console.log("Start button is pressed");
  }

  // Re-execute the function with each animation frame
  if (gamepads.length > 0) {
    window.requestAnimationFrame(checkStatus);
  }
}
Enter fullscreen mode Exit fullscreen mode

This function would be called on the gamepadconnected event handler to start querying only when there's a gamepad connected to the browser. Also, it is essential to add a stop condition if no gamepads are connected. Otherwise, the app's efficiency will suffer by performing continuous unnecessary queries.

Joysticks are not buttons

Another discovery for me was how directional buttons (joysticks or axis) work. I must admit I was expecting them to behave like buttons, with pressed/not pressed, touched/not touched... but the axes property in Gamepad is just an array with an even number ranging from -1.0 to 1.0. Not even close to how the buttons look.

The trick is to divide that array into groups of two. Each group will be an axe/joystick in the gamepad:

  • The first value represents the X axe of the joystick, then -1.0 means left, and 1.0 represents right.
  • The second value represents the Y axe of the joystick, in which -1.0 means up/forwards, and 1.0 represents down/backward.

joystick diagram with arrows

Diagram of the joystick axes and values, with an example.
 

Translated into code, it would be something like this:

// Read the status of the gamepads
const gamepads = navigator.getGamepads();

// Example: log message while directional joystick in first gamepad is pressed
// horizontal movement
if (gamepads[0].axes[0] == 1.0) {
  console.log("Move right");
} else if (gamepads[0].axes[0] == -1.0) {
  console.log("Move left");
}

// vertical movement
if (gamepads[0].axes[1] == 1.0) {
  console.log("Move down");
} else if (gamepads[0].axes[1] == -1.0) {
  console.log("Move up");
}
Enter fullscreen mode Exit fullscreen mode

Beware: While everything seemed to work fine on my side, some people reported that the demos were not working. After some tests, the culprit was the browser: Firefox detects an additional axis, and you will have to make up for that!

Sensitivity threshold

A nice thing to do while developing for a joystick/axis is to allow different sensitivity thresholds. Not all joysticks are created equal, and not everyone has the same likings or needs for how a joystick must behave.

The value for the axis is a double, ranging between -1.0 and 1.0, but that doesn't mean that 0.0 is the at-rest status and 1.0/-1.0 is active. The rest status was never 0 in any of the gamepads that I have tested. (Most of them have a negligible value like 0.0003.) So, why must 1.0/-1.0 be the threshold that triggers the directional action?

For accessibility and usability reasons, consider allowing users to change the threshold in which the directional event is triggered. Modified snippet from the example above:

const threshold = 0.5;

// vertical movement (triggered "half way" instead of full movement)
if (gamepads[0].axes[1] >= threshold) {
  console.log("Move down");
} else if (gamepads[0].axes[1] == -threshold) {
  console.log("Move up");
}
Enter fullscreen mode Exit fullscreen mode

Vibration

The Gamepad API has an extension to allow controller vibration when available. If the API in itself is experimental, this extension could be considered experimental to the square.

If you looked into the console message logged when the gamepad was connected, you might have noticed a property that was not described as part of the Gamepad interface: vibrationActuator. That has a method playEffect() that will allow you to make the game controller vibrate.

It's just that there's a big problem: that is not the standard extension to control the vibration, but rather the one that is available on Chrome. The standard way is using hapticActuators, which is available in some other browsers, notably Firefox.

For this example, I will focus only on the standard hapticActuators.

hapticActuators only allows one value at the moment ("vibration") and contains the pulse method that will permit us to trigger vibration specifying intensity and duration:

// read the first gamepad
const gamepads = navigator.getGamepads();
const myGamepad = gamepads[0];

// trigger a max intensity vibration for 1.5 seconds
myGamepad.hapticActuator[0].pulse(1.0, 1500);
Enter fullscreen mode Exit fullscreen mode

One tricky thing about the hapticActuators is that when I found it, instead of being an array of GamepadHapticActuators as defined in the standard, it was a single object of that type. Implementation is still really dependent on the browser. Developer beware.

Developing a Library

As you may have noticed, the Gamepad API is relatively easy but cumbersome, as every action needs multiple steps. If I wanted to explore and play with it more, I would need to simplify the experience.

Creating a small module to provide a higher-level interface to all these methods and events made sense. Something that would simplify every action and allow for more standard-looking calls.

For example, if I wanted to check if the Start button was pressed, I had to do this with the Gamepad API:

  • Set up an interval with requestAnimationFrame.
  • Call getGamepad() in each animation frame.
  • Identify the gamepad I want to check (with an ID previously saved).
  • Read the buttons array.
  • Access the particular button I want (button 9 for Start).
  • Read the value of the pressed property.
  • Perform the action I want.

Each of those steps would take several lines of code. Moving that complexity to a library/module would allow us to do something similar in a simpler jQuery-ish looking kind of way:

myGamepad.on("start", function() {
  console.log("The Start button is pressed");
});
Enter fullscreen mode Exit fullscreen mode

All the code needed would still be there, but behind the scenes, facilitating the use of the Gamepad API and making it look like other APIs in which events are listened to instead of queried.

Developing a game

The library simplified the process. Now, I could focus more on the other parts of the web applications while using a friendlier interface in JavaScript for the gamepads' functionality.

An easy game to develop was the classic Pong. The interaction with the controllers is simple: up or down. I just had to calculate the ball's movement while dealing with the main difficulty of detecting the collision of the ball with the paddle.

Here is the code and a demo (connect your gamepads to play):

If you don't have gamepads connected to your computer, that Codepen may not work, although I added some keyboard functionality, too.

What's next?

In this article, we have touched on most of the current features of the Gamepad API:

  • How to listen for a game controller connection
  • The difference between buttons and joysticks
  • How to read the events
  • Use of vibration with the gamepad

But we left some things out: the Gamepad Pose interface, which would allow us to get information from gamepads such as position, orientation, velocity, and acceleration (if available). This would be great for augmented reality and virtual reality devices. Unfortunately, it's not well supported.

Also, new changes will come to the API. After all, it is an experimental technology, and it is continuously updated. New events could be added, such as gamepadchange, gamepadaxischange, which would simplify the API... and make my library obsolete.

Extra: Be a Gamepad API Rockstar!

After testing the different types of controllers that work with the Gamepad API, I had an idea: if my old PS3 controllers worked, how about my old Rock Band drums and guitar? How about the Dance Dance Revolution mat?

Long story short, these are the results:

...and here we are playing our own version of "Web DDR":


I created a tutorial on how to make your personalized version of a Rock Band video game using JavaScript and HTML in this other article.

Top comments (5)

Collapse
 
raddevus profile image
raddevus

That's a great article and a very cool library.
I've starred it on GitHub. Thanks for sharing this fantastic article and code.
I was able to try the Pong game with my attached GamePad via Brave web browser (chrome engine) running on Ubuntu 22.04.3 LTS. 👍🏽
I wish the example allowed me to control both paddles with different controls or something because I only have 1 gamepad. Maybe you could switch the sample to use the dpad (four arrow buttons to control left pong paddle and joystick to control right pong paddle). Just an idea.
As it stands, my left joystick controls the left pong paddle.

Thanks again.

Collapse
 
alvaromontoro profile image
Alvaro Montoro

Thank you!
About the game, you can control both paddles with the keyboard: one uses Q/A to got up/down, and the other one uses the arrow keys (only up/down). I thought I had it set up for two controllers, I will double check as I may have shared the wrong demo 😅

Collapse
 
sreno77 profile image
Scott Reno

This is very cool. I look forward to seeing what web games people come up with using this library.

Collapse
 
adseng profile image
huangzhaohu • Edited
Collapse
 
adseng profile image
huangzhaohu

would like to use it on tank-online game