Metric Panda Games

One pixel at a time.

Hot Swappable Game Modules

Rival Fortress Update #1

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.

Fast iteration

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.

Data oriented/driven

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.

Scripting

Another approach often used is integrating a scripting using languages like Lua, Python or JavaScript in the game engine. This is dobe by exposing bindings to the scripting language and a callable API that can be used to modify in real time game behavior.

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++.

Modularity

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:

for(u32 LibIdx = 0; LibIdx < LibCount; ++LibIdx)
{
  MPEGameLib* GameLib = MPE_LoadGameLib(Engine, GameLibs[LibIdx]);
  if (GameLib)
  {
    // Lib initialization
  }
  else
  {
    MPE_LogWarn("Couldn't load: %s", GameLibs[LibIdx]);
  }
}

GameLibs is an array of char* (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:

GameLib->Handle = dlopen(GameLib->Path, RTLD_LAZY | RTLD_GLOBAL);
if (GameLib->Handle)
{
  auto EntryPoint = (MPEGameLibMetadata(*)())dlsym(GameLib->Handle, MPE_GAME_LIB_METADATA);
  if (EntryPoint)
  {
    // Populate GameLib metadata
  }
}

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:

struct MPEGameLibAPI
{
  MPELibState* (*Initialize)(MPEEngine* Engine, MPEGameState* GameState);
  void (*Shutdown)(MPEEngine* Engine, MPEGameState* GameState, MPELibState* State);
  void (*Reload)(MPEEngine* Engine, MPEGameState* GameState, MPELibState* State);
  void (*Unload)(MPEEngine* Engine, MPEGameState* GameState, MPELibState* State);
  void (*Step)(MPEEngine* Engine, MPEGameState* GameState, MPELibState* State);
};

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 GameState.

The Game Loop

Everything comes together in the main game loop as you can see in the following excerpt:

while(Engine->GameRunning)
{
  // Some stuff before
  for (MPEGameLib* GameLib = Engine->FirstGameLib;
        GameLib;
        GameLib = GameLib->Next)
  {
    if (GameLib->IsStale)
    {
      MPE_Reload(GameLib);
    }
    GameLib->API.Step(Engine, GameState, GameLib->State);
  }
  // Some stuff after
}

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.

Hot swappability

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.