Playing with the Gamepad API

By admin
My journey programming with the GamePad API with detailed explanations and code to get JavaScript games ready to use controllers on the browser

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
CARDINAL

things happened that encouraged me to start using gamepads on my computer. The

first
ORDINAL

was the nostalgia of playing

90s/early 2000s
DATE

video games, which drove me to purchase a set with a Raspberry Pi, a case, and a few controllers. Building a

RetroPie
ORG

became a personal project.

The

second
ORDINAL

one was an

afternoon
TIME

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
ORG

, 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
TIME

… What did I have to lose?

After reading the

first
ORDINAL

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
ORG

tutorial and simplified the

first
ORDINAL

code example a little:

window .

addEventListener
PERSON

( " gamepadconnected " , function () { console . log ( "

Gamepad Connected
WORK_OF_ART

" ); });

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
CARDINAL

events available on the

API
ORG

. Now back to our story.

The page loaded on the browser, and I giddily plugged the

RetroPie
ORG

controller into my computer and…

Nothing.

Unplugged the gamepad. Replugged it, and…

Nothing.


Unplugged
PERSON

. 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

I reloaded the page, pressed a button, and

the "Gamepad Connected
EVENT

" message popped up again on the console. I learned the

first
ORDINAL

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
ORG

, the browser I was using, supported

the Gamepad API
EVENT

, my next step was to test it with different browser operating systems. I tried on a

Mac
ORG

and a

Windows
PRODUCT

machine, different browsers, and the same browser on different OSs, and for an experimental API, it is widely supported. It even runs on

Edge
ORG

on Windows!

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
ORG

, but I had controllers for PS1,

PS2
ORG

, PS3, and

Xbox One
PRODUCT

. (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
PRODUCT

controllers (independently of the version) or with the

Nintendo Switch
ORG

controllers. Some friends tested their

Wii
ORG

controllers with a demo page, and they worked like a charm, too.

Xbox
ORG

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
ORG

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
PERSON

( " gamepadconnected " , function ( e ) { console . log ( "

Gamepad Connected
WORK_OF_ART

" ); console . log ( e . gamepad ); });

I was expecting something that matched the definition of the

Gamepad
ORG

interface:

interface Gamepad { id :

String
PERSON

, index : Long , connected : Boolean , timestamp : Timestamp , mapping : enum ( " standard " , "" ), axes : Array < double > buttons : Array < GamepadButton > }

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

{ id : " USB gamepad (Vendor: 081f Product: e401) " , index :

0
CARDINAL

, connected : true , timestamp : " 2007.0849999901839 " mapping : "" , vibrationActuator : null , axes : [ – 0.003921568393707275 , –

0.003921568393707275
CARDINAL

], buttons : [ { pressed : false , touched : false , value :

0
CARDINAL

}, { pressed : true , touched : true , value :

1
CARDINAL

}, { pressed : false , touched : false , value :

0
CARDINAL

}, { pressed : false , touched : false , value :

0
CARDINAL

}, { pressed : false , touched : false , value :

0
CARDINAL

}, { pressed : false , touched : false , value :

0
CARDINAL

}, { pressed : false , touched : false , value :

0
CARDINAL

}, { pressed : false , touched : false , value :

0
CARDINAL

}, { pressed : false , touched : false , value :

0
CARDINAL

}, { pressed : false , touched : false , value :

0
CARDINAL

}, ] }

In which:

id is a string that identifies the make/model/type of controller.

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.)

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.

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.

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.

, 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.

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
CARDINAL

buttons and

two
CARDINAL

axis, but looking at the physical device, I could see

12
CARDINAL

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
EVENT

object has a buttons property that contains an array of buttons. These buttons have their own interface (

GamepadButton
ORG

) which is an object with

three
CARDINAL

read-only values:

interface

GamepadButton
ORG

{ pressed : Boolean , touched : Boolean , value : Double }

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.

indicates if the button is being pressed or not. It will be 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.)

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
CARDINAL

means not pressed at all, and

1.0
CARDINAL

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:

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
ORG

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
PRODUCT

" functionality is activated.

Something was missing, though. I couldn’t see any event being triggered when

one
CARDINAL

of the buttons was pressed. Not in the documentation (where

only two
CARDINAL

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
CARDINAL

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
PERSON

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
ORG

interface. getGamepads will return an array with snapshots of the connected gamepads and their status:

const gamepads = navigator . getGamepads ();

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 ();

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
ORDINAL

gamepad is pressed if ( gamepads [ 0 ]. buttons [

9
CARDINAL

]. pressed ) { console . log ( " Start button is pressed " ); } // Re-execute the function with each animation frame if ( gamepads . length > 0 ) { window . requestAnimationFrame (

checkStatus
ORG

); } }

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
DATE

is just an array with an even number ranging from

-1.0
CARDINAL

to

1.0
CARDINAL

. Not even close to how the buttons look.

The trick is to divide that array into groups of

two
CARDINAL

. Each group will be an axe/joystick in the gamepad:

The

first
ORDINAL

value represents the X axe of the joystick, then

-1.0
CARDINAL

means left, and

1.0
CARDINAL

represents right.

The

second
ORDINAL

value represents the Y axe of the joystick, in which

-1.0
CARDINAL

means up/forwards, and

1.0
CARDINAL

represents down/backward.


Diagram
ORG

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
ORDINAL

gamepad is pressed // horizontal movement if ( gamepads [ 0 ]. axes [ 0 ] ==

1.0
CARDINAL

) { console . log ( " Move right " ); } else if ( gamepads [ 0 ]. axes [ 0 ] == – 1.0 ) { console . log ( " Move left " ); } // vertical movement if ( gamepads [ 0 ]. axes [

1
CARDINAL

] ==

1.0
CARDINAL

) { console . log ( " Move down " ); } else if ( gamepads [ 0 ]. axes [

1
CARDINAL

] == – 1.0 ) { console . log ( " Move up " ); }

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
CARDINAL

, but that doesn’t mean that

0.0
CARDINAL

is the at-rest status and

1.0/-1.0
CARDINAL

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
CARDINAL

.) So, why must

1.0/-1.0
CARDINAL

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
CARDINAL

; // vertical movement (triggered "

half
CARDINAL

way" instead of full movement) if ( gamepads [ 0 ]. axes [

1
CARDINAL

] >= threshold ) { console . log ( " Move down " ); } else if ( gamepads [ 0 ]. axes [

1
CARDINAL

] == – threshold ) { console . log ( " Move up " ); }

Vibration


The Gamepad API
WORK_OF_ART

has an extension to allow controller vibration when available. If the

API
ORG

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
ORG

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
ORG

. The standard way is using hapticActuators , which is available in some other browsers, notably

Firefox
ORG

.

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

hapticActuators only allows

one
CARDINAL

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

// read the

first
ORDINAL

gamepad const gamepads = navigator . getGamepads (); const myGamepad = gamepads [ 0 ]; // trigger a max intensity vibration for

1.5 seconds
TIME

myGamepad . hapticActuator [ 0 ]. pulse (

1.0
CARDINAL

, 1500 );


One
CARDINAL

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
ORG

button was pressed, I had to do this with the Gamepad API:

Set up an interval with requestAnimationFrame .

. Call

getGamepad
PERSON

() in each animation frame.

in each animation frame. Identify the gamepad I want to check (with an ID previously saved).

Read the buttons array.

array. Access the particular button I want (button

9
CARDINAL

for Start).

Read the value of the pressed property.

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
WORK_OF_ART

is pressed " ); });

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
PRODUCT

for the gamepads’ functionality.

An easy game to develop was the classic

Pong
PERSON

. 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
PERSON

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
EVENT

:

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
ORG

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
ORG

. After all, it is an experimental technology, and it is continuously updated. New events could be added, such as

gamepadchange
ORG

,

gamepadaxischange
ORG

, which would simplify the

API
ORG

… and make my library obsolete.

Extra: Be a Gamepad API Rockstar!

After testing the different types of controllers that work with

the Gamepad API
EVENT

, I had an idea: if my old PS3 controllers worked, how about my old

Rock Band
ORG

drums and guitar? How about

the Dance Dance Revolution
EVENT

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
ORG

video game using

JavaScript
PRODUCT

and HTML in this other article.