The slowest part of making a game usually isn't the compiler — it's you, walking the character back to the tricky jump for the fortieth time to test a tweak. Hot-reloading kills that loop. Change a number, save, and the running game picks up your new code without dropping the player, the level, or the debugger you carefully positioned. Once you've built this, going back feels like editing with the lights off.
The core idea: split host from game
Hot-reload works when your program is two pieces. A thin host (the executable) owns the window, the input, the memory, and the main loop. The game — all the logic you actually iterate on — lives in a shared library the host loads at runtime. When the library changes on disk, the host unloads the old one and loads the new one. The host never restarts, so nothing it owns is lost.
The pivot that makes this safe: the game code must own no persistent state itself. All state lives in a plain struct the host allocates and hands to the game every frame.
// Host allocates this once and keeps it alive across reloads.
typedef struct { float player_x, player_y; int score; } GameState;
// The one function the host calls each frame.
typedef void (*GameUpdateFn)(GameState *, float dt);
Loading and reloading the library
On POSIX you use dlopen/dlsym/dlclose; on Windows it's LoadLibrary/GetProcAddress/FreeLibrary. The pattern is identical: open the library, look up game_update, call it every frame. To reload, close the handle and open the fresh file.
typedef struct {
void *handle;
GameUpdateFn update;
long mtime; // last-modified time we loaded
} GameLib;
bool game_load(GameLib *lib, const char *path) {
lib->handle = dlopen(path, RTLD_NOW);
if (!lib->handle) return false;
lib->update = (GameUpdateFn)dlsym(lib->handle, "game_update");
return lib->update != NULL;
}
dlopen that copy.Watch the file, reload on change
Detecting a rebuild can be as simple as polling the library's modification time once per frame. If it's newer than what you loaded, swap. You can graduate to inotify or ReadDirectoryChangesW later, but a stat call per frame is invisible in the profile and gets you 95% of the value on day one.
void maybe_reload(GameLib *lib, const char *path) {
long now = file_mtime(path);
if (now == lib->mtime) return; // nothing changed
if (lib->handle) dlclose(lib->handle);
if (game_load(lib, path)) {
lib->mtime = now;
log_info("reloaded game module");
}
// GameState is untouched — the player never noticed.
}
Why state must live in the host
When you dlclose the old library, any memory it allocated internally and any globals it declared vanish. If your player position lived inside the game module, the reload would teleport the player to the origin. Because the host owns GameState and passes a pointer in, the new code reads exactly the values the old code left behind. That's the whole trick: the code is disposable, the data is not.
One caveat worth internalizing early: if you change the layout of GameState — add a field, reorder members — the old memory no longer matches the new struct. During heavy iteration you keep the struct stable; when it has to change, you restart once. In practice you'll change logic a hundred times for every struct change, so the payoff is enormous.
Where this leads
Hot-reload pairs beautifully with the reloadable-asset system in the tooling track — reload a texture and the game code together and you're tuning a level in real time. It also exposes bugs early: code that assumes it runs from a clean slate breaks the moment it's reloaded mid-game, which is exactly the kind of hidden assumption you want to find on your machine, not a player's.
