Let’s write Pong in WebAssembly

Michael Bebenita
WebAssembly Pong

Demo Fiddle

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.

How the code is partitioned.

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]);

Michael Bebenita

Written by

Researcher at Mozilla

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade