Skip to content

Instantly share code, notes, and snippets.

@mzabriskie
Last active August 29, 2015 14:02
Show Gist options
  • Save mzabriskie/8f06c9137211a689f3a8 to your computer and use it in GitHub Desktop.
Save mzabriskie/8f06c9137211a689f3a8 to your computer and use it in GitHub Desktop.
Discussion for how to best implement Gamepad support

gamepad.js

I haven't been able to find any libraries for interacting with the HTML5 Gamepad API that I really like. This is an attempt to sort out some of my ideas for what I would want out of such a library and ultimately implement it.

Goals

  • Abstract away complexity
    • Manage gamepad support (detection, vender impl, polling, etc.)
    • Make it dead simple for developers to implement
    • Allow devs to focus on making games, not fiddling with controller config
  • Multi player support
    • Handle input from multiple gamepads
    • Don't sacrifice simplicity for single gamepad usage (most common use case)
  • Independent controls per player
    • Allow player 1 to have different control settings from player 2
    • Player 1 prefers button A to jump
    • Player 2 prefers right bumper to jump
  • Manage key codes
    • Track sequence of inputs and handle successful sequence
    • Easter egg (Konami Code)
    • Character special moves (Mortal Kombat)

Constants

Define constants for all the indices of raw gamepad state, as well as all events.

Gamepad.Events = {
    CONNECT: 'connect',
    DISCONNECT: 'disconnect',
    TICK: 'tick',
    ERROR: 'error',
    
    BUTTON_A: 'button-a',
    BUTTON_B: 'button-b',
    BUTTON_X: 'button-x',
    BUTTON_Y: 'button-y',
    BUTTON_LEFT_BUMPER: 'button-left-bumper',
    BUTTON_RIGHT_BUMPER: 'button-right-bumper',
    BUTTON_LEFT_TRIGGER: 'button-left-trigger',
    BUTTON_RIGHT_TRIGGER: 'button-right-trigger',
    BUTTON_BACK: 'button-back',
    BUTTON_START: 'button-start',
    BUTTON_LEFT_ANALOGUE: 'button-left-analogue',
    BUTTON_RIGHT_ANALOGUE: 'button-right-analogue',
    BUTTON_UP: 'button-up',
    BUTTON_DOWN: 'button-down',
    BUTTON_LEFT: 'button-left',
    BUTTON_RIGHT: 'button-right',
    
    LEFT_ANALOGUE_FORWARD: 'left-analogue-forward',
    LEFT_ANALOGUE_BACKWARD: 'left-analogue-backward',
    LEFT_ANALOGUE_LEFT: 'left-analogue-left',
    LEFT_ANALOGUE_RIGHT: 'left-analogue-right',
    RIGHT_ANALOGUE_FORWARD: 'right-analogue-forward',
    RIGHT_ANALOGUE_BACKWARD: 'right-analogue-backward',
    RIGHT_ANALOGUE_LEFT: 'right-analogue-left',
    RIGHT_ANALOGUE_RIGHT: 'right-analogue-right'
};

Gamepad.Buttons = {
    A: 0,
    B: 1,
    X: 2,
    Y: 3,
    LEFT_BUMPER: 4,
    RIGHT_BUMPER: 5,
    LEFT_TRIGGER: 6,
    RIGHT_TRIGGER: 7,
    BACK: 8,
    START: 9,
    LEFT_ANALOGUE: 10,
    RIGHT_ANALOGUE: 11,
    UP: 12,
    DOWN: 13,
    LEFT: 14,
    RIGHT: 15
};

Gamepad.Axes = {
    LEFT_ANALOGUE_LEFT_TO_RIGHT: 0,
    LEFT_ANALOGUE_FRONT_TO_BACK: 1,
    RIGHT_ANALOGUE_LEFT_TO_RIGHT: 2,
    RIGHT_ANALOGUE_FRONT_TO_BACK: 3
};

Utility

Provide an API for processing raw gamepad state. This would be ideal when used in conjunction with the tick event.

Gamepad.getButtonAValue = function (state) { /*...*/ };
Gamepad.getButtonBValue = function (state) { /*...*/ };
Gamepad.getButtonXValue = function (state) { /*...*/ };
Gamepad.getButtonYValue = function (state) { /*...*/ };
/*...*/

Gamepad.getLeftAnalogueForwardValue = function (state) { /*...*/ };
Gamepad.getLeftAnalogueBackwardValue = function (state) { /*...*/ };
Gamepad.getLeftAnalogueLeftValue = function (state) { /*...*/ };
Gamepad.getLeftAnalogueRightValue = function (state) { /*...*/ };

Gamepad.getRightAnalogueForwardValue = function (state) { /*...*/ };
Gamepad.getRightAnalogueBackwardValue = function (state) { /*...*/ };
Gamepad.getRightAnalogueLeftValue = function (state) { /*...*/ };
Gamepad.getRightAnalogueRightValue = function (state) { /*...*/ };

Event Management

Observer Approach

Gamepad.player(1)
	.on('connect', function () {
		console.log('Player 1 is connected');
	})
	.on('tick', function (state, delta) {
		console.log(state);
		// {buttons: Array[16], axes: Array[4], mapping: "standard", connected: true...}
	})
	.on('buttonpress', function (which, value) {
		if (which === Gamepad.Buttons.A) {
			console.log('Player 1 pressed A');
		}
	})
	.on('axischange', function (which, value) {
		if (which === Gamepad.Axes.LEFT_ANALOGUE_FRONT_TO_BACK && value  < 0)  {
			console.log('Player 1 forward motion left analogue');
		}
	});

Pros

  • Familiar EventEmmiter pattern

Cons

  • Verbose
  • Conditional testing for which button/axis triggered event

Mapping Approach

Gamepad.player(1).listen({
	Gamepad.Events.CONNECT: function () {
		console.log('Player 1 is connected');
	},
	Gamepad.Events.TICK: function (state, delta) {
		console.log(state);
		// {buttons: Array[16], axes: Array[4], mapping: "standard", connected: true...}
	},
	Gamepad.Events.BUTTON_A: function (value) {
		console.log('Player 1 pressed A');
	},
	Gamepad.Events.LEFT_ANALOGUE_FORWARD: function (value) {
		console.log('Player 1 forward motion left analogue');
	}
});

Pros

  • No need for conditional testing for which
  • Easier to support custom mapping for each user

Cons

  • Only a single handler per event

Player Management

For sake of example, let's assume the event driven approach.

// Reference players by index
Gamepad.player(1).on('connect', function () { /*...*/ });
Gamepad.player(2).on('connect', function () { /*...*/ });

// Terse reference to player 1 for single gamepad usage
Gamepad.on('connect', function () { /*...*/ });

Reference

@mzabriskie
Copy link
Author

Can you give me an example of changing listeners at runtime?

When would you want multiple handlers for a single input? Take a Mario style game for example. Pressing the left D-Pad moves Mario left, right D-Pad moves right, etc. Is there any secondary action that ever happens with a single input? I am coming at this as more of a gamer, than a game developer. Maybe there's something that I'm not considering.

The native HTML5 Gamepad API already provides the state that you would want for polling. I am accounting for this by providing the tick event. This is called on every tick of requestAnimationFrame, which depending on frame rate would likely by 60 ticks/second. I've updated the proposed code to illustrate.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment