196 lines
12 KiB
Markdown
196 lines
12 KiB
Markdown
# 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 `ScriptableObject` settings 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 `Boot` and `EntryPoint`.** `Boot` establishes the platform context early so that platform-specific data is available before the Initializer loads, and `EntryPoint` then selects and fully initializes the concrete `IPlatform` for 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's `Update`/`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 implements `EnterGameState / Tick / LateTick / ExitGameState / Dispose`.
|
|
- **Play Modes (`IPlayMode`)** — gameplay sub-modes living inside `GameMode`: `Adventure`, `Town`, `Rest`, `Combat`, `PauseMenu`. `GameModeGameState` handles 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**:
|
|
|
|
- `GameStateRunner` keeps a single *active* `IGameState` and ticks only that one. The others simply exist in a lookup and consume nothing.
|
|
- Inside `GameMode`, `GameModeGameState` keeps a single *active* `IPlayMode` and 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 `GameStateRunner` via manual ticks — predictable order, no scattered `Update()` methods.
|
|
- **Constructor injection.** States and play modes receive their dependencies explicitly; no service locators or singletons in gameplay.
|
|
- **Data-driven.** Behavior is configured through `ScriptableObject` settings (`*Settings`, `*References`) loaded as Addressables, not hardcoded.
|
|
- **Interface-first.** `IGameState`, `IPlayMode`, `IPlatform`, `ICameraController`, `ISceneTransition`, save-system interfaces — concrete types are swappable.
|
|
- **Platform abstraction.** `IPlatform` + `PlatformSelector` isolate input/platform differences (Desktop vs. Editor).
|
|
- **Allocation-aware.** Uses `ZLinq` for 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, implements `IMenuView`)** — the view logic. It receives its references and the data it renders via constructor injection, and exposes `Initialize / Show / Hide / Tick`. Examples: `AdventureView`, `PauseMenuView`, `PartyGuiView`.
|
|
|
|
Key points:
|
|
|
|
- **Manually ticked.** Views are driven by their owning play mode (e.g. `AdventurePlayMode.Tick()` calls `adventureView.Tick()`) — consistent with the single-update-loop model, no hidden `Update()` methods.
|
|
- **One-way coupling.** UI never reaches into game logic directly; it requests changes through `GameDataState` (e.g. a button calls `ChangePlayMode(PlayMode.PauseMenu)`). State drives UI, UI signals intent.
|
|
- **Lazy, Addressable-loaded.** UI prefabs are loaded by address and instantiated on demand (`PauseMenuView` pulls `PauseMenuPrefabs`); references already present in the scene are reused via `FindFirstObjectByType`.
|
|
- **Allocation-aware updates.** Views update only what changed — `AdventureView` rewrites text only when day/time changes; `PartyGuiView` uses 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 the `Jovian.PopupSystem` package, 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
|
|
|
|
1. Open with **Unity 6000.3.7f1**.
|
|
2. 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 `Startup` scene first, then the normal boot chain. Closest to a real build.
|
|
- **Scene Boot** — instantiates the `Initializer` directly into the *current* scene, so you can press Play on `Adventure`, `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` — which `IGameState` the scene belongs to (e.g. `GameMode`)
|
|
- `playMode` — which `IPlayMode` to 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.cs` to follow the control flow top-down.
|
|
- `AdventurePlayMode.cs` is 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** — `Combat` exists 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](LICENSE) for full terms.
|