Most games start with mechanics and layer content on top.
This project did the opposite.
From the beginning, the goal was not to build features, but to build systems that survive content growth, engine constraints, and tooling changes.
That decision led to a strict rule:
Unity delivers UI and rendering.
Game logic and data live outside the engine.

Unity Is Not the Core
Unity is used for:
- UI rendering (UI Toolkit)
- input handling
- presentation
- lifecycle glue
Unity is not:
- the source of truth for game logic
- where narrative rules live
- where content behavior is defined
All domain logic — conditions, actions, dialogue flow, state evaluation — is implemented as plain C#, independent of Unity APIs.
This separation is intentional.
Data + Pure C# as the Authority
At the core of the project:
- data defines what exists
- pure C# systems define how it behaves
- Unity asks questions and renders answers
The game does not ask:
“Which MonoBehaviour should run?”
It asks:
“What does the data allow right now?”
What This Enables
1. Engine-Agnostic Logic
Because logic does not depend on Unity:
- switching engines is possible, not theoretical
- UI can be rewritten without rewriting systems
- simulation code survives engine churn
Unity is treated as a frontend, not a foundation.
2. External Toolchains
Because data + logic are decoupled:
- validators can run outside Unity
- content editors can be built without the engine
- CI can fail on broken content before runtime
Examples:
- YAML schema validation in VS Code
- headless content loaders
- test runners without Unity Test Framework
3. Deterministic, Testable Systems
Pure C# systems mean:
- no scene setup for tests
- no hidden state in GameObjects
- no editor-only assumptions
Example: condition evaluation can be tested like any other library.
4. Long-Term Content Scaling
As content grows:
- systems stay fixed
- schemas evolve carefully
- data volume increases without logic explosion
This avoids the classic:
“We need to refactor everything because content doubled.”
5. Inspectability and Debuggability
If something breaks:
- inspect the data
- inspect the state
- re-run the evaluation
No guessing which script ran in which order.
A Small Concrete Example
Condition Evaluation (Pure C#)
public static bool Evaluate(ConditionBlock when, GameState state)
{
if (when == null)
return true;
if (when.All != null && when.All.Any(c => !c.Evaluate(state)))
return false;
if (when.Any != null && !when.Any.Any(c => c.Evaluate(state)))
return false;
return true;
}
No Unity types. No engine dependencies. Fully testable.
Data-Driven Action Definition (YAML)
- !Action
id: pay-debt # pseudo-unigue identifier for the action
label: Pay the bar tab # Label to show for the UI
when: # Condition block
all: # Evaluation clause
- hasTag: owes_bar
- hasCredits: 50
effects: # Effect of the action to the state
- removeTag: owes_bar
- changeCredits: -50
The UI does not know why this action appears. It only knows whether it does.
foreach (var action in visibleActions)
{
var button = new Button(() => Execute(action));
button.text = action.Label;
actionContainer.Add(button);
}
Unity renders - Systems decide.
This Also Enables Engine Switching
Engine changes are expensive — unless logic is isolated.
In this architecture, the engine is not the game.
It is a delivery mechanism.
All authoritative behavior lives outside the engine:
- game state
- condition evaluation
- action resolution
- dialogue flow
- narrative rules
Because of this separation:
- data remains unchanged
- core systems remain unchanged
- only the presentation layer needs to be rewritten
This does not make engine switching cheap.
It makes it possible without rewriting the game itself.
That distinction matters.
An engine swap becomes a controlled migration instead of a total rewrite:
- Unity → another engine
- desktop → mobile
- real-time → turn-based presentation
The game logic does not care.
The cost moves from “rewrite everything” to “replace the shell”.
For a long-lived project, that alone justifies the architecture.