12 KiB
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.
License
Copyright © 2026 Sebastian Bularca. All Rights Reserved.
This repository is proprietary. It is shared for evaluation purposes only (e.g. portfolio or hiring review): you may view and clone it, but you may not reproduce, modify, distribute, or reuse any part of it. See LICENSE for full terms.