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.