-
Notifications
You must be signed in to change notification settings - Fork 66
Lib API
Last Updated: 10/25/18 Version: 0.2.0
This is the API Design for the WasmBoy lib. Meaning, if you are making an app in Javascript in something like React, a hybrid app in something like Ionic, or even a headless Node app, this is what you would want to use. This offers an easy to use API to get playing gameboy games in the browser, without having to handle running and outputting the game from the core yourself.
Currently, a good point of reference as a completed JS app using WasmBoy is VaporBoy.
First, you will need to install the WasmBoy into your project.
npm: npm install --save wasmboy
github: npm install --save https://github.com/torch2424/wasmBoy.git
Then, you would want to import it into your JS file. This can be done with either Node's require()
or ES6 imports.
require(): const WasmBoy = require('wasmboy')
import: import { WasmBoy } from 'wasmboy'
Now that we have the wasmboy Object, let's take a step back and understand some patterns of the lib:
-
The
WasmBoy
object is a singleton. Meaning, that it returns a single, and the same, instance of WasmBoy across your application. -
The lib is promise based, meaning almost every function in the lib will return a promise. This allows freedom for us to add asynchronous code where we can without worrying about breaking things in the future, and this makes it more consistent for our end-users 😄.
Next, we need to configure the WasmBoy object. To configure the WasmBoy Object, you must run the function config
with the WasmBoyOptions Object, and an output canvas element. The WasmBoyOptions
object is covered in more detail in the "Complete API", where it describes each option, and it's effects (The callbacks allow for especially cool things). Here is an example using the some default settings for running on mobile:
// Get our HTML5 Canvas element
const canvasElement = document.querySelector('canvas');
const WasmBoyOptions = {
headless: false,
useGbcWhenOptional: true,
isAudioEnabled: true,
frameSkip: 1,
audioBatchProcessing: true,
timersBatchProcessing: false,
audioAccumulateSamples: true,
graphicsBatchProcessing: false,
graphicsDisableScanlineRendering: false,
tileRendering: true,
tileCaching: true,
gameboyFPSCap: 60,
updateGraphicsCallback: false,
updateAudioCallback: false,
saveStateCallback: false
}
WasmBoy.config(WasmBoyOptions, canvasElement).then(() => {
console.log('WasmBoy is configured!');
// You may now load games, or use other exported functions of the lib.
}).catch(() => {
console.error('Error Configuring WasmBoy...');
});
For the rest of the "Getting Started" guide we will assume WasmBoy was configured, and the following code was run in the .then() block
Now, we can start loading ROMs into WasmBoy. WasmBoy accepts .gb
, .gbc
, and .zip
files, where in the case of .zip
files will just use the first found .gb
or .gbc
file in the extracted zip. To load these file types, we will use the function .loadROM(myROM)
. Where, myRom
is either a:
-
URL that we can use fetch to download the file.
-
A file object from something like an input of type="file".
-
A Uint8Array of the bytes that make up the ROM.
Here is a quick example of the process:
<input type="file" id="input" onchange="loadROM(event)">
loadROM(event) {
WasmBoy.loadROM(event.target.files[0]).then(() => {
console.log('WasmBoy ROM loaded!');
}).catch(() => {
console.error('Error loading the ROM');
});
}
Now that we have a ROM loaded, we can now play WasmBoy! Simply call .play()
and you are good to go!
WasmBoy.play().then(() => {
console.log('WasmBoy is playing!');
}).catch(() => {
console.error('WasmBoy had an error playing...');
});
To pause WasmBoy, simply call .pause()
.
WasmBoy.pause().then(() => {
console.log('WasmBoy is paused!');
}).catch(() => {
console.error('WasmBoy had an error pausing...');
});
Now that we can play and pause WasmBoy, the only thing left we would need to do for basic usage, is to allow resetting WasmBoy, with the same or a different game! But this entails multiple paths:
-
"I want to reconfigure WasmBoy". Simply called
.config
again. -
"I want to keep the same configuration, but load a different ROM". Simply called
.loadROM(myROM)
, with the new ROM. -
"I want to keep my current configuration, and the same ROM". Simply called
.reset()
, for example:
WasmBoy.reset().then(() => {
console.log('WasmBoy is reset!');
}).catch(() => {
console.error('WasmBoy had an error reseting...');
});
NOTE: calling .config()
or .reset()
pauses the game. To continue executing, simply call .play()
in the .then()
block.
Rad! We got the game playing, hooray! And you may notice, you can already play it! Even with Controllers! That is because WasmBoy uses the npm package repsonsive-gamepad for an all-in-one implementation of the GameBoy Joypad. However, you may want to add mobile controls, and this can be done with .addTouchInput()
. Please see the package documentation, or the debugger / demo implementation, on how to do this. However, you may think that the package isn't doing quite what you want it to. If so, you can disable the default Joypad implementation with .disableDefaultJoypad()
, but if you realize you want it back actually, you can use .enableDefaultJoypad()
. Also, be sure you view the correct documentation for the repsonsive-gamepad release that is being used by your wasmboy release.
Finally, the last thing that you may want to do is, allow in-game saves, and save and load states. WasmBoy uses indexedDb to allow for offline browser storage, and localStorage to catch when the browser is closing. Also, WasmBoy handles determining which ROM represents each individual gameboy game.
So now we know how it works under the hood, how do we implement this? Well, for in-game saves, WasmBoy will automatically handle backing up the Cartridge RAM for you, thus this works out-of-the-box. However, Save states require the function .saveState()
. Please note, .saveState()
will pause the game, thus .play()
would need to be called in the .then()
block to continue execution.
WasmBoy.saveState().then(() => {
console.log('WasmBoy saved the state!');
// Call .play() here to continue playing the ROM.
}).catch(() => {
console.error('WasmBoy had an error saving the state...');
});
Awesome! We saved the state! But how do we load it back? Oh wait, what if we saved like 100 states? How do we know which state to load? To get all saved states for the current loaded ROM, we can call .getSaveStates()
. Which, in the .then()
block, will return an array of all the save state objects.
WasmBoy.getSaveStates().then((arrayOfSaveStateObjects) => {
console.log('Got WasmBoy Save States for the loaded ROM: ', arrayOfSaveStateObjects);
}).catch(() => {
console.error('Error getting the save states for the loaded ROM');
});
Now that we have all the save states. We can load a specific one. I suggest showing the options to the end user, and then using the function loadState()
. Please note, .loadState()
will pause the game, thus .play()
would need to be called in the .then()
block to continue execution.
WasmBoy.loadState(saveStateFromArrayOfSaveStates).then(() => {
console.log('WasmBoy loaded the state!');
// Call .play() here to continue playing the ROM.
}).catch(() => {
console.error('WasmBoy had an error loading the state...');
});
And that's it! You now have a fully functional implementation of WasmBoy! Pat yourself on the back, and have a nice day! I suggest reading through the complete API, especially the options section, detailing the callbacks, as they allow experimenting with the emulator itself, and creating new experiences!
This documents the complete lib API. This is useful for direct reference, and what responses to expect from the lib. Please not, debugging API functions (denoted by the _underscore in front of their names) is not documented, though they are available to be used. Please see the WasmBoy index for all available API functions. However, proceed with caution, debugging API functions may change among feature versions, only the public api will be considered breaking changes.
NOTE: Almost all functions return a Promise for API consistency.
.getVersion()
Parameters: None
Returns: number
Gives the version of the currently installed WasmBoy lib.
.config(WasmBoyOptions, canvasElement)
Parameters: JS Object (WasmBoyOptions Schema), Canvas Element
Returns: Promise
This function configures WasmBoy to the passed WasmBoyOptions, and sets WasmBoy's Graphical output target to the passed Canvas element. If WasmBoy is currently running, this will pause WasmBoy, which would then require you to call .play()
in the corresponding .then()
block of the promise.
.getConfig()
Parameters: None
Returns: Object
This returns the current wasmboy config that is being used by WasmBoy.
.setCanvas(canvasElement)
Parameters: Canvas HTMLElement
Returns: None
This sets the current canvas target that is being used to output frames.
.getCanvas()
Parameters: None
Returns: CanvasHTMLElement
This returns the current Canvas element that is the target for outputting frames.
.loadROM(myROM, loadOptions)
Parameters:
myROM
is one of the following object types:
-
URL that we can use fetch to download the file.
-
A file object from something like an input of type="file".
-
A Uint8Array of the bytes that make up the ROM.
loadOptions
is an object that may have the following keys:
-
headers
- any headers you want to pass with thefetch
GET request. -
fileName
- an explicit file name to be used when determining the file type. This is useful when you have links that are not directly to the file, or do not contain a file extension.
Returns: Promise
This function loads a ROM into WasmBoy. Which can then be executed by .play()
.play()
Parameters: None
Returns: Promise
This starts, or resumes, execution of a loaded ROM in WasmBoy.
.pause()
Parameters: None
Returns: Promise
This pauses execution of a loaded ROM in WasmBoy.
.reset()
Parameters: None
Returns: None
This resets the currently running rom to the initial state. Similar to if you turned off the gameboy, and turned it back on.
isPlaying()
Parameters: None
Returns: Boolean
This returns if WasmBoy is currently playing a ROM.
isPaused()
Parameters: None
Returns: Boolean
This returns if WasmBoy is not currently playing a ROM.
isReady()
Parameters: None
Returns: Boolean
This returns if WasmBoy is configured and has a ROM loaded, so that .play()
can be called to play the ROM.
isLoadedAndStarted()
Parameters: None
Returns: Boolean
This returns if WasmBoy has had a ROM Loaded, and way played. This is useful for determining if you should allowing saving the state. And showing/hiding elements.
.saveState()
Parameters: None
Returns: Promise (resolves saveState)
This saves the current state of the executing ROM in WasmBoy. Save states are saved to indexedDb to allow for offline browser storage. Note: this will also call .pause()
, therefore to continue execution, .play()
must be called in the resulting .then()
block. Also, the .then()
block resolves with the saveState
.
.getFPS()
Parameters: None
Returns: Number
This returns the current frames per second for the running ROM.
.getSaveStates()
Parameters: None
Returns: Promise
This returns an array of all of the save states for the loaded ROM, in the .then()
block handler function of .getSaveStates()
. This is useful for allowing a user to choose a save state to be loaded.
.loadState(saveState)
Parameters: JS Object (SaveState Schema)
Returns: Promise
This loads the passed save state into WasmBoy. Note: this will also call .pause()
, therefore to continue execution, .play()
must be called in the resulting .then()
block.
.enableDefaultJoypad()
Parameters: None
Returns: Promise
This will enable the default responsive-gamepad that WasmBoy uses for Joypad Input.
.disableDefaultJoypad()
Parameters: None
Returns: Promise
This will disable the default responsive-gamepad that WasmBoy uses for Joypad Input.
.setJoypadState(joyPadState)
Parameters: Joypad State Object
Returns: None
This instantly sets the Joypad State for WasmBoy. NOTE: This should only be called after disabling the default joypad with .disableDefaultJoypad()
. See the JoyPad State Object in Object Schemas for the expected object.
.addTouchInput(seeDocumentation)
This simply calls the equivalent function in responsive-gamepad. Please see the documentation there, or the usage in the debugger / demo.
.removeTouchInput(seeDocumentation)
This simply calls the equivalent function in responsive-gamepad. Please see the documentation there, or the usage in the debugger / demo.
const WasmBoyOptionsSchema = {
headless: false,
useGbcWhenOptional: true,
isAudioEnabled: true,
frameSkip: 1,
audioBatchProcessing: true,
timersBatchProcessing: false,
audioAccumulateSamples: true,
graphicsBatchProcessing: false,
graphicsDisableScanlineRendering: false,
tileRendering: true,
tileCaching: true,
gameboyFPSCap: 60,
updateGraphicsCallback: false,
updateAudioCallback: false,
saveStateCallback: false,
onReady: false,
onPlay: false,
onPause: false,
onLoadedAndStarted: false
}
-
headless
- This will run the emulator headless. This is useful for TAS implementations, or for testing. If headless is set totrue
, then the canvas element passed in.config
, may be undefined. Headless mode will not output any graphics or audio, though you may still use the callbacks to obtain access to their respective buffers. -
useGbcWhenOptional
- Some ROMs allow for both Gameboy, and Gameboy Color playback. This sets whether these types of ROMs will choose Gameboy or Gameboy Color ROMs to execute. -
isAudioEnabled
- This enables/disables audio -
frameSkip
- This sets the number of frames that will be skipped rendering on the canvas elements. -
audioBatchProcessing
- TODO: See the Performance Options Section -
timersBatchProcessing
- TODO: See the Performance Options Section -
audioAccumulateSamples
- TODO: See the Performance Options Section -
graphicsBatchProcessing
- TODO: See the Performance Options Section -
graphicsDisableScanlineRendering
- TODO: See the Performance Options Section -
tileRendering
- TODO: See the Performance Options Section -
tileCaching
- TODO: See the Performance Options Section -
FPSCap
- The maximum number of frames per second that the core will run. This does NOT affect how many frames are outputted, but more of the speed at which the emulator runs. For instance, if theFPSCap
is set to 120, the emulator will run twice as fast. -
updateGraphicsCallback
- A function called right before passing the canvas element. The function passed into this option, takes in an ImageData Array, and can edit the image data array in place. Any modifications made to the Image Data Array will be persisted. For instance, if we wanted to invert colors in the resulting frame:
const updateGraphicsCallback = (imageDataArray) => {
// Logic from: https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Pixel_manipulation_with_canvas
for (var i = 0; i < imageDataArray.length; i += 4) {
imageDataArray[i] = 255 - imageDataArray[i]; // red
imageDataArray[i + 1] = 255 - imageDataArray[i + 1]; // green
imageDataArray[i + 2] = 255 - imageDataArray[i + 2]; // blue
}
}
-
updateAudioCallback
- A function called right before connecting our AudioBufferSourceNode to the AudioContext Destination. For some examples of this process, please see this Web Audio guide on MDN. The function passed into this option, takes in an AudioContext and a AudioBufferSourceNode, and can edit the Audio Source Node in place. Any modifications made to the Audio Source Node will be persisted. However, if you plan to connect more nodes (such as in the lowpass filter example below), you must return the final connecting node in the chain, to allow for it to be the last node to connect to the audio source destination. For an example, if we just wanted to return the exact same AudioBufferSourceNode we were passed in, it would be:
const updateAudioCallback = (audioContext, audioBufferSourceNode) => {
return audioBufferSourceNode;
}
Or if we wanted to add a simple low pass filter:
// Logic from: https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API/Using_Web_Audio_API
// And: https://developer.mozilla.org/en-US/docs/Web/API/BiquadFilterNode
// Only need to make the filter once, since the WasmBoy is a singleton, and only has a single Audio Context.
let lowPassFilter = undefined;
const updateAudioCallback = (audioContext, audioBufferSourceNode) => {
if(!lowPassFilter) {
lowPassFilter = audioContext.createBiquadFilter();
lowPassFilter.type = "lowpass";
lowPassFilter.frequency.value = 650;
}
audioBufferSourceNode.connect(lowPassFilter);
return lowPassFilter;
}
-
saveStateCallback
- A function called right before saving the Save State Object to the indexedDb. The function passed into this option, takes in a Save State object, and can edit the save state object in place. Any modifications made to the Save State object will be persisted. For example, if you wanted to add screenshots from the canvas element to every Save State object. you could do the following:
const canvasElement = document.querySelector('canvas');
const saveStateCallback = (saveStateObject) => {
saveStateObject.screenshotCanvasDataURL = canvasElement.toDataURL();
}
-
onReady
- Callback called whenever WasmBoy enters the ready state. See:WasmBoy.isReady()
. -
onPlay
- Callback called whenever WasmBoy enters the playing state. See:WasmBoy.isPlaying()
. -
onPause
- Callback called whenever WasmBoy enters the paused state. See:WasmBoy.isPaused()
. -
onLoadedAndStarted
- Callback called whenever WasmBoy enters the loadedAndStarted state. See:WasmBoy.isLoadedAndStarted()
.
const WasmBoySaveStateSchema = {
wasmBoyMemory: {
wasmBoyInternalState: [],
wasmBoyPaletteMemory: [],
gameBoyMemory: [],
cartridgeRam: []
},
date: undefined,
isAuto: undefined
}
-
wasmBoyMemory
- The individual bytes used for the save state, this should NOT be modified -
date
- The current system time the save state was made. -
isAuto
- Represents if the save state was automatically made by the browser being closed.
const WasmBoyJoypadState = {
up: false,
right: false,
down: false,
left: false,
a: false,
b: false,
select: false,
start: false
}
This names are pretty self-explanatory, and represents the buttons on the Gameboy. However, it should be known that false
is means the button is released, true
means the button is pressed.
As referenced in the Complete API introduction. There are debugging functions that are available to be used. However, proceed with caution, debugging API functions may change among feature versions, only the public api will be considered breaking changes. These debugging functions are extremely handy for running within a Node environment, for things like a headless TAS, or audio recording. But, here is a quick example of using a function from the Core API within the JS Lib directly. Also note that, all of these mentioned functions below will return a promise, since it sends messages to the wasm module within a web worker. Let's say we want to run setJoypadState
from the core. We could do the following:
// Assume within async function
const result = await WasmBoy._runWasmExport('setJoypadState', [arrayOfParameters])
Where 'setJoypadState'
is the function key on the core, and the [arrayOfParameters]
is the parameters to be passed to the function using .apply. Since the project is
Or, let's say we wanted to gain access to the internal Gameboy Memory (0x0000 -> 0xFFFF).
// Assume within async function
const location = await WasmBoy._getWasmConstant('GAMEBOY_INTERNAL_MEMORY_LOCATION');
const size = await WasmBoy._getWasmConstant('GAMEBOY_INTERNAL_MEMORY_LOCATION');
const memoryUint8Array = await WasmBoy._getWasmMemorySection(location, location + size);
This will essentially .slice the section of memory from the Wasm Memory. Where, _getWasmConstant
will return a constant from the core API, and _getWasmMemorySection
will return any section of memory within the wasm memory. Memory is visualized on the WasmBoy Memory Map.