Adedd proper readme text

This commit is contained in:
Sebastian Bularca
2026-06-18 11:45:43 +02:00
parent fb3cec2e04
commit 346edc0432

188
README.md
View File

@@ -1,3 +1,187 @@
# trail-into-darkness # Trail into Darkness (Nox)
The repo for the game prototype A Trail Into Darkness 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.