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

Loading/Saving and Gamepad Support #42

Closed
wants to merge 2 commits into from
Closed
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
1 change: 1 addition & 0 deletions AUTHORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ Thanks to:
* Jens Lindstrom for some optimisations.
* Rafal Chlodnicki for an Opera fix.
* Ecin Krispie for fixing player 2 controls.
* Joseph Lewis for adding gamepad support.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
JSNES
=====

A JavaScript NES emulator.
A JavaScript NES emulator that includes support for saving using localstorage and gamepads.

Build
-----
Expand Down
25 changes: 21 additions & 4 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,63 +7,80 @@

<body>
<h1>JSNES</h1>

<div id="emulator"></div>

<h2>Controls</h2>
<table id="controls">
<tr>
<th>Button</th>
<th>Player 1</th>
<th>Player 2</th>
<th>Gamepad</th>
</tr>
<tr>
<td>Left</td>
<td>Left</td>
<td>Num-4</td>
<td>D-pad Left</td>
<tr>
<td>Right</td>
<td>Right</td>
<td>Num-6</td>
<td>D-pad Right</td>
</tr>
<tr>
<td>Up</td>
<td>Up</td>
<td>Num-8</td>
<td>D-pad Up</td>
</tr>
<tr>
<td>Down</td>
<td>Down</td>
<td>Num-2</td>
<td>D-pad Down</td>
</tr>
<tr>
<td>A</td>
<td>X</td>
<td>Num-7</td>
<td>Button 1 (usually "A")</td>
</tr>
<tr>
<td>B</td>
<td>Z/Y</td>
<td>Num-9</td>
<td>Button 2 (usually "B")</td>
</tr>
<tr>
<td>Start</td>
<td>Enter</td>
<td>Num-1</td>
<td>Start</td>
</tr>
<tr>
<td>Select</td>
<td>Ctrl</td>
<td>Num-3</td>
<td>Select (sometimes "back")</td>
</tr>
</table>

<p>Note: The gamepad API is currently experimental, Firefox and Chrome
currently support it.</p>

<p>In Firefox some controllers are improperly mapped.
Try using Chrome if you experience issues.</p>

<script src="lib/jquery-1.4.2.min.js" type="text/javascript" charset="utf-8"></script>
<script src="lib/dynamicaudio-min.js" type="text/javascript" charset="utf-8"></script>
<script src="source/nes.js" type="text/javascript" charset="utf-8"></script>
<script src="source/utils.js" type="text/javascript" charset="utf-8"></script>
<script src="source/cpu.js" type="text/javascript" charset="utf-8"></script>
<script src="source/keyboard.js" type="text/javascript" charset="utf-8"></script>
<script src="source/gamepad_keyboard.js" type="text/javascript" charset="utf-8"></script>
<script src="lib/gamepad.js" type="text/javascript" charset="utf-8"></script>
<script src="source/mappers.js" type="text/javascript" charset="utf-8"></script>
<script src="source/papu.js" type="text/javascript" charset="utf-8"></script>
<script src="source/ppu.js" type="text/javascript" charset="utf-8"></script>
Expand All @@ -80,15 +97,15 @@ <h2>Controls</h2>
],
"Working": [
['Bubble Bobble', 'local-roms/Bubble Bobble (U).nes'],

['Contra', 'local-roms/Contra (U) [!].nes'],
['Donkey Kong', 'local-roms/Donkey Kong (JU).nes'],
['Dr. Mario', 'local-roms/Dr. Mario (JU).nes'],
['Golf', 'local-roms/Golf (JU).nes'],
['The Legend of Zelda', 'local-roms/Legend of Zelda, The (U) (PRG1).nes'],
['Lemmings', 'local-roms/Lemmings (U).nes'],
['Lifeforce', 'local-roms/Lifeforce (U).nes'],

['Mario Bros.', 'local-roms/Mario Bros. (JU) [!].nes'],
['Mega Man', 'local-roms/Mega Man (U).nes'],
['Pac-Man', 'local-roms/Pac-Man (U) [!].nes'],
Expand Down
284 changes: 284 additions & 0 deletions lib/gamepad.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
/**
* Copyright 2012 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @author [email protected] (Marcin Wichary)
*/

var gamepadSupport = {
// A number of typical buttons recognized by Gamepad API and mapped to
// standard controls. Any extraneous buttons will have larger indexes.
TYPICAL_BUTTON_COUNT: 16,

// A number of typical axes recognized by Gamepad API and mapped to
// standard controls. Any extraneous buttons will have larger indexes.
TYPICAL_AXIS_COUNT: 4,

// Whether we’re requestAnimationFrameing like it’s 1999.
ticking: false,

// The canonical list of attached gamepads, without “holes” (always
// starting at [0]) and unified between Firefox and Chrome.
gamepads: [],

// Remembers the connected gamepads at the last check; used in Chrome
// to figure out when gamepads get connected or disconnected, since no
// events are fired.
prevRawGamepadTypes: [],

// Previous timestamps for gamepad state; used in Chrome to not bother with
// analyzing the polled data if nothing changed (timestamp is the same
// as last time).
prevTimestamps: [],

/**
* Initialize support for Gamepad API.
*/
init: function() {
var gamepadSupportAvailable = navigator.getGamepads ||
!!navigator.webkitGetGamepads ||
!!navigator.webkitGamepads;

if (!gamepadSupportAvailable) {
// It doesn’t seem Gamepad API is available – show a message telling
// the visitor about it.
tester.showNotSupported();
} else {
// Check and see if gamepadconnected/gamepaddisconnected is supported.
// If so, listen for those events and don't start polling until a gamepad
// has been connected.
if ('ongamepadconnected' in window) {
window.addEventListener('gamepadconnected',
gamepadSupport.onGamepadConnect, false);
window.addEventListener('gamepaddisconnected',
gamepadSupport.onGamepadDisconnect, false);
} else {
// If connection events are not supported just start polling
gamepadSupport.startPolling();
}
}
},

/**
* React to the gamepad being connected.
*/
onGamepadConnect: function(event) {
// Add the new gamepad on the list of gamepads to look after.
gamepadSupport.gamepads.push(event.gamepad);

// Ask the tester to update the screen to show more gamepads.
tester.updateGamepads(gamepadSupport.gamepads);

// Start the polling loop to monitor button changes.
gamepadSupport.startPolling();
},

/**
* React to the gamepad being disconnected.
*/
onGamepadDisconnect: function(event) {
// Remove the gamepad from the list of gamepads to monitor.
for (var i in gamepadSupport.gamepads) {
if (gamepadSupport.gamepads[i].index == event.gamepad.index) {
gamepadSupport.gamepads.splice(i, 1);
break;
}
}

// If no gamepads are left, stop the polling loop.
if (gamepadSupport.gamepads.length == 0) {
gamepadSupport.stopPolling();
}

// Ask the tester to update the screen to remove the gamepad.
tester.updateGamepads(gamepadSupport.gamepads);
},

/**
* Starts a polling loop to check for gamepad state.
*/
startPolling: function() {
// Don’t accidentally start a second loop, man.
if (!gamepadSupport.ticking) {
gamepadSupport.ticking = true;
gamepadSupport.tick();
}
},

/**
* Stops a polling loop by setting a flag which will prevent the next
* requestAnimationFrame() from being scheduled.
*/
stopPolling: function() {
gamepadSupport.ticking = false;
},

/**
* A function called with each requestAnimationFrame(). Polls the gamepad
* status and schedules another poll.
*/
tick: function() {
gamepadSupport.pollStatus();
gamepadSupport.scheduleNextTick();
},

scheduleNextTick: function() {
// Only schedule the next frame if we haven’t decided to stop via
// stopPolling() before.
if (gamepadSupport.ticking) {
if (window.requestAnimationFrame) {
window.requestAnimationFrame(gamepadSupport.tick);
} else if (window.mozRequestAnimationFrame) {
window.mozRequestAnimationFrame(gamepadSupport.tick);
} else if (window.webkitRequestAnimationFrame) {
window.webkitRequestAnimationFrame(gamepadSupport.tick);
}
// Note lack of setTimeout since all the browsers that support
// Gamepad API are already supporting requestAnimationFrame().
}
},

/**
* Checks for the gamepad status. Monitors the necessary data and notices
* the differences from previous state (buttons for Chrome/Firefox,
* new connects/disconnects for Chrome). If differences are noticed, asks
* to update the display accordingly. Should run as close to 60 frames per
* second as possible.
*/
pollStatus: function() {
// Poll to see if gamepads are connected or disconnected. Necessary
// only on Chrome.
gamepadSupport.pollGamepads();

for (var i in gamepadSupport.gamepads) {
var gamepad = gamepadSupport.gamepads[i];

// Don’t do anything if the current timestamp is the same as previous
// one, which means that the state of the gamepad hasn’t changed.
// This is only supported by Chrome right now, so the first check
// makes sure we’re not doing anything if the timestamps are empty
// or undefined.
if (gamepad.timestamp &&
(gamepad.timestamp == gamepadSupport.prevTimestamps[i])) {
continue;
}
gamepadSupport.prevTimestamps[i] = gamepad.timestamp;

gamepadSupport.updateDisplay(i);
}
},

// This function is called only on Chrome, which does not yet support
// connection/disconnection events, but requires you to monitor
// an array for changes.
pollGamepads: function() {
// Get the array of gamepads – the first method (getGamepads)
// is the most modern one and is supported by Firefox 28+ and
// Chrome 35+. The second one (webkitGetGamepads) is a deprecated method
// used by older Chrome builds.
var rawGamepads =
(navigator.getGamepads && navigator.getGamepads()) ||
(navigator.webkitGetGamepads && navigator.webkitGetGamepads());

if (rawGamepads) {
// We don’t want to use rawGamepads coming straight from the browser,
// since it can have “holes” (e.g. if you plug two gamepads, and then
// unplug the first one, the remaining one will be at index [1]).
gamepadSupport.gamepads = [];

// We only refresh the display when we detect some gamepads are new
// or removed; we do it by comparing raw gamepad table entries to
// “undefined.”
var gamepadsChanged = false;

for (var i = 0; i < rawGamepads.length; i++) {
if (typeof rawGamepads[i] != gamepadSupport.prevRawGamepadTypes[i]) {
gamepadsChanged = true;
gamepadSupport.prevRawGamepadTypes[i] = typeof rawGamepads[i];
}

if (rawGamepads[i]) {
gamepadSupport.gamepads.push(rawGamepads[i]);
}
}

// Ask the tester to refresh the visual representations of gamepads
// on the screen.
if (gamepadsChanged) {
tester.updateGamepads(gamepadSupport.gamepads);
}
}
},

// Call the tester with new state and ask it to update the visual
// representation of a given gamepad.
updateDisplay: function(gamepadId) {
var gamepad = gamepadSupport.gamepads[gamepadId];

// Update all the buttons (and their corresponding labels) on screen.
tester.updateButton(gamepad.buttons[0], gamepadId, 'button-1');
tester.updateButton(gamepad.buttons[1], gamepadId, 'button-2');
tester.updateButton(gamepad.buttons[2], gamepadId, 'button-3');
tester.updateButton(gamepad.buttons[3], gamepadId, 'button-4');

tester.updateButton(gamepad.buttons[4], gamepadId,
'button-left-shoulder-top');
tester.updateButton(gamepad.buttons[6], gamepadId,
'button-left-shoulder-bottom');
tester.updateButton(gamepad.buttons[5], gamepadId,
'button-right-shoulder-top');
tester.updateButton(gamepad.buttons[7], gamepadId,
'button-right-shoulder-bottom');

tester.updateButton(gamepad.buttons[8], gamepadId, 'button-select');
tester.updateButton(gamepad.buttons[9], gamepadId, 'button-start');

tester.updateButton(gamepad.buttons[10], gamepadId, 'stick-1');
tester.updateButton(gamepad.buttons[11], gamepadId, 'stick-2');

tester.updateButton(gamepad.buttons[12], gamepadId, 'button-dpad-top');
tester.updateButton(gamepad.buttons[13], gamepadId, 'button-dpad-bottom');
tester.updateButton(gamepad.buttons[14], gamepadId, 'button-dpad-left');
tester.updateButton(gamepad.buttons[15], gamepadId, 'button-dpad-right');

// Update all the analogue sticks.
tester.updateAxis(gamepad.axes[0], gamepadId,
'stick-1-axis-x', 'stick-1', true);
tester.updateAxis(gamepad.axes[1], gamepadId,
'stick-1-axis-y', 'stick-1', false);
tester.updateAxis(gamepad.axes[2], gamepadId,
'stick-2-axis-x', 'stick-2', true);
tester.updateAxis(gamepad.axes[3], gamepadId,
'stick-2-axis-y', 'stick-2', false);

// Update extraneous buttons.
var extraButtonId = gamepadSupport.TYPICAL_BUTTON_COUNT;
while (typeof gamepad.buttons[extraButtonId] != 'undefined') {
tester.updateButton(gamepad.buttons[extraButtonId], gamepadId,
'extra-button-' + extraButtonId);

extraButtonId++;
}

// Update extraneous axes.
var extraAxisId = gamepadSupport.TYPICAL_AXIS_COUNT;
while (typeof gamepad.axes[extraAxisId] != 'undefined') {
tester.updateAxis(gamepad.axes[extraAxisId], gamepadId,
'extra-axis-' + extraAxisId);

extraAxisId++;
}

}
};
Loading