Code Reload: Game Code as a Module

← Overview | Projects | Widgets | Status & Roadmap | Home

Status: Designed, not implemented. Stage 1 is part of the foundation of Projects: the editor loading a project's module is the same mechanism as loading it again. One question is still open, in DecisionsDecisions.


The Problem

A game's code is linked into its executable with the engine (/WHOLEARCHIVE, --whole-archive), and registers what it declares when the program starts: components and entities (TNX_REGISTER_COMPONENT, TNX_REGISTER_ENTITY), Constructs, game states and modes, commands, view models, and widget classes (TNX_REGISTER_WIDGET). The editor is that same program. A change to any of it, even one method of a widget class, means closing the editor, building, and opening it again, losing the open documents, the play session, and the designer's canvas.

The Idea

In development builds the game's code is a module the editor loads, and loads again after a build. A packaged game links it statically, as today, and pays nothing for it. A reload happens at one safe point, puts back what the code registered and what it had made, and leaves everything the engine owns (worlds, assets, documents, undo history) where it was.


Shape

  • Two modules in development. The engine is a shared library (TNX_ENGINE_SHARED, development builds only); the game is a module that links it; the editor or game executable is a small host that loads both. Engine and game are built together, in one CMake tree and one IDE, and changes to either are made in the same session. The game module is copied to a name of its own before it is loaded, so the next build can overwrite the original while it runs.
  • Every registration is the module's. Registries take entries with the module that made them, and a module's entries go when it unloads. Function pointers into its code (a widget class's Inputs, Call, Make; a Construct's ticks; a command's handler) and text in its read-only data (TNX_WIDGET names, entity type names) are only valid while it is loaded. TnxName already owns its text; any registry keeping a const char* or a std::string_view from a module copies it.
  • One safe point. A reload runs on Sentinel at the spawn handshake, with the Brain stopped at the top of its frame and the Encoder between frames: the same window every other change to engine state uses. UI documents build again afterwards, so nothing built from the old code is read after it unloads.
  • Unload, in order: UI first (HUD widgets, designer instances, documents built from widget classes), then Constructs (destroyed as a world going away destroys them), then registrations. Then the module unloads.
  • Load, in order: the new module registers; component layouts are compared with the old ones; Constructs are made again from their views' ECS data (they keep nothing of their own that isn't in it, by design); UI is made again (below).

What Survives

KindAcross a reload
ECS dataKept. A component whose layout changed is copied field by field by name through reflection; a new field takes its default, a removed one is dropped and listed.
ConstructsMade again from their views' entities. Their C++ members are not kept, as for a level load.
Assets, documents, undoUntouched: they are engine data. Widget assets are checked again against the new classes.
Widget classesHUDs ask their game to set them again (TnxHud::OnCodeReloaded, as OnNewWorld for a new world). The designer remakes its instance from its preview contexts.
Play sessionStage 1 ends it and starts it again from its scene. Stage 2 keeps it.
Networked sessionNever reloaded during one: a reload is a new timeline, and the other machines did not take it.

Stages

  1. Foundation: reload in edit mode. The engine as a shared library in development builds, module-owned registries, the safe point, unload and load in order, layout migration by field name. The editor's toolbar has a Build and Reload command and notices a module built elsewhere. A play session is stopped first. A Testbed test loads a small test module, registers a widget class from it, unloads it, and loads a changed one.
  2. Production: reload while playing. The world is kept across a reload during play: its slabs stay, Constructs are made again in place, the rollback history is dropped (a new timeline). Problems in the new code (a failed build, a changed layout that can't be copied) leave the old module loaded and say why.
  3. Differentiators: patching in place. A change that only edits function bodies patches the running code without unloading anything, so nothing is made again; a change that adds or removes members falls back to a reload. The determinism boundary is kept: a patch never runs half a step.

What Starts a Reload

Either, depending on the setup (Projects): the editor notices a module built by the IDE and loads it, or the editor's Build and Reload runs the build itself, with its output in the Log panel.

Decisions

  1. While playing, in stage 1: stop and restart the session from its scene (the proposal), or refuse to reload until play stops.