How to make a game engine for WebAssembly: Part 3
(index)
Welcome back!
I know we’ve been going through a bunch of “boring” stuff, and you’re probably itching to get something that looks like a game up and running. Unfortunately, we’ve still got a few more things we need to take care of first. I know this hasn’t been the most exciting series so far, but taking care of the fundamentals will make our engine better in the long run.
Also, I made a GitHub repository for this series! This post will start with the code in “part 2” and end up with the code in “part 3” by the end.
Making native builds
This part is mainly about compiling and running native executables for our games. Why would we want to do this if we’re building a web game? I can think of several reasons:
- It’s faster to make and run a native build than a web build
- Being able to make a native build in addition to a web build is a nice thing to have.
- Debugging a webassembly application is hard, and if possible, it is much easier to debug and fix the problem in a native build.
As a bonus, having native builds will make part 4 (which is about reading files) much easier to explain.
Changing our I/O
For native builds, we’ll have text printed to standard output instead of a custom terminal. We’ll also read standard input, since it won’t have the same pitfalls as last time.
Emscripten defines an __EMSCRIPTEN__ macro when compiling. We can check when it’s not defined and use a different print function.
Open web.cpp. I’m going to rename this file to io.cpp to better reflect its updated purpose. Then, replace lines 2–24 with this:
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
//web version of print
EM_JS(void, printJS,(const char* toPrint),{
const MAX_CHARS = 65536;
output = document.getElementById("output");
output.value += UTF8ToString(toPrint);
if(output.value.length > MAX_CHARS){
output.value = output.innerText.substr(-MAX_CHARS);
}
output.scrollTop = output.scrollHeight;
});
void print(const char* toPrint){
printJS(toPrint);
};
#else
//native version of print
#include <stdio.h>
void print(const char* toPrint){
//some compilers *really* want you to use their "_s" variants of printf.
#ifndef printf_s
#define printf_s printf
#endif
printf_s("%s",toPrint);
}
#endif
Basically: If we’re making a web build, we use our previous version of print, and if we’re making a native build, we use our new version which prints to standard output.
That’s it for standard input. Now, we can replace our int main() function in main.cpp to read from standard input:
int main(){
println("Hello, world!");
#ifndef __EMSCRIPTEN__
while(true){
//get line from standard input
std::string input;
std::getline(std::cin, input);
//process the line
processInput(input.c_str());
}
#endif
return(0);
};
Also, insert this line after #include main.h:
#include <iostream>
That’s all the changes we need to make.
At this point, I’d recommend making and running a web build to make sure our code still works there. If you renamed web.cpp to io.cpp like I did, remember to change your build command to reflect that.
Here’s an updated command to build the project, for your convenience:
emcc main.cpp io.cpp -s WASM=1 -o index.html --shell-file shell.html -sEXPORTED_FUNCTIONS=_onConsoleInput,_main -sEXPORTED_RUNTIME_METHODS=ccall,cwrap
Compiling a native build should be as simple as passing in all the input files to your non-Emscripten compiler. For example, here’s a command for MSVC:
cl src/*.cpp /EHsc
And here’s a command for g++:
g++ src/*.cpp
When you run the executable, it should behave like the web version but use a native terminal emulator.
Debugging native builds
Since this falls under “general c/c++ knowledge”, I’ll assume you already know how to do this. As mentioned earlier, it’s usually much easier to debug problems in native builds.
Debugging WASM builds
But what if we need to debug our web builds?
One option is to print text to standard output. Even when we’re using a custom shell file, printing to standard output will print the corresponding text to the developer console. I would hesitate to call this a “real” debugging solution, but the Emscripten docs has a section on printf debugging. Despite its flaws, printf debugging is a useful skill to have, especially on rare occasions when printing to the console is the only tool you have.
For “real” debugging, you can put the following options in your build command:
-gsource-map --source-map-base "./" --no-emrun-detect
(aside: if you’re wondering why I’m not using emrun here: by default, it sets EXIT_RUNTIME=1, which means the program will terminate after our main function does. This breaks our build, since we need to call processInput() after main() finishes. We can fix this by adding -s NO_EXIT_RUNTIME=1, but this workaround seems to be very finicky in my experience, and all emrun does is mirror console output to your terminal.)
This will let you set breakpoints in your C++ code. In my experience, debugging this way is only somewhat useful. There are some more advanced debugging features, but it seems like they haven’t reached mass adoption yet.
(aside: “hasn’t reached mass adoption yet” seems to perfectly describe WebAssembly. If only the ecosystem was just a little more robust…)
That’s it!
Thanks for reading!