Basic hot-reload architecture of my side project game

Nov 25 2016

In order to release some frustration on my current programming in Nubarron, I started creating a game in C++, and I wanted to share some of the infrastructures I’m creating just in case you may find it useful/interesting.

Hot-reload

One of the things I wanted to have for sure that annoys me a lot is fast iteration when implementing algorithms or testing gameplay, Unity used to be good enough at this but, at some point, it stopped working or taking too long and breaking stuff. So I figured out that I wanted hot reload to be a first class citizen in my engine. C++ is not great for this I must admit, if you use classes it’s a pain in the ass to hot reload, especially if you keep your data inside the classes. So I figured out that for this game I wanted to try something different: Splitting the data model from the functionality. You’ll see why this is important given the architecture I ended up with.

Binary + Dynamic Library

To hot reload code I had to rely on using two separate code bases, a binary executable for the static code (code that doesn’t get hot-reloaded) and a dynamic library (DLL in windows, so in Linux). Right now I’m working on Linux, so the code I’ll show only applies to Unix environments (Linux and OSX use the same code).

The initial idea is simple, you initialize your application, open the dynamic library, bind the methods that you need from it, and when it changes (because you recompiled it), you unload it, reload it, and bind the methods again.

This is only easy and simple if the glue between your library and the binary is just a couple of methods bound with something like dlsym.

Because of this, I decided to limit myself to a few bridges between the static binary (that has the game loop) and the dynamic library. I actually have only 4 methods: updategame, updateeditor, rendergame, rendereditor. These could be very much collapsed into three, but I wanted to keep my editor memory separated from the game itself, so I added specific methods for the editor.

typedef void (*game_function)(Permanent::Memory*,Temporary::Memory*,Debug::Memory*);
typedef void (*editor_function)(Permanent::Memory*,Temporary::Memory*,Editor::Memory*,
                                Debug::Memory*);

game_function update_game;
game_function render_game;

editor_function update_editor;
editor_function render_editor;

You may wonder what all those ::Memory* things are, well, they’re basically pointers to chunks of memory where I define all the data. I’ll talk about this in a future post, all you need to know for now is that all the data model is defined somewhere inside those structs (in fact each subsystem of the game defines its own data set and adds it to that memory pool). The important part to keep in mind is that all that data is not coupled with the code that uses it, it is defined in some .h that gets included in both the executable and the shared library. If you change the data model, you’ll have to restart the application, but if you only modify behavior you’re good to go.

Now for the actual meat.

Game loop

The main game loop does something like this:


int main(int argc, char** argv)
{
  [...]
  // This line among other stuff, loads the library
  platform::init();
  bind_dynamic_library_functions();

  // Game Loop
  while(platform::keep_running())
  {
    {
      platform::update(); // Among other stuff, reloads the library if is was updated

      if(platform::reload_dynamic_lib)
      {
        bind_dynamic_library_functions();
      }
      [...]
      update_game(permanent_memory, temporary_memory, debug_memory)
      update_editor(permanent_memory, temporary_memory, editor_memory, debug_memory)
      [...]
    }

    {
      [...]
      render_game(permanent_memory, temporary_memory, debug_memory)
      render_editor(permanent_memory, temporary_memory, editor_memory, debug_memory)
      [...]
    }
  }
  [...]

Nothing too complicated, just load the library, bind the functions. If the library is modified, it gets reloaded and the main loop re-binds the methods.

bind_dynamic_library_functions is pretty straightforward, it calls the right bind function on the platform layer:


void bind_dynamic_library_functions()
{
  update_game = platform::bind_game_function("update_game");
  render_game = platform::bind_game_function("render_game");
  update_editor = platform::bind_editor_function("update_editor");
  render_editor = platform::bind_editor_function("render_editor");
}

and in the platform layer we do something like:


game_function bind_game_function(const char* name)
{
  game_function f = (game_function)dlsym(game_lib_handle, name);
  [...]
  // Some error checking
  [...]
  return f;
}

In this method we use game_lib_handle which is just a void* to the library in memory. We pass it to dlsym and get a handle to the specific function we need, then, we return it. I avoided the error checking code because it’s just noise. bind_editor_function it’s basically the same thing, but it returns an editor_function.

Platform Layer

In the platform layer, we have all the glue code between our game and the operating system. As I said I only implemented this in Linux (and OSX), so this will vary in a Windows environment.

The way the reload works is pretty simple, we have a build directory where the dynamic library and a “status” file are located. Whenever we build the library we also run touch build/reload-libs. Our platform layer will check if this file changed in each frame, if it changed it will reload the functions and set reload_dynamic_lib to true:


bool reload_dynamic_lib = false;
const char* game_lib_status = "build/reload-libs";
struct stat game_lib_current_stats;
struct stat game_lib_last_stats;

[...]

void update()
{
  stat(game_lib_status, &game_lib_current_stats);
  reload_dynamic_lib = game_lib_current_stats.st_mtime > game_lib_last_stats.st_mtime;

  if(reload_dynamic_lib)
  {
    printf("Reloading Libraries...\n");
    open_dynamic_library(true);
  }
}

To check if the file changed, we use stat, and then check the last value of st_mtime with the current one. If the current one is newer, we reload the library.

The open function is:


const char* game_lib_name = "build/game.so";

void open_dynamic_library(bool close_current)
{
  if(close_current)
  {
    dlclose(game_lib_handle);
  }

  stat(game_lib_status, &game_lib_last_stats);
  game_lib_handle = dlopen(game_lib_name, RTLD_NOW);

  [...] // Error handling
}

It just closes the opened library, updates the status using stat and then loads the handle.

So, this is it for the hot-reload stuff, given the decision of splitting memory from logic, it turned out that the implementation of hot reload is super simple. On top of this, I added a guard setup to automatically re-build the library every time I save a file. I also did the same for the static code. This is not strictly necessary, you can build them by hand or from your editor… in fact, I’m still not sure I want to reload the whole binary whenever I save a file, so I’ll see if I change this behavior later.

I’m pretty happy with how this turned out, the overall architecture is simple and clean, we’ll see how easy it will grow from now on…

As always, I’d love to hear your thoughts about this, be sure to ping me on twitter to discuss about these topics!

comments powered by Disqus