Skip to content
Permalink
Browse files

JS Game Boy Emulator: Add gamepad support

Add HTML5 gamepad support to the javascript gameboy emulator.
When w3c "standard" mapping is available for the gamepad, buttons
will map to the expected locations Otherwise the first 4 buttons
map to A/B/Select/Start.

Gamepad support co-exists and does not interfere with keyboard and
on-screen controls. Polling only starts if a button is pressed on
gamepad, and will stop if the gamepad disconnects.

Sound will also start when the gamepad polling is started, similar
to keyboard input.
  • Loading branch information
bbbbbr committed Oct 17, 2019
1 parent ab55a3a commit bffdeef5ce1930031850a39fe1579aa610b281b3
Showing with 209 additions and 0 deletions.
  1. +209 −0 appData/js-emulator/js/other/controls.js
@@ -33,6 +33,51 @@ var btnStart = document.getElementById("controller_start");
var btnSelect = document.getElementById("controller_select");
var dpad = document.getElementById("controller_dpad");


// HTML Gamepad API support
// Poll for gamepad input about ~4 times per gameboy frame (~240 times second)
const GAMEPAD_POLLING_INTERVAL = 1000 / 60 / 4;
const GAMEPAD_KEYMAP_STANDARD_STR = "standard"

// When gamepad.mapping reports "standard"
const GAMEPAD_KEYMAP_STANDARD = [
{gb_key: "b", gp_button: 0, type: "button"},
{gb_key: "a", gp_button: 1, type: "button"},
{gb_key: "select", gp_button: 8, type: "button"},
{gb_key: "start", gp_button: 9, type: "button"},
{gb_key: "up", gp_button: 12, type: "button"},
{gb_key: "down", gp_button: 13, type: "button"},
{gb_key: "left", gp_button: 14, type: "button"},
{gb_key: "right", gp_button: 15, type: "button"}
];

const GAMEPAD_KEYMAP_DEFAULT = [
{gb_key: "a", gp_button: 0, type: "button"},
{gb_key: "b", gp_button: 1, type: "button"},
{gb_key: "select", gp_button: 2, type: "button"},
{gb_key: "start", gp_button: 3, type: "button"},
{gb_key: "up", gp_button: 2, type: "axis"},
{gb_key: "down", gp_button: 3, type: "axis"},
{gb_key: "left", gp_button: 0, type: "axis"},
{gb_key: "right", gp_button: 1, type: "axis"}
];

// gamepad related vars
var gp = {
apiID: undefined,
timerID: undefined,
keybinds: undefined,
axes: {
last: undefined,
cur: [],
changed: [] },
buttons: {
last: undefined,
cur: [],
changed: [] }
};


function bindButton(el, code) {
el.addEventListener("touchstart", function(e) {
e.preventDefault();
@@ -162,3 +207,167 @@ if (isTouchEnabled) {
controller.style.display = "none";
}
bindKeyboard();


// HTML Gamepad API Support

// Load a key map for gamepad-to-gameboy buttons
function gamepadBindKeys(strMapping) {

// Try to use the w3c "standard" gamepad mapping if available
// (Chrome/V8 seems to do that better than Firefox)
//
// Otherwise use a default mapping that assigns
// A/B/Select/Start to the first four buttons,
// and U/D/L/R to the first two axes.

if (strMapping === GAMEPAD_KEYMAP_STANDARD_STR)
gp.keybinds = GAMEPAD_KEYMAP_STANDARD;
else
gp.keybinds = GAMEPAD_KEYMAP_DEFAULT;
}


function gamepadCacheValues(gamepad) {

// Read Buttons
for(let k=0; k<gamepad.buttons.length; k++) {
// .value is for analog, .pressed is for boolean buttons
gp.buttons.cur[k] = (gamepad.buttons[k].value > 0 ||
gamepad.buttons[k].pressed == true);

// Update state changed if not on first input pass
if (gp.buttons.last !== undefined)
gp.buttons.changed[k] = (gp.buttons.cur[k] != gp.buttons.last[k]);
}

// Read Axes
for(let k=0; k<gamepad.axes.length; k++) {
// Decode each dpad axis into two buttons, one for each direction
gp.axes.cur[(k*2) ] = (gamepad.axes[k] < 0);
gp.axes.cur[(k*2)+1] = (gamepad.axes[k] > 0);

// Update state changed if not on first input pass
if (gp.axes.last !== undefined) {
gp.axes.changed[(k*2) ] = (gp.axes.cur[(k*2) ] != gp.axes.last[(k*2) ]);
gp.axes.changed[(k*2)+1] = (gp.axes.cur[(k*2)+1] != gp.axes.last[(k*2)+1]);
}
}

// Save current state for comparison on next input
gp.axes.last = gp.axes.cur.slice(0);
gp.buttons.last = gp.buttons.cur.slice(0);
}


function gamepadHandleButton(keyBind) {

var buttonCache;

// Select button / axis cache based on key bind type
if (keyBind.type === "button")
buttonCache = gp.buttons;
else if (keyBind.type === "axis")
buttonCache = gp.axes;

// Make sure the button exists in the cache array
if (keyBind.gp_button < buttonCache.changed.length) {

// Send the button state if it's changed
if (buttonCache.changed[keyBind.gp_button])
if (buttonCache.cur[keyBind.gp_button])
GameBoyKeyDown(keyBind.gb_key);
else
GameBoyKeyUp(keyBind.gb_key);
}
}


function gamepadGetCurrent() {

// Chrome requires retrieving a new gamepad object
// every time button state is queried (the existing object
// will have stale button state). Just do that for all browsers
var gamepad = navigator.getGamepads()[gp.apiID];

if (gamepad)
if (gamepad.connected)
return gamepad;

return undefined;
}


function gamepadUpdate() {

var gamepad = gamepadGetCurrent();

if (gamepad !== undefined) {

// Cache gamepad input values
gamepadCacheValues(gamepad);

// Loop through buttons and send changes if needed
for (let i=0; i<gp.keybinds.length; i++)
gamepadHandleButton(gp.keybinds[i]);
}
else {
// Gamepad is no longer present, disconnect
gamepadStop();
}
}


function gamepadStart(gamepad) {

// Make sure it has enough buttons and axes
if ((gamepad.mapping === GAMEPAD_KEYMAP_STANDARD_STR) ||
((gamepad.axes.length >= 2) && (gamepad.buttons.length >= 4))) {

// Save API index for polling (required by Chrome/V8)
gp.apiID = gamepad.index;

// Assign gameboy keys to the gamepad
gamepadBindKeys(gamepad.mapping);

// Start polling the gamepad for input
gp.timerID = setInterval( () => gamepadUpdate(), GAMEPAD_POLLING_INTERVAL);
}
}


function gamepadStop() {

// Stop polling the gamepad for input
if (gp.timerID !== undefined)
clearInterval(gp.timerID);

// Clear previous button history and controller info
gp.axes.last = undefined;
gp.buttons.last = undefined;
gp.keybinds = undefined;

gp.apiID = undefined;
}


function initGamePad()
{
// When a gamepad connects, start polling it for input
window.addEventListener("gamepadconnected", (event) => {
initSound();
gamepadStart( navigator.getGamepads()[event.gamepad.index] );
});

// When a gamepad disconnects, shut down polling for input
window.addEventListener("gamepaddisconnected", (event) => {
gamepadStop();
});
}


initGamePad();




0 comments on commit bffdeef

Please sign in to comment.