Sebastian Bularca ca1e3e8488 A bit of cleaning
2026-06-18 23:59:35 +02:00
2026-06-18 23:59:35 +02:00
2026-05-19 15:52:04 +02:00
2026-05-19 15:52:04 +02:00
2026-06-18 11:50:25 +02:00
2026-06-18 11:50:25 +02:00

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.csGameStateRunner.csGameModeGameState.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 implementedCombat 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 for full terms.

Description
No description provided
Readme 46 MiB
Languages
C# 90.8%
ShaderLab 4.2%
HTML 1.7%
GLSL 1.3%
HLSL 1.1%
Other 0.9%