Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTML5 Gamepad Support #1

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion user_scripts/CoreGlueCode.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

var IodineGUI = {
Iodine:null,
Blitter:null,
Expand All @@ -20,6 +21,19 @@ var IodineGUI = {
startTime:(+(new Date()).getTime()),
mixerInput:null,
currentSpeed:[false,0],
gamepad : {
apiID: undefined,
timerID: undefined,
keybinds: undefined,
axes: {
last: undefined,
cur: [],
changed: [] },
buttons: {
last: undefined,
cur: [],
changed: [] }
},
defaults:{
timerRate:8,
sound:true,
Expand Down Expand Up @@ -221,7 +235,7 @@ function findRealClock() {
count = 0;
}
}

}
}
}
Expand Down
2 changes: 2 additions & 0 deletions user_scripts/GUIGlueCode.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ function registerGUIEvents() {
//Add DOM events:
addEvent("keydown", document, keyDown);
addEvent("keyup", document, keyUpPreprocess);
addEvent("gamepadconnected", window, gamepadStart);
addEvent("gamepadisconnected", window, gamepadStop);
addEvent("change", document.getElementById("rom_load"), fileLoadROM);
addEvent("change", document.getElementById("bios_load"), fileLoadBIOS);
addEvent("click", document.getElementById("play"), function (e) {
Expand Down
193 changes: 192 additions & 1 deletion user_scripts/JoyPadGlueCode.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,50 @@

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

// HTML Gamepad API support
// Poll for gamepad input about ~4 times per gameboy advance frame (~240 times second)
const GAMEPAD_POLLING_INTERVAL = 1000 / 60 / 4;
const GAMEPAD_KEYMAP_STANDARD_STR = "standard"
// GBA Core key mapping
const GBA_A = 0
const GBA_B = 1
const GBA_R = 8
const GBA_L = 9
const GBA_SELECT = 2
const GBA_START = 3
const GBA_LEFT = 4
const GBA_RIGHT = 5
const GBA_UP = 6
const GBA_DOWN = 7

// When gamepad.mapping reports "standard"
const GAMEPAD_KEYMAP_STANDARD = [
{gba_key: GBA_B, gp_button: 0, type: "button"},
{gba_key: GBA_A, gp_button: 1, type: "button"},
{gba_key: GBA_L, gp_button: 4, type: "button"},
{gba_key: GBA_R, gp_button: 5, type: "button"},
{gba_key: GBA_SELECT, gp_button: 8, type: "button"},
{gba_key: GBA_START, gp_button: 9, type: "button"},
{gba_key: GBA_UP, gp_button: 12, type: "button"},
{gba_key: GBA_DOWN, gp_button: 13, type: "button"},
{gba_key: GBA_RIGHT, gp_button: 14, type: "button"},
{gba_key: GBA_LEFT, gp_button: 15, type: "button"}
];

const GAMEPAD_KEYMAP_DEFAULT = [
{gba_key: GBA_A, gp_button: 0, type: "button"},
{gba_key: GBA_B, gp_button: 1, type: "button"},
{gba_key: GBA_L, gp_button: 4, type: "button"},
{gba_key: GBA_R, gp_button: 5, type: "button"},
{gba_key: GBA_SELECT, gp_button: 2, type: "button"},
{gba_key: GBA_START, gp_button: 3, type: "button"},
{gba_key: GBA_UP, gp_button: 2, type: "axis"},
{gba_key: GBA_DOWN, gp_button: 3, type: "axis"},
{gba_key: GBA_RIGHT, gp_button: 0, type: "axis"},
{gba_key: GBA_LEFT, gp_button: 1, type: "axis"}
];

function keyDown(e) {
var keyCode = e.keyCode | 0;
for (var keyMapIndex = 0; (keyMapIndex | 0) < 10; keyMapIndex = ((keyMapIndex | 0) + 1) | 0) {
Expand Down Expand Up @@ -121,4 +165,151 @@ function togglePlayState() {
else {
IodineGUI.Iodine.play();
}
}
}


// 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)
IodineGUI.gamepad.keybinds = GAMEPAD_KEYMAP_STANDARD;
else
IodineGUI.gamepad.keybinds = GAMEPAD_KEYMAP_DEFAULT;
}


function gamepadCacheValues(gamepad, IOgp) {

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

// Update state changed if not on first input pass
if (IOgp.buttons.last !== undefined)
IOgp.buttons.changed[k] = (IOgp.buttons.cur[k] != IOgp.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
IOgp.axes.cur[(k*2) ] = (gamepad.axes[k] < 0);
IOgp.axes.cur[(k*2)+1] = (gamepad.axes[k] > 0);

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

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


function gamepadHandleButton(keyBind) {

var buttonCache;

// Select button / axis cache based on key bind type
if (keyBind.type === "button")
buttonCache = IodineGUI.gamepad.buttons;
else if (keyBind.type === "axis")
buttonCache = IodineGUI.gamepad.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])
IodineGUI.Iodine.keyDown(keyBind.gba_key);
else
IodineGUI.Iodine.keyUp(keyBind.gba_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()[IodineGUI.gamepad.apiID];

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

return undefined;
}


function gamepadUpdate() {

var gamepad = gamepadGetCurrent();

if (gamepad !== undefined) {

// Cache gamepad input values
gamepadCacheValues(gamepad, IodineGUI.gamepad);

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


function gamepadStart(event) {

var gamepad = navigator.getGamepads()[event.gamepad.index];

// 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)
IodineGUI.gamepad.apiID = gamepad.index;

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

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


function gamepadStop() {

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

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

IodineGUI.gamepad.apiID = undefined;
}