Madrigal Games
This blog post as well as all code listed in it are 100% written by me, a human, without any help from LLMs or other AI tools.
Code “hot reloading” means updating the code of a application while it is running. It is very useful during development when you want to quickly iterate on a feature without having to restart the game between every change, and it also gives you some serious superpowers when it comes to debugging, as we’ll see later on. The concept goes by many names, “Hot reloading”, “Live updating”, “Live reloading” etc, but they usually mean the same thing: The ability to change the logic of your app without having to shut it down.
Some environments/languages support hot reloading out of the box. Google’s Flutter UI framework can update the UI of your app as you are writing the code. Many scripting languages such as Lua, which are designed to be embedded in a host program, support hot reloading. Native languages, on the other hand, have historically not been great at it though it can be done with a little elbow grease as we’ll see in this blog post. I am going to explain how I added hot reloading support for the gameplay code in Traction Point, the game I am building as an indie developer. It isn’t necessarily the best way to do it, but it works well and I managed to do it in 1.5 weeks with only minimal rewrites of existing code.
Image 1. Traction Point's hybrid C++/Zig codebase
A hybrid C++/Zig codebase
As we saw in the previous blog post, Traction Point has a hybrid codebase where the game engine is written in C++ and the gameplay code (ie. the game itself) is written in Zig. Why, you ask? The TLDR is that I had a mature C++ game engine I had spent years building, but I fell in love with the simplicity of Zig and wanted to use it to make a game. So I added an interop layer and exposed most of the engine APIs to Zig.
Image 1 shows the language split on a high level. The engine starts up and loads a Zig DLL containing the game code. After the initial startup, the engine takes a backseat and lets the game code drive the application. “Basis” is the name of my custom game engine, while “Means” is the project name of the game before it got its official name of Traction Point, and you’ll find it all over the codebase. The previous blog post goes into more detail about the architecture.
Hot reloading in practice
Before we dive deeper into my particular case, let’s think about what hot reloading is and what we need to implement it using a natively complied language. On a fundamental level, hot reloading means ripping out part of the logic of your app and replacing it with a different version. Ideally, the state (ie. the in-memory data) of your app survives this process as-is and the app continues running seamlessly using the new logic. For this, we need to support at least the following:
Loading and unloading a part of the app logic. This depends on the target platform, but generally it means organizing your app into an executable + one or more shared libraries / DLLs. The DLLs are hot reloadable, while the executable typically isn’t since we cannot “unload” it without shutting it down. When a new version of a DLL is available, the executable unloads the old and loads the new.
Keeping the state of the app intact across the hot reload. There are (at least) two ways to handle this: serialization vs. keeping the state in memory. Serialization means writing the whole state of the program to a data block, and reading that data block back when the new code is loaded, almost like a save file in a game. Keeping the state in memory means just that, don’t throw away the memory when unloading the DLL. There are pros and cons to each one and we’ll talk more about them later.
Patching up pointers that changed as part of the reload. Depending on how you tackled the previous point, this can be simple or complicated. (De)serialization typically sets up pointers as a natural part of loading the data back in, while keeping the memory loaded might mean that you have to patch things up yourself later. On the other hand, if the memory stayed loaded during the process, there probably aren’t that many pointers needing fix up.
Let’s see how I solved each of these for Traction Point.
Loading and unloading the game library
The code architecture turned out to be an excellent fit for hot reloading. Because of the C++/Zig language split, I already had the project using a separate DLL file for the gameplay logic, which gave me a pretty sensibel goal of trying to make the gameplay code hot reloadable, while leaving the game engine code out.
It’s not quite as simple as calling LoadLibrary()<br>when the new code has been compiled though, as the operating system prevents you from writing to the DLL file while the game is running. The solution is to not...