Runnning A Raylib Game in the Browser with WebAssembly

I wrote a simple game, it’s here.

It’s a simple pong clone. I’m not a game developer my goals here were mostly educational. I wanted to brush up on my C skills, which I haven’t worked on since an Operating Systems class I took in Uni. I also wanted to see how hard it would be to port a desktop application to web, and what are the tradeoffs in doing so.

Building for Desktop

It’s a small game so the desktop build is very quick and easy. I just use pkg-config to handle my dependency on raylib.

cc pong.c `pkg-config --libs --cflags raylib` -o pong;

Building for Web

It’s not so easy for web. I won’t go through all the steps in great detail, there’s a great guide raylib here: https://github.com/raysan5/raylib/wiki/Working-for-Web-(HTML5)

But the steps I took were 1. Installing Emscripten I had some trouble with the version I installed with my package manager, so I ended up building from source. Building from source was relatively easy and painless through. 2. Building raylib. This is just the framework I used for the game. 3. Add the generated raylib.h and libraylib.a files to my project I added them to the creatively named /include and /lib directories.

Preparing the code for Web

This is the most concerning part if you’re trying to build cross platform. If you have to change the code too much, at a certain point you may as well have written a seperate codebase for each platform.

The good news is that it’s relatively easy. I did need to move a lot of logic out of the main function into a new gameTick function that looks like this

void gameTick(void *arg) {
    struct GameState *gamesState = (struct GameState *) arg;
    ...
}

then pass the game state down to the function in the main loop.

    while (!WindowShouldClose()) {
        gameTick(&gamesState);
    }

This is so that it is easier to pass the same function into emscripten_set_main_loop for web.

#if defined(PLATFORM_WEB)
    typedef void (*myFuncDef)(void *);
    myFuncDef cb = &gameTick;
    emscripten_set_main_loop_arg(cb, &gamesState, 0, 1);
#else
    while (!WindowShouldClose()) {
        gameTick(&gamesState);
    }
#endif

It’s not ideal, because the game state isn’t type safe, but given we’re only using it in these two places, it’s not really the end of the world.

(emscripten_set_main_loop)[https://emscripten.org/docs/api_reference/emscripten.h.html#c.emscripten_set_main_loop], with fps set to 0 or lower, hands more contol over to the browser. It uses the browsers requestAnimationFrame to ensure a smoother framerate and helps make sure our program plays well with the browser.

This is the largest change I needed to make. To toggle betweeen web and desktop I just toggle the definition of PLATFORM_WEB by commenting or uncommenting it. I’ll have to find a better solution for future projects, but for this simple thing it works.

Resources

I thought the biggest problem would be loading resources. I added the music and sound effects mostly to see what how this works. It turns out this is all handled by emscripten automatically all you need to do is add --preload-file {your_file} and it will work as expected.

The Makefile for web

CC=emcc
CFLAGS=-Iinclude/
LDFLAGS=-s USE_GLFW=3 -s USE_WEBGL2=1 -s FULL_ES3=1 -Iinclude/ -Llib/
LIBS=-lraylib
OBJS=pong.o
BIN=main.html

all:$(BIN)

main.html: $(OBJS)
    $(CC) $(OBJS) $(LDFLAGS) $(LIBS) -o main.html --preload-file collision.wav --preload-file song_loop.wav

pong.o: pong.c
    $(CC) $(CFLAGS) -c {{_}}lt; -o $@

clean:
    $(RM) -r main.html *.wasm *.o *.dSYM *.js

This is what my final makefile looked like. It’s very basic so I could have just written the command in an sh file, but I need to learn make at some point.

To switch between the two platforms I use this build file:

#!/bin/sh

if [ "$1" == "web" ]; then
    echo "clearing old stuff out...";
    make clean;
    echo "building for web...";
    make;
elif [ "$1" == "clean" ]; then
    echo "cleaning old stuff...";
    make clean;
    rm pong;
    echo "all clean";
else
    echo "building for desktop...";
    cc raygame.c `pkg-config --libs --cflags raylib` -o pong;
    echo "build finished";
fi

Improving

I’ve started a new project where I’ve learnt more about make so that I can move more build logic into the Makefile. This project has a Makefile that looks like this

all: $(BIN)

$(BIN): $(OBJ)
    $(CC) $(OBJ) -O3 $(PKG_CONF) -o $(BIN)

dev: $(OBJ)
    $(CC) -g -O0 $(OBJ) $(PKG_CONF) -o $(BIN)

%.o:%.c
    $(CC) -c -o $@ $^ $(INC)


clean:
    rm src/*.o; rm $(BIN); rm -r $(WEB_BIN) $(WEB_DIR)/*.wasm $(WEB_DIR)/*.js $(WEB_DIR)/*.data;rm -f $(UNIT_BIN) $(BENCH_BIN); rm bench/*.o

web: $(WEB_BIN)

$(WEB_BIN): $(WEB_OBJ)
    $(WEB_CC) $(WEB_OBJ) $(WEB_LDFLAGS) $(WEB_LIBS) -o $(WEB_BIN) --preload-file resources 
%_w.o:%.c
    $(WEB_CC) $(WEB_FLAGS) $(WEB_CONST) -c {{_}}lt; -o $@

test: $(UNIT_BIN)
    $(TEST_ENV) $(UNIT_BIN)

$(UNIT_BIN): $(MUNIT_HEADER) $(UNIT_SRC) $(UNIT_OBJ)
    $(CC) $(UNIT_OBJ) $(CFLAGS) $(UNIT_SRC) $(PKG_CONF) -o $(UNIT_BIN)

bench: $(BENCH_BIN)
    $(BENCH_ENV) $(BENCH_BIN)

$(BENCH_BIN): $(BENCH_SRC) $(BENCH_OBJ)
    $(CC) $(BENCH_OBJ) -O3 $(CFLAGS) $(BENCH_SRC) $(PKG_CONF) -o $(BENCH_BIN)

It has specific commands for web, as well as another “dev” command that builds the project so that a debugger will work on it. I also included a “test” and a “bench” command for benchmarking and unit testing.

Conclusion

My original conclusion was going to be about how easy it is to develop multiplatform apps now thanks to WebAssembly. That is true, but the biggest lesson here is how powerful build tools can be, and how important it is to use them well.

I’ve focused on my original Makefile and bashscript in this post, not because it’s a very good build system, but because it’s a very basic one. When your first learning a new system it doesn’t matter if you do things well it only matters that you do them.