Yesterday I blogged about building DaChip8, my C# Chip-8 interpreter (I recommend reading that post before this one; the changes detailed here will make more sense if you have an idea of the original code written). It works pretty well but running on the desktop is sooo 2010. It’s 2016, it’s Browser or Bust.
The problem with the browser is JavaScript. I know lots of you love it, but I don’t. Luckily we have many options for compiling other languages down to JavaScript and the one I happen to have been using recently at work is Bridge.NET. I thought it’d be fun to run my code through it and see what’s involved in making it run in the browser. It turned out to be much less effort than I expected and the main chip implementation is identical code to the C# version!
Live Demo
Before we go on, here it is. Running in the browser. It works for me in Chrome and Edge but not in IE (no ArrayBuffer support on XmlHttpRequest to fetch the ROM, and possibly more). It even works on Chrome for Android (with touch)! YMMV.
The ROM loaded below is Breakout (Brix hack) [David Winter, 1997] from the Chip-8 Pack.
You can use the cursor keys to move the paddle on a desktop or tap on the left/right half of the gamescreen on a mobile device.
Video
In case the demo doesn’t work in your browser, here’s a little video (taken well before it was complete, but my internet sucks too much to upload another one!):
Code Changes
Obviously since rendering on the web is very different to rendering in WinForms I had to make some changes. I also had to work with C# 5 for Bridge (for now). The changes are all external to the Chip8 class (which has hooks to call out for rendering the buffer to screen or playing a beep to keep it platform-agnostic). Below is a summary of the changes I had to make.
C# 5
Currently Bridge.NET only supports C# 5 so I had to make a couple of changes for it to compile.
Expression-bodied methods had to become normal methods:
// C# 6
void AddXToI(OpCodeData data) => I += V[data.X];
// C# 5
void AddXToI(OpCodeData data)
{
I += V[data.X];
}
String interpolation had to be replaced with string.Format
calls:
// C# 6
Debug.WriteLine($"Drawing {data.N}-line sprite from {I} at {startX}, {startY}"));
// C# 5
Debug.WriteLine(string.Format("Drawing {0}-line sprite from {1} at {2}, {3}", data.N, I, startX, startY));
Key and Touch Mappings
I had to tweak the key mappings (which previously used the .NET Framework’s Keys
enum) to my own KeyCode enum which contains the keys required (I added cursor keys here to make the demo ROM better which was missing from the desktop version).
Game Loop Changes
In the C# version my game loop hooked the Idle
event of the WinForm and used a timer to tell when it was time to call Tick
and Tick60Hz
. The Bridge version turned out to be much simpler using setInterval
:
static void StartGameLoop()
{
Window.SetInterval(chip8.Tick, targetElapsedTime);
Window.SetInterval(chip8.Tick60Hz, targetElapsedTime60Hz);
}
Actually, it turned out to be slightly more complicated than that. setInterval
has a minimum resolution of 4ms, so I had to tweak this slightly when I realised the game loop wasn’t running as fast as expected (which meant a slower game - Chip-8 games don’t account for their run speed).
// HTML5 has minimum resolution of 4ms
// http://developer.mozilla.org/en/DOM/window.setTimeout#Minimum_delay_and_timeout_nesting
static readonly int minimumSetIntervalResolution = 4;
static void StartGameLoop()
{
Window.SetInterval(Tick, minimumSetIntervalResolution);
Window.SetInterval(chip8.Tick60Hz, targetElapsedTime60Hz);
}
static void Tick()
{
var numTicksToExecute = minimumSetIntervalResolution / targetElapsedTime;
for (var i = 0; i < numTicksToExecute; i++)
chip8.Tick();
}
ROM Loading
In the C# version I just used File.ReadAllBytes
to read the ROM from the disk. Obviously I can’t do that in the browser so I used XmlHttpRequest
to fetch the ROM. I had to refactor the code a little to allow starting the game when that completed (the C# version loaded synchronously).
[Ready]
public static void OnReady()
{
// ...
// Kick off async loading of ROM.
BeginLoadRom(ROM);
}
static void BeginLoadRom(string rom)
{
var req = new XMLHttpRequest();
req.ResponseType = XMLHttpRequestResponseType.ArrayBuffer;
req.Open("GET", rom);
req.OnLoad = e => EndLoadRom(GetResponseAsByteArray(req));
req.Send();
}
// Convert the response to a byte[] as that's what our existing C# expects.
static byte[] GetResponseAsByteArray(XMLHttpRequest req)
{
return new Uint8Array(req.Response as ArrayBuffer).As<byte[]>();
}
// After loading the ROM, only then is it valid to begin the game loop.
static void EndLoadRom(byte[] data)
{
chip8.LoadProgram(data);
StartGameLoop();
}
Rendering Changes
The constructor of my Chip8 class takes an Action<bool[,]>
which gets called whenever the screen needs to be rendered. This made it easy to change the rendering code without making changes to Chip8. I used HTML5 canvas to render to:
<canvas id="screen" width="64" height="32" style="min-width: 640px; min-height: 320px;"></canvas>
Note the width
/height
on a canvas relate to the coordinates being used for drawing and not the actual size in the document. These settings make the canvas ten times bigger than the resolution of the Chip-8.
I did a bit of Googling to see what’s the fastest way to render pixels to a canvas and found a solution that involves creating ImageData
for the pixel then rendering that at each location:
// Set up canvas rendering.
screen = Document.GetElementById<HTMLCanvasElement>("screen");
screenContext = screen.GetContext(CanvasTypes.CanvasContext2DType.CanvasRenderingContext2D);
lightPixel = screenContext.CreateImageData(1, 1);
lightPixel.Data[1] = 0x64; // Set green part (#006400)
lightPixel.Data[3] = 255; // Alpha
static void Draw(bool[,] buffer)
{
var width = buffer.GetLength(0);
var height = buffer.GetLength(1);
// For performance, we only draw lit pixels so we need to clear the screen first.
screenContext.ClearRect(0, 0, width, height);
// Render each lit pixel.
for (var x = 0; x < width; x++)
for (var y = 0; y < height; y++)
if (buffer[x, y])
screenContext.PutImageData(lightPixel, x, y);
}
Sound Changes
In the WinForms version I used Console.Beep
. As with the drawing, I pass an action into the Chip8 constructor for this (an Action<int>
this time, withthe int being the duration). The simplest way to play a beep in JS seemed to be with an HTML5 Audio element with a data-url for the beep. I didn’t have control of the duration here, but I figured it’d work fine for now.
// Set up audio.
// http://stackoverflow.com/a/23395136/25124
beep = new HTMLAudioElement("data:audio/wav;base64,//uQRAAAAWM (trim a big long string)");
static void Beep(int milliseconds)
{
beep.Play();
}
Unfortunately I came across an issue when testing this on mobile. You can’t play sounds that weren’t initiated by the user! As a workaround, I made the sound play when you tap to start the emulator.
Touch Support
Since a large portion of traffic to this blog comes from mobile (and I thought having an embedded demo would be pretty cool), I figured I should ensure it works there. I added some touch handlers to the canvas that just emulated pressing the cursor keys based on the side of the canvas that was touched:
// Set up touch events for canvas so we can play on mobile.
screen.OnTouchStart += SetTouchDown;
screen.OnTouchEnd += SetTouchUp;
static void SetTouchDown(TouchEvent<HTMLCanvasElement> e)
{
if (e.Touches[0].ClientX < 320)
chip8.KeyDown(keyMapping[KeyCode.LeftCursor]);
else
chip8.KeyDown(keyMapping[KeyCode.RightCursor]);
e.PreventDefault();
}
static void SetTouchUp(TouchEvent<HTMLCanvasElement> e)
{
chip8.KeyUp(keyMapping[KeyCode.LeftCursor]);
chip8.KeyUp(keyMapping[KeyCode.RightCursor]);
e.PreventDefault();
}
And that was pretty much it!
All source code for this project can be found on GitHub.