Joël van der Werf
All posts

Rules, views and a game session: structuring game code

How the diorama RPG keeps its battle maths testable without a GPU, its UI dumb, and its run state alive across scene changes.

Engine code has its rules; game code needs its own. Once Eonlore had battles, quests, a party and menus, the scripts started to tangle: battle maths inside UI handlers, state lost on scene changes, and nothing testable without starting the game.

The structure I settled on has four pieces. It will look familiar to anyone who has built Unity games with MVP or MVC, and that's on purpose.

1. A game session for run state

The engine creates one game session when a run starts (Play in the editor, or the player's start) and destroys it when the run ends. It survives every scene change in between.

class RpgSession : public GameSession
{
public:
    static RpgSession& Get() { return GameSession::Get<RpgSession>(); }
    PartyState Party;    // who has joined; the first ones fight
    BattleState Battle;  // the hand-off between field and battle
    QuestLog Quests;
    Inventory Items;
};

Stop and Play start a fresh run with no reset code anywhere. What must cross scenes (who started a battle, who won, a quest's stage) is state in the session. A scene left on the scene stack is suspended, not destroyed, and reads the state when it resumes.

2. Rules are static functions over plain values

BattleRules::ResolveHit, LootRules::Roll, PartyRules::Equip: they take what they need as arguments, including a random function, and return what happened. They never touch entities or the scene, so a headless test runs them without a session, a scene or a GPU. The script decides what to show; the rule decides what happened.

3. Views are plain classes a script owns

A view finds its UI entities once, turns UI events into requests ("use this item", "close"), and writes the UI from state. It doesn't decide anything. The field menu owns five page views this way. A view only becomes a script of its own when it needs its own update, like a dialogue box typing its lines in.

4. Content is data

Heroes, enemies, abilities, items, loot and quests are reflected data types in .hydef files (one record) and .hytable files (rows by ID), edited in the inspector. Adding a hero is a table row and one PartyState::Get().Join("Id") where the story says so. Nothing else changes.

Saving falls out of it

Because all run state lives in one session, a save game is just that session written to YAML plus where the player stands. Loading reads it back and asks the scene system to replace the current scene. New state that should survive a save gets one line in Write and one in Load.

Takeaways

  • Put run state in one owned place, and scene changes and Play mode stop being special.
  • Keep the rules pure, and you can test the interesting part of a game without running it.
  • Keep views dumb. UI changes far more often than rules do.
Older: Game content as data: Hydro's ScriptableObjectsNewer: Making an engine readable to AI agents

Building a game, an engine, or the tools around them?

I'm based in Tokyo and open to conversations about Unity, C++ and engine roles, C# and .NET software roles, and teams that want to put AI agents to work on real codebases.