Using Box2D and SDL2 in a WASM project

TL;DR: After some initial problems with CMake I had a great experience with C++ to WASM. During this experiment, things that compiled natively, also compiled to WASM without any problems.

Recently on HN, there have been a lot of WASM-related posts. The ability to run native code in the browser sounds pretty amazing and I was blown away by Funky Karts, a C++ game that the author compiled to WASM (and provided pretty detailed documentation of the process). So far, WASM is a nascent technology and the success of your project shouldn't probably depend on it. However, I wanted to get some sort of an idea how reliable it is at the moment and what would be possible with it. As a result, I produced this tiny demo that renders things with SDL2 and uses Box2D for movement and collision-detection. It looks pretty terrible, but I'm left convinced that it's possible to do something with WASM and even use some well-known libraries. Check the repo for further details.

Building

I'm under the impression that at the moment CMake is the easiest way to build your C++ WASM project. I had no prior experience with CMake so it was pretty painful to get the compile commands right at first. However, after getting into terms with it, I really prefer CMake over graphical IDEs I'm used to when linking C++.

We can define a CMake script which enables us to compile to native target like this:

cmake ..
make

And to WASM target like this:

emcmake cmake ..
emmake make

See the full script.

Some details

This tells the linker where the includes and the sources are. At it's present form, it takes everything from ./src/.

include_directories(${CMAKE_SOURCE_DIR}/include/Box2D/)

file(GLOB SRC
    "src/*.h"
    "src/*.cpp"
)

We can use a single CMake script for both native and WASM target.

if (${CMAKE_SYSTEM_NAME} STREQUAL "Emscripten")

    # If build target WASM.

else ()

    # If build target native.

endif ()

Since the native and WASM libraries have to be compiled with different compilers, you'll need to take that into account in linking the project. In the following we put WASM libs to ./lib/web/ and native libs to ./lib/native/.

if (${CMAKE_SYSTEM_NAME} STREQUAL "Emscripten")
  # ...
  link_directories(${CMAKE_SOURCE_DIR}/lib/web)
  # ...
else ()
  # ...
  link_directories(${CMAKE_SOURCE_DIR}/lib/native)
  # ...
endif ()

It's worth pointing out that we use a different compiler for WASM.

if (${CMAKE_SYSTEM_NAME} STREQUAL "Emscripten")
  # ...
  set(CMAKE_CXX_COMPILER "em++")
  # ...
else ()
  # ...
endif ()

You'll need to make similar changes to compile commands when compiling library that will be linked with an emscripten project.

About the C++ part

The demo code isn't very interesting, I'll describe it on a general level. The main loop is extremely generic.

void main_loop() {
  update();
  draw();
}

The update function

The update function measures the time since the last frame and simulates physics based on the elapsed time (Step is a function defined in Box2D World class). Then any SDL events (such as keydown) are processed by first turning them our custom event object which are then passed to game objects (physical things you see on the screen).

void update() {
  // Tick the timer and simulate physics.
  timer.tick();
  world->Step(timer.delta, 1, 1);

  // Loop through SDL_Events
  SDL_Event e;
  if (SDL_PollEvent(&e)) {

    // Parse the SDL_Event to our custom event object.
    std::unique_ptr<EventData> event_data =
        std::unique_ptr<EventData>(new EventData(e));

    // Loop through objects
    for (auto const &o : game_state->objects) {
      o->handle_event(event_data.get());
    }

    // Break from the main loop if quit received.
    run = !event_data->quit;
  }
}

By the way, notice a design-decision here. I wanted to be able to pass event data to all game objects, but they should respond to events differently based on whether they are, for example, the player or some other object. After digging around a bit it seems that the OOP way to do this is to inherit and override: The vector game_state->objects contains objects of a generic type GameObject which defines a virtual event handler that does nothing.

class GameObject {

/* ... */

  virtual void handle_event(EventData *event_data) {}

/* ... */

};

The Player class inherits GameObject and overrides the superclass handle_event to change the player velocity.

class Player : public GameObject {

/* ... */

  void handle_event(EventData *event_data) override {
  // Alter the player velocity.
  }

/* ... */

};

See the complete game_object.h for details.

The render function

The render function is quite self-explanatory.

void draw() {

  // Clear the screen.
  SDL_RenderClear(interface->renderer.get());

  // Invoke render() of each GameObject.
  for (auto const &o : game_state->objects) {
    o->render(interface.get(), game_state->camera->GetPosition(), screen_size);
  }

  // Flip the buffers.
  SDL_RenderPresent(interface->renderer.get());
}

The implementation of render() in GameObject takes care of positioning the sprites according to the Box2D body positions.