Play the Chrome dino game with your gamepad

Learn how to use the Gamepad API to push your web games to the next level.

Chrome's offline page easter egg is one of the worst-kept secrets in history ([citation needed], but claim made for the dramatic effect). If you press the space key or, on mobile devices, tap the dinosaur, the offline page becomes a playable arcade game. You might be aware that you don't actually have to go offline when you feel like playing: in Chrome, you can just navigate to about://dino, or, for the geek in you, browse to about://network-error/-106. But did you know that there are 270 million Chrome dino games played every month?

Chrome's offline page with the Chrome dino game.
Press the space key to play!

Another fact that arguably is more useful to know and that you might not be aware of is that in arcade mode you can play the game with a gamepad. Gamepad support was added roughly one year ago as of the time of this writing in a commit by Reilly Grant. As you can see, the game, just like the rest of the Chromium project, is fully open source. In this post, I want to show you how to use the Gamepad API.

Use the Gamepad API

Feature detection and browser support

The Gamepad API has universally great browser support across both desktop and mobile. You can detect if the Gamepad API is supported using the following snippet:

if ('getGamepads' in navigator) {
  // The API is supported!
}

How the browser represents a gamepad

The browser represents gamepads as Gamepad objects. A Gamepad has the following properties:

  • id: An identification string for the gamepad. This string identifies the brand or style of connected gamepad device.
  • displayId: The VRDisplay.displayId of an associated VRDisplay (if relevant).
  • index: The index of the gamepad in the navigator.
  • connected: Indicates whether the gamepad is still connected to the system.
  • hand: An enum defining what hand the controller is being held in, or is most likely to be held in.
  • timestamp: The last time the data for this gamepad was updated.
  • mapping: The button and axes mapping in use for this device, either "standard" or "xr-standard".
  • pose: A GamepadPose object representing the pose information associated with a WebVR controller.
  • axes: An array of values for all axes of the gamepad, linearly normalized to the range of -1.01.0.
  • buttons: An array of button states for all buttons of the gamepad.

Note that buttons can be digital (pressed or not pressed) or analog (for example, 78% pressed). This is why buttons are reported as GamepadButton objects, with the following attributes:

  • pressed: The pressed state of the button (true if the button is pressed, and false if it is not pressed.
  • touched: The touched state of the button. If the button is capable of detecting touch, this property is true if the button is being touched, and false otherwise.
  • value: For buttons that have an analog sensor, this property represents the amount by which the button has been pressed, linearly normalized to the range of 0.01.0.
  • hapticActuators: An array containing GamepadHapticActuator objects, each of which represents haptic feedback hardware available on the controller.

One additional thing that you might encounter, depending on your browser and the gamepad you have, is a vibrationActuator property. It allows for two kinds rumble effects:

  • Dual-Rumble: The haptic feedback effect generated by two eccentric rotating mass actuators, one in each grip of the gamepad.
  • Trigger-Rumble: The haptic feedback effect generated by two independent motors, with one motor located in each of the gamepad's triggers.

The following schematic overview, taken straight from the spec, shows the mapping and the arrangement of the buttons and axes on a generic gamepad.

Schematic overview of the button and axes mappings of a common gamepad.
Visual representation of a standard gamepad layout (Source).

Notification when a gamepad gets connected

To learn when a gamepad is connected, listen for the gamepadconnected event that triggers on the window object. When the user connects a gamepad, which can either happen using USB or using Bluetooth, a GamepadEvent is fired that has the gamepad's details in an aptly named gamepad property. In the following, you can see an example from an Xbox 360 controller that I had lying around (yes, I am into retro gaming).

window.addEventListener('gamepadconnected', (event) => {
  console.log('✅ 🎮 A gamepad was connected:', event.gamepad);
  /*
    gamepad: Gamepad
    axes: (4) [0, 0, 0, 0]
    buttons: (17) [GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton]
    connected: true
    id: "Xbox 360 Controller (STANDARD GAMEPAD Vendor: 045e Product: 028e)"
    index: 0
    mapping: "standard"
    timestamp: 6563054.284999998
    vibrationActuator: GamepadHapticActuator {type: "dual-rumble"}
  */
});

Notification when a gamepad gets disconnected

Being notified of gamepad disconnections happens analogously to the way connections are detected. This time the app listens for the gamepaddisconnected event. Note how in the following example connected is now false when I unplug the Xbox 360 controller.

window.addEventListener('gamepaddisconnected', (event) => {
  console.log('❌ 🎮 A gamepad was disconnected:', event.gamepad);
  /*
    gamepad: Gamepad
    axes: (4) [0, 0, 0, 0]
    buttons: (17) [GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton, GamepadButton]
    connected: false
    id: "Xbox 360 Controller (STANDARD GAMEPAD Vendor: 045e Product: 028e)"
    index: 0
    mapping: "standard"
    timestamp: 6563054.284999998
    vibrationActuator: null
  */
});

The gamepad in your game loop

Getting ahold of a gamepad starts with a call to navigator.getGamepads(), which returns an array with Gamepad items. The array in Chrome always has a fixed length of four items. If zero or less than four gamepads are connected, an item may just be null. Always be sure to check all items of the array and be aware that gamepads "remember" their slot and may not always be present at the first available slot.

// When no gamepads are connected:
navigator.getGamepads();
// (4) [null, null, null, null]

If one or several gamepads are connected, but navigator.getGamepads() still reports null items, you may need to "wake" each gamepad by pressing any of its buttons. You can then poll the gamepad states in your game loop as shown in the following code.

const pollGamepads = () => {
  // Always call `navigator.getGamepads()` inside of
  // the game loop, not outside.
  const gamepads = navigator.getGamepads();
  for (const gamepad of gamepads) {
    // Disregard empty slots.
    if (!gamepad) {
      continue;
    }
    // Process the gamepad state.
    console.log(gamepad);
  }
  // Call yourself upon the next animation frame.
  // (Typically this happens every 60 times per second.)
  window.requestAnimationFrame(pollGamepads);
};
// Kick off the initial game loop iteration.
pollGamepads();

The vibration actuator

The vibrationActuator property returns a GamepadHapticActuator object, which corresponds to a configuration of motors or other actuators that can apply a force for the purposes of haptic feedback. Haptic effects can be played by calling Gamepad.vibrationActuator.playEffect(). The only valid effect types are 'dual-rumble' and 'trigger-rumble'.

Supported rumble effects

if (gamepad.vibrationActuator.effects.includes('trigger-rumble')) {
  // Trigger rumble supported.
} else if (gamepad.vibrationActuator.effects.includes('dual-rumble')) {
  // Dual rumble supported.
} else {
  // Rumble effects aren't supported.
}

Dual rumble

Dual-rumble describes a haptic configuration with an eccentric rotating mass vibration motor in each handle of a standard gamepad. In this configuration, either motor is capable of vibrating the whole gamepad. The two masses are unequal so that the effects of each can be combined to create more complex haptic effects. Dual-rumble effects are defined by four parameters:

  • duration: Sets the duration of the vibration effect in milliseconds.
  • startDelay: Sets the duration of the delay until the vibration is started.
  • strongMagnitude and weakMagnitude: Set the vibration intensity levels for the heavier and lighter eccentric rotating mass motors, normalized to the range 0.01.0.
// This assumes a `Gamepad` as the value of the `gamepad` variable.
const dualRumble = (gamepad, delay = 0, duration = 100, weak = 1.0, strong = 1.0) => {
  if (!('vibrationActuator' in gamepad)) {
    return;
  }
  gamepad.vibrationActuator.playEffect('dual-rumble', {
    // Start delay in ms.
    startDelay: delay,
    // Duration in ms.
    duration: duration,
    // The magnitude of the weak actuator (between 0 and 1).
    weakMagnitude: weak,
    // The magnitude of the strong actuator (between 0 and 1).
    strongMagnitude: strong,
  });
};

Trigger rumble

Trigger rumble is the haptic feedback effect generated by two independent motors, with one motor located in each of the gamepad's triggers.

// This assumes a `Gamepad` as the value of the `gamepad` variable.
const triggerRumble = (gamepad, delay = 0, duration = 100, weak = 1.0, strong = 1.0) => {
  if (!('vibrationActuator' in gamepad)) {
    return;
  }
  // Feature detection.
  if (!('effects' in gamepad.vibrationActuator) || !gamepad.vibrationActuator.effects.includes('trigger-rumble')) {
    return;
  }
  gamepad.vibrationActuator.playEffect('trigger-rumble', {
    // Duration in ms.
    duration: duration,
    // The left trigger (between 0 and 1).
    leftTrigger: leftTrigger,
    // The right trigger (between 0 and 1).
    rightTrigger: rightTrigger,
  });
};

Integration with Permissions Policy

The Gamepad API spec defines a policy-controlled feature identified by the string "gamepad". Its default allowlist is "self". A document's permissions policy determines whether any content in that document is allowed to access navigator.getGamepads(). If disabled in any document, no content in the document will be allowed to use navigator.getGamepads(), nor will the gamepadconnected and gamepaddisconnected events fire.

<iframe src="index.html" allow="gamepad"></iframe>

Demo

A gamepad tester demo is embedded in the following example. The source code is available on Glitch. Try the demo by connecting a gamepad using USB or Bluetooth and pressing any of its buttons or moving any of its axis.

Bonus: play Chrome dino on web.dev

You can play Chrome dino with your gamepad on this very site. The source code is available on GitHub. Check out the gamepad polling implementation in trex-runner.js and note how it is emulating key presses.

For the Chrome dino gamepad demo to work, I have ripped out the Chrome dino game from the core Chromium project (updating an earlier effort by Arnelle Ballane), placed it on a standalone site, extended the existing gamepad API implementation by adding ducking and vibration effects, created a full screen mode, and Mehul Satardekar contributed a dark mode implementation. Happy gaming!

Acknowledgements

This document was reviewed by François Beaufort and Joe Medley. The Gamepad API spec is edited by Steve Agoston, James Hollyer, and Matt Reynolds. The former spec editors are Brandon Jones, Scott Graham, and Ted Mielczarek. The Gamepad Extensions spec is edited by Brandon Jones. Hero image by Laura Torrent Puig.