Rival Fortress will use a custom 3D game engine that I’m building from scratch in C++ and a bit of Assembly. Currently the only dependencies for the game are SDL, and the truetype library from Sean Berret.
The engine is still in active development, but so far the most fun and interesting feature has been the Hot Swappable Game Modules.
Zippy iteration times are very important to me. I hate having to wait around for compile times and having to close and re-open the game just to see some small changes in action, especially when I’m tweaking some game mechanic.
A commonly used approach is data-oriented game engine design, or as Unreal Engine calls it data driven gameplay, where the engine supports loading gameplay variables from external files that are usually human editable, like CSV tables or Excel spreadsheets.
This works well for tweaking existing variables, but doesn’t allow you to add or remove variables, or even functions.
This can be very useful, especially when thinking about game moddability, but the cost is in performance: A scripting language will never be as fast as native well optimized C/C++.
The route I chose to take for Rival Fortress is to split the engine and game into shared libraries (DLLs for Windows, dylibs for OSX and libs on Linux). The game itself is launched through a thin shell executable that takes care of the initial bootstrapping and in turn loads all compatible libraries found in the game directory.
This is the stripped down part that makes it happen:
GameLibs is an array of
wchar_t on Windows) filenames populated by crawling the game directory and looking for libraries supported by the host operative system.
MPE_LoadGameLib tries to load the shared library in memory and checks if it provides a valid entry point like so:
EntryPoint is a function pointer that returns metadata information of the library, like version, dependencies, run priority, etc. It also contains the API with function pointers that will be called by the shell during execution, like so:
As you can see, each library has its own state that is allocated from the main game allocator, as well as access to the engine and the global
The Game Loop
Everything comes together in the main game loop as you can see in the following excerpt:
Each time through the main loop a link list sorted by priority is walked. Each module is reloaded if stale and the
Step function pointer is called.
The Janitor does all the work
To ensure game modules are always up to date, the engine keeps a Janitor worker in a low priority thread that keeps checking the last write timestamp of the loaded modules, as well as looking for new modules in the game directory, and keeping the linked list sorted.
This allows the game to be edited and tweaked in real-time, as each recompile is picked up immediately.
The game state of each module is also preserved, since the main shell owns the memory from which it is allocated (I’ll talk more about the memory model used in Rival Fortress in a future update.)
All in all this has been a very fun feature to implement, and while is not really essential, it has been saving me a lot of “dead time” while rapidly iterating on ideas/implementations.