Let’s write Pong in WebAssembly
I’ll be writing the game mostly in C and with a little help from JavaScript for rendering and event handling. Since I’ve got two languages to work with, I could slice the application in several ways but I want to write as much of the game in C as I can, and only use JavaScript for library support. I will not include any .h
files thus no external libraries will be available to me, no printf
, no malloc
, just plain old C. The point of this exercise is to demonstrate how to wire up a minimal WebAssembly program.
Here are all the JavaScript bits. I instantiate the module and provide an environment object that the instance will link to. Most of these functions are straight forward, except for setInterval
which receives the C function pointer f
(as a number, not a function value) and then calls the exported runCallback
C function to dispatch to the appropriate target function on the C side.
var ctx = ... // CanvasRenderingContext2D
var wasmModule = new WebAssembly.Module(wasmCode);
var wasmInstance = new WebAssembly.Instance(wasmModule, {
env: {
jsSetInterval: function (f, n) {
setInterval(function () {
wasmInstance.exports.runCallback(f);
}, n);
},
jsFillRect: function (x, y, w, h) {
ctx.fillRect(x, y, w, h);
},
jsClearRect: function (x, y, w, h) {
ctx.clearRect(x, y, w, h);
}
}
});wasmInstance.exports.main();
On the C side, setInterval
is declared as extern
(meaning that it needs to be defined outside of the module). The runCallback
function is used to dispatch on the received function pointer.
extern setInterval(int (*callback)());int runCallback(int (*callback)()) {
return callback();
}
These two functions make it possible to wire up the event loop directly from C code
int tick() {
...
}int main() {
jsSetInterval(tick);
}
Any function defined outside of the module needs to be marked as extern
extern void jsFillRect(int x, int y, int w, int h);
extern void jsClearRect(int x, int y, int w, int h);
Now, let’s write our game. I’ll need some basic data types
typedef struct Vec2 {
float x;
float y;
} Vec2;typedef struct Rect {
float x;
float y;
float w;
float h;
} Rect;typedef enum Type {
WALL,
BALL
} Type;typedef struct Object {
Type t; // Object Type
Rect r; // Bounds
Vec2 v; // Velocity
} Object;
and a way to keep track of all the objects in the world.
Object world[] = {
{.r = { 0, 0, W, 32 }}, // Top Wall
{.r = { 0, H - 32, W, 32 }}, // Bottom Wall
{.r = { -32, 0, 32, H }}, // Left Wall
{.r = { W, 0, 32, H }}, // Right Wall
{.r = { 16, H/2 - 64, 32, 128 }}, // Left Paddle
{.r = { W - 48, H/2 - 64, 32, 128 }}, // Right Paddle
{.t = BALL, .r = { W/2, H/2, 32, 32 }, .v = { 0.9f, 0.7f }}
};
malloc
/ free
are not available so I need to declare all the game objects statically.
Now, I have enough to draw the game world
int drawWorld() {
int c = sizeof(world) / sizeof(Object);
for (int i = 0; i < c; i++) {
fillRect(world[i].r);
}
}
The tick
function draws the world and advances the position of any game object that can move.
int tick() {
jsClearRect(0, 0, W, H);
drawWorld();
move(&world[6]); // 6th game object is our ball.
}
The most complicated part of the game is the move
function which attempts to move a game object as long as it doesn’t intersect any other world object.
void move(Object *o) {
int c = sizeof(world) / sizeof(Object); Rect n = o->r;
n.x += o->v.x;
n.y += o->v.y; for (int i = 0; i < c; i++) {
Rect r = world[i].r;
if (world[i].t == o->t) {
continue;
}
if (o != &world[i] && rectOverlap(n, r)) {
// X Axis
n = o->r;
n.x += o->v.x;
if (rectOverlap(n, r)) {
o->v.x *= -1;
}
// Y Axis
n = o->r;
n.y += o->v.y;
if (rectOverlap(n, r)) {
o->v.y *= -1;
}
// Move
n.x += o->v.x;
n.y += o->v.y;
}
}
o->r = n;
}
And I’m done. Well, at least I am, you can take it from here. Here’s a fiddle where you can run the example and extend it to include mouse and/or keyboard events to control the paddles, score tracking, fancier collision response, etc.
The C code doesn’t use malloc
and/or free
but it does need some stack space. The compiled code stores the stack address as a 32-bit value at address 0x4
so before calling main, I need to allocate some stack space using
new Int32Array(wasmInstance.exports.memory.buffer)[1] = 0x2000;
Really strange things can happen if you don’t remember to allocate enough stack space before calling main
.
How else could I have architected this game?
So, C is kinda hard to write, and most of the functions are trivial enough that I can move them over to JavaScript. This leads me to the following architecture:
I want to keep the complicated move
function and the game state in C for performance reasons.], but now I have the problem that the drawWorld
JavaScript function needs to read the state, which is in C. drawWorld
needs to read the Rect struct within the Object struct. Since I have access to the module’s memory, I could poke around (provided that I knew the offsets) and read the structs that way. Alternatively, I could write a C helper function to return the members of the Rect struct (which feels kind of gross).
int getRectMember(int i, int n) {
if (n == 0) return world[i].r.x;
if (n == 1) return world[i].r.y;
if (n == 2) return world[i].r.w;
if (n == 3) return world[i].r.h;
}
What if I move the game state to JavaScript?
This is even worse, because the move
function reads (and writes) a lot of data fields, much more than drawWorld
does. To make this work, I would need to write several JavaScript helper functions to read/write member fields making the code both inefficient and horrible.
What if I copy the state back and forth?
Before I call move
, I could copy the program state into C structs then move it back after move
is done. Better than the previous approach, but still bad, because I would now have to worry about keeping the two states in sync.
What if I write everything in C?
For completeness sake, I could also move everything into C. clearRect
and fillRect
are relatively simple drawing routines and I could implement them from scratch. However, I would now have to copy a large image buffer on every frame into the Canvas object on the JavaScript side. There’s no real benefit to this, especially since the browser’s native clearRect
/ fillRect
methods are much faster than what I could write (even in WebAssembly).
I think the original architecture was the best option.
One final note, the WebAssembly program is 1,394 bytes when compiled with -Os.
(I can get it down to 1,190 bytes if I avoid passing structs by value.)
var wasmCode = new Uint8Array([0,97,115,109,1,0,0,0,1,166,128,128,128,0,7,96,0,1,127,96,4,127,127,127,127,1,127,96,1,127,1,127,96,0,0,96,3,125,125,125,1,127,96,2,127,127,1,127,96,1,127,0,2,182,128,128,128,0,3,3,101,110,118,11,106,115,67,108,101,97,114,82,101,99,116,0,1,3,101,110,118,10,106,115,70,105,108,108,82,101,99,116,0,1,3,101,110,118,11,115,101,116,73,110,116,101,114,118,97,108,0,2,3,137,128,128,128,0,8,2,0,4,5,6,0,0,3,4,133,128,128,128,0,1,112,1,2,2,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,214,128,128,128,0,8,6,109,101,109,111,114,121,2,0,11,114,117,110,67,97,108,108,98,97,99,107,0,3,9,100,114,97,119,87,111,114,108,100,0,4,12,118,97,108,117,101,73,110,82,97,110,103,101,0,5,11,114,101,99,116,79,118,101,114,108,97,112,0,6,4,109,111,118,101,0,7,4,116,105,99,107,0,8,4,109,97,105,110,0,9,9,136,128,128,128,0,1,0,65,0,11,2,10,8,10,151,135,128,128,0,8,135,128,128,128,0,0,32,0,17,0,0,11,194,128,128,128,0,1,1,127,65,188,126,33,0,3,64,32,0,65,216,1,106,42,2,0,168,32,0,65,220,1,106,42,2,0,168,32,0,65,224,1,106,42,2,0,168,32,0,65,228,1,106,42,2,0,168,16,1,26,32,0,65,28,106,34,0,13,0,11,32,0,11,141,128,128,128,0,0,32,0,32,1,96,32,0,32,2,95,113,11,153,129,128,128,0,2,2,125,2,127,2,64,2,64,32,0,42,2,0,34,2,32,1,42,2,0,34,3,93,32,2,32,2,92,32,3,32,3,92,114,114,13,0,65,1,33,4,32,2,32,3,32,1,42,2,8,146,95,13,1,11,32,3,32,2,96,32,3,32,2,32,0,42,2,8,146,95,113,33,4,11,2,64,2,64,32,0,42,2,4,34,2,32,1,42,2,4,34,3,93,32,2,32,2,92,32,3,32,3,92,114,114,13,0,65,1,33,5,32,2,32,3,32,1,42,2,12,146,95,13,1,11,32,3,32,2,96,32,3,32,2,32,0,42,2,12,146,95,113,33,5,11,32,4,32,5,113,11,216,132,128,128,0,7,4,125,1,127,2,125,1,127,4,125,7,127,4,125,32,0,65,8,106,42,2,0,34,2,32,0,65,16,106,42,2,0,34,4,146,33,7,32,2,32,0,65,24,106,34,13,42,2,0,34,20,146,33,23,32,0,65,16,107,33,8,32,0,42,2,4,34,1,32,0,42,2,20,34,21,146,33,22,32,1,32,0,65,12,106,42,2,0,34,3,146,33,6,32,0,40,2,0,33,5,65,0,33,17,32,0,65,20,106,33,16,3,64,2,64,32,8,32,17,70,13,0,32,17,65,16,106,40,2,0,32,5,70,13,0,32,17,65,32,106,42,2,0,33,12,32,17,65,24,106,42,2,0,33,10,32,17,65,20,106,42,2,0,34,9,32,17,65,28,106,42,2,0,146,33,11,2,64,2,64,32,22,32,9,93,32,22,32,22,92,32,9,32,9,92,34,14,114,114,13,0,65,1,33,18,32,22,32,11,95,13,1,11,32,9,32,22,96,32,9,32,22,32,3,146,95,113,33,18,11,32,10,32,12,146,33,12,2,64,2,64,32,23,32,10,93,32,23,32,23,92,32,10,32,10,92,34,15,114,114,13,0,65,1,33,19,32,23,32,12,95,13,1,11,32,10,32,23,96,32,10,32,23,32,4,146,95,113,33,19,11,32,18,32,19,113,65,1,71,13,0,2,64,2,64,32,1,32,21,146,34,23,32,9,93,32,23,32,23,92,32,14,114,114,13,0,65,1,33,18,32,23,32,11,95,13,1,11,32,9,32,23,96,32,9,32,23,32,3,146,95,113,33,18,11,2,64,2,64,32,2,32,10,93,32,2,32,2,92,32,15,114,114,13,0,65,1,33,19,32,2,32,12,95,13,1,11,32,10,32,2,96,32,10,32,7,95,113,33,19,11,2,64,32,18,32,19,113,65,1,71,13,0,32,16,32,21,140,34,21,56,2,0,11,32,2,32,20,146,33,23,2,64,2,64,32,1,32,9,93,32,1,32,1,92,32,14,114,114,13,0,65,1,33,18,32,1,32,11,95,13,1,11,32,9,32,1,96,32,9,32,6,95,113,33,18,11,2,64,2,64,32,23,32,10,93,32,23,32,23,92,32,15,114,114,13,0,65,1,33,19,32,23,32,12,95,13,1,11,32,10,32,23,96,32,10,32,4,32,23,146,95,113,33,19,11,2,64,32,18,32,19,113,65,1,71,13,0,32,13,32,20,140,34,20,56,2,0,11,32,23,32,20,146,33,23,32,1,32,21,146,33,22,11,32,17,65,28,106,34,17,65,196,1,71,13,0,11,32,0,65,8,106,32,23,56,2,0,32,0,65,4,106,32,22,56,2,0,11,155,128,128,128,0,1,1,127,65,0,65,0,65,128,8,65,128,8,16,0,26,16,4,26,65,184,1,16,7,32,0,11,137,128,128,128,0,0,65,1,16,2,26,65,42,11,131,128,128,128,0,0,0,11,11,203,129,128,128,0,1,0,65,16,11,196,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,128,68,0,0,0,66,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,120,68,0,0,128,68,0,0,0,66,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,194,0,0,0,0,0,0,0,66,0,0,128,68,0,0,0,0,0,0,0,0,0,0,0,0,0,0,128,68,0,0,0,0,0,0,0,66,0,0,128,68,0,0,0,0,0,0,0,0,0,0,0,0,0,0,128,65,0,0,224,67,0,0,0,66,0,0,0,67,0,0,0,0,0,0,0,0,0,0,0,0,0,0,116,68,0,0,224,67,0,0,0,66,0,0,0,67,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,68,0,0,0,68,0,0,0,66,0,0,0,66,102,102,102,63,51,51,51,63]);