Trail into Darkness (Nox)
A party-based exploration RPG demo built in Unity 6 (6000.3.7f1) with URP. The player travels a world map, manages supplies and time, and resolves randomized encounters. The codebase is the focus here — the gameplay is a vertical slice meant to exercise the architecture.
This is a portfolio demo. It showcases a hand-rolled application framework, clean separation of concerns, and a set of reusable in-house packages — not a finished game.
At a Glance
- Engine: Unity 6, Universal Render Pipeline
- Input: new Input System
- Asset loading: Addressables (everything content-facing is loaded by address/reference)
- Config: data-driven via
ScriptableObjectsettings assets - Language: C# 9+ (nullable enabled in core gameplay)
- Localization, Test Framework, Memory Profiler wired in
Goals
The architecture is built around a few deliberate goals, aimed at production workflows for small-to-medium teams:
- Scene-independent. Logic does not depend on a particular scene being set up by hand. Scenes are data; the framework boots and wires itself regardless of where you press Play.
- Core abstracted from gameplay. Gameplay coders work inside relatively isolated, modular "feature cocoons" against clean interfaces, without needing to understand boot, platform, or state-management internals.
- Built to extend. Adding or modifying features — new play modes, systems, or DLC-style content — should be additive and low-risk, not a rewrite.
- Testable by design. POCO (Plain Old CLR Object — a plain C# class with no
MonoBehaviour/engine dependency) logic and interface boundaries keep systems coverable by both editor tests and play-mode tests, decoupled from the engine. - Team-friendly. Code lives in C# and small assets rather than large monolithic scenes/prefabs, reducing the asset-locking and merge conflicts that stall multi-person teams.
- Swappable systems. Concrete implementations sit behind interfaces (save, platform, input, camera, transitions, …) so any one system can be replaced without touching its callers.
Architecture
The project runs on a custom state machine with a single Unity update driver. Almost all gameplay logic lives in plain C# classes (POCOs) that are ticked manually — MonoBehaviour is used only where Unity forces it.
Boot flow
Boot (RuntimeInitializeOnLoadMethod)
├─ platform initialization (early, pre-Initializer)
└─ instantiates "Initializer" Addressable
└─ EntryPoint (MonoBehaviour)
├─ loads InitializerSettingsFile + BootstrapReferences
├─ selects + initializes IPlatform (Desktop / UnityEditor)
├─ builds GameDataState + the IGameState lookup
└─ spawns GameStateRunner ── drives everything
Platform initialization runs in both
BootandEntryPoint.Bootestablishes the platform context early so that platform-specific data is available before the Initializer loads, andEntryPointthen selects and fully initializes the concreteIPlatformfor the rest of the runtime. This two-step setup guarantees the correct platform-specific data is provided at every stage of boot.
Layers
Boot/EntryPoint— entry point and dependency wiring. Loads settings, picks the platform, constructs the game states, hands off to the runner.GameStateRunner— the only class that uses Unity'sUpdate/FixedUpdate/LateUpdate. Ticks the active state and owns screen-fade transitions between states.- Game States (
IGameState) — top-level application modes:BootState(splash),MainMenu,GameMode. Each implementsEnterGameState / Tick / LateTick / ExitGameState / Dispose. - Play Modes (
IPlayMode) — gameplay sub-modes living insideGameMode:Adventure,Town,Rest,Combat,PauseMenu.GameModeGameStatehandles switching, caching, and pause suspend/resume.
How the state machines run
There are two nested state machines, and at any moment exactly one branch of each is live:
GameStateRunnerkeeps a single activeIGameStateand ticks only that one. The others simply exist in a lookup and consume nothing.- Inside
GameMode,GameModeGameStatekeeps a single activeIPlayModeand ticks only that one. Inactive modes are kept in a cache (or suspended), dormant, until switched back in.
This keeps each part isolated and self-contained — a state or play mode owns its own setup, update, and teardown, and never reaches into another's lifecycle. It also means no resources are spent on ticks that don't matter:
- Nothing ticks until it reports ready (
IsGameStateInitialized/IsGameModeInitialized); half-loaded systems stay idle instead of running against missing data. - Ticks are frozen entirely during fade transitions, so nothing runs mid-swap.
- Pausing suspends the underlying play mode rather than tearing it down — it stops ticking but keeps its state, so resume is instant and allocation-free.
- Switching play modes reuses the cached instance when possible, avoiding reload/re-alloc churn.
The result is one predictable update per frame: one state, one play mode, with everything else parked.
Design principles
- One update loop. All logic is driven from
GameStateRunnervia manual ticks — predictable order, no scatteredUpdate()methods. - Constructor injection. States and play modes receive their dependencies explicitly; no service locators or singletons in gameplay.
- Data-driven. Behavior is configured through
ScriptableObjectsettings (*Settings,*References) loaded as Addressables, not hardcoded. - Interface-first.
IGameState,IPlayMode,IPlatform,ICameraController,ISceneTransition, save-system interfaces — concrete types are swappable. - Platform abstraction.
IPlatform+PlatformSelectorisolate input/platform differences (Desktop vs. Editor). - Allocation-aware. Uses
ZLinqfor allocation-free LINQ on hot paths.
UI architecture
The UI follows the same philosophy as the rest of the codebase: logic lives in plain C#, MonoBehaviour is only a thin bridge to the scene. Each screen is split into two pieces:
*References(MonoBehaviour) — a dumb scene-authoring component that just holds serialized links to the actual widgets (buttons, images, TMP texts, containers). No logic. Examples:GuiReferences,PauseMenuReferences,MainMenuReference.*View(POCO, implementsIMenuView) — the view logic. It receives its references and the data it renders via constructor injection, and exposesInitialize / Show / Hide / Tick. Examples:AdventureView,PauseMenuView,PartyGuiView.
Key points:
- Manually ticked. Views are driven by their owning play mode (e.g.
AdventurePlayMode.Tick()callsadventureView.Tick()) — consistent with the single-update-loop model, no hiddenUpdate()methods. - One-way coupling. UI never reaches into game logic directly; it requests changes through
GameDataState(e.g. a button callsChangePlayMode(PlayMode.PauseMenu)). State drives UI, UI signals intent. - Lazy, Addressable-loaded. UI prefabs are loaded by address and instantiated on demand (
PauseMenuViewpullsPauseMenuPrefabs); references already present in the scene are reused viaFindFirstObjectByType. - Allocation-aware updates. Views update only what changed —
AdventureViewrewrites text only when day/time changes;PartyGuiViewuses a fixed-size slot pool (activate/deactivate, never instantiate per frame) and rebuilds only when party composition changes. - Data-driven popups. Rich tooltips/popups are composed through a builder (
PopupContentBuilder) backed by theJovian.PopupSystempackage, so content is declared, not hand-wired per widget. - Built on uGUI + TextMeshPro.
In-house packages (Packages/com.jovian.*)
Reusable systems are split into local packages, each with Runtime/Editor (and some Tests) assembly definitions:
| Package | Purpose |
|---|---|
save-system |
Save/load with JSON or obfuscated binary, slots, file-system storage |
encounter-system |
Encounter tables, registries, quest progress/log |
zone-system |
Spatial zones used to trigger encounters during travel |
calendar |
World clock, day phases, in-game time |
popup-system |
Categorized UI popups |
ingame-logging / logger |
In-game event log + general logging |
tag-system |
Lightweight tagging |
inspector-tools / assets-history / unitypackagesync |
Editor tooling |
utilities |
Shared helpers |
Third-party: ZLinq (zero-alloc LINQ), ayellowpaper.serialized-dictionary.
Project layout
Assets/
Code/
Core/ Boot, EntryPoint, GameStateRunner, IGameState, settings
GameState/
Camera/ camera controller + settings
Entities/ characters, parties, stats, perks, modifiers (factories + registries)
PlayModes/ Adventure, Town, Rest, Combat, Pause + encounters, map, time
UI/ views and GUI references
Input/ IInput, desktop/editor input + settings
Platform/ IPlatform, desktop/editor platforms + settings
SplashMainMenuUI/ splash + main menu views
Util/ BootMode (editor), SceneReference editor
Scenes/ Startup, MainMenu, Adventure, Combat
Art/ map, UI, animations
Packages/ in-house com.jovian.* packages + third-party
Running the project
- Open with Unity 6000.3.7f1.
- Press Play — boot behavior depends on the boot mode (below). Default, play from any scene.
Boot modes — start from any scene
The project can launch from any scene. This is controlled by the Nox/Boot editor menu, which sets a BootType:
- Full Boot — loads the
Startupscene first, then the normal boot chain. Closest to a real build. - Scene Boot — instantiates the
Initializerdirectly into the current scene, so you can press Play onAdventure,MainMenu, etc. and boot in place. - Unity Default — no boot injection; plays the scene as-is (raw Unity behavior).
In a player build, the full boot chain always runs.
Scene authoring (SceneReference)
Each standalone-playable scene carries a SceneReference component declaring its entry point:
gameState— whichIGameStatethe scene belongs to (e.g.GameMode)playMode— whichIPlayModeto start in (e.g.Adventure)
EntryPoint reads this on boot to start in the right state. If a scene has no SceneReference, it defaults to BootState.
Notes for reviewers
- Start with
EntryPoint.cs→GameStateRunner.cs→GameModeGameState.csto follow the control flow top-down. AdventurePlayMode.csis the most representative gameplay file — it shows how a play mode composes handlers (movement, time, encounters, inventory, UI) and integrates the in-house packages.- Save/load round-trips through
NoxSaveData+Jovian.SaveSystem.
Status
This is a vertical slice. The application framework, boot/state flow, save system, and adventure loop are in place, but:
- The combat system is not implemented —
Combatexists as a play mode hook only. - Design data is still minimal — zones, encounters, encounter tables, and related content are placeholders rather than authored, balanced data.
The intent is to demonstrate the architecture and systems, with content authoring and combat as the next steps.