Skip to content

Instantly share code, notes, and snippets.

@mzabriskie
Last active August 29, 2015 14:02
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • 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

@Problematic
Copy link

I would go with the EventEmitter pattern, since you'll probably expose that API anyway to change listeners at runtime (and to add multiple listeners, unless you support each key in the mapping having an array of listeners).

A thought: my use case would probably be polling the controller from a game loop, so you might want to expose current state in a map or something, instead of just through events.

@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