unity-roject-test/workspace/outline/tech/index.page

229 lines
16 KiB
Plaintext
Raw Permalink Normal View History

2026-09-25 20:32:02 +00:00
<!DOCTYPE html>
<html lang="en"><head>
<meta charset="UTF-8">
<title>New Page</title>
<style>
@import url('https://styles.rokojori.com/get-font?family=Barlow&weights=100,400,700,900');
</style>
</head>
<body>
<page--header></page--header>
<page--root>
<full--width--block data--page--block="">
<full--width--area data--page--area=""><h1>Tech Outline</h1></full--width--area>
</full--width--block>
<full--width--block>
<full--width--area><br></full--width--area>
</full--width--block><two--columns--block>
<two--columns--area><h2>Project Organization</h2>Code goes into Assets/Scripts.<div><br></div><div>The main scene is found at:&nbsp;<span style="background-color: var(--block--background--color); color: var( --color ); font-family: var( --font--family ); font-size: var( --font--scale ); letter-spacing: calc(-0.3px);">Assets/Main</span></div><div><br></div><div>Unity Settings are in:&nbsp;Assets/Settings<br><div><br></div></div><div>UI will land in: Assets/UI</div></two--columns--area>
<two--columns--area><h2>Scene Organization</h2>There will be one main scene that manages and contains all singletons as well as UI, camera, player and other globally available scripts.<div><br></div><div>Only the levels would be loaded additionally in the scene.&nbsp;</div><div><br></div><div>In the demo there's no need to load different levels.</div></two--columns--area>
</two--columns--block><two--columns--block>
<two--columns--area><h2>Tech Areas</h2>The prototype should contain the following sections:<div><br></div><div><br></div><div>- GameState Management</div><div>- Dialog Management</div><div>- Simulation Management</div><div><br></div><div><br></div><div><h3>Writing Managers/Singletons</h3><div>Managers exist in the main scene as MonoBehaviours and are accessed via Unique (Assets/Scripts/Rokojori/Unique.cs)&nbsp; when no reference exists.</div></div><div>One example is the LocaleManager:&nbsp;Assets/Scripts/Rokojori/Locales/LocaleManager.cs</div><div>It's not ready yet, but it will later have a function to set the Language Code and iterate all elements that need to be updated.</div><div><br></div><div><br></div><div><br></div><div><br></div></two--columns--area>
<two--columns--area><h2>Writing Code</h2>There are two types of code that can be written and the differ in the way of the usage.<div><br></div><div><h3>Library Code</h3></div><div>Code that is generic goes here. It can't reference any project code. It only is allowed to reference Unity core libraries. it can be found here:<br><marked-text>Assets/Scripts/Rokojori</marked-text></div><div><br></div><div><div><h3>Project Code</h3></div><div>Code that is related generic goes here, it can be found here:<br><marked-text>Assets/Scripts/Project</marked-text></div><div><br style="color: rgb(146, 156, 175); letter-spacing: -0.3px;"></div></div><div><br></div></two--columns--area>
</two--columns--block><two--columns--block>
<two--columns--area><h2>Game State Manager</h2><div>Shared state layer underneath both the Simulation Manager and the Dialog Manager - both read and write through it, nothing owns its own copy of game data.</div><div><br></div><div><marked-text>GameStateManager</marked-text> holds a list of <code>GameStateVariable</code>s and builds a lookup dictionary per type on ready.</div><div><br></div><div>Reading a value goes through typed getters (<code>GetValue( GameStateNumberVariable )</code>, <code>GetValue( GameStateBoolVariable )</code>), so callers never touch the raw dictionaries.</div><div><br></div><div>Changes are broadcast through <marked-text>EventSlots</marked-text> (<code>Assets/Scripts/Rokojori/Events/EventSlot.cs</code>), so phases and dialog conditions can react without polling. The full set of variables can be serialized to and deserialized from JSON, for save/load.</div></two--columns--area>
<two--columns--area><code--block data-language="rokojori-cs">
<code--area>public class GameStateManager : MonoBehaviour
{
public List&lt;GameStateVariable&gt; variables;
void OnReady()
{
// build a per-type value dictionary
}
public double GetValue( GameStateNumberVariable var ) { }
public bool GetValue( GameStateBoolVariable var ) { }
public string ToJson() { }
public void FromJson( string json ) { }
}
public abstract class GameStateVariable : ScriptableObject
{
public string name;
}
public class GameStateNumberVariable : GameStateVariable
{
public double initialValue;
public EventSlot&lt;double&gt; onChanged;
}
public class GameStateBoolVariable : GameStateVariable
{
public bool initialValue;
public EventSlot&lt;bool&gt; onChanged;
}
</code--area>
</code--block></two--columns--area>
</two--columns--block><full--width--block>
<full--width--area><h2>How It Fits Together</h2><div>Dialog runs through <code>DialogManager</code>, which any code can call directly. <code>DialogPhase</code> is just a thin wrapper so a dialog can also be one step in the Simulation Manager's turn loop.</div><code--block data-language="rokojori-cs">
<code--area> GameStateManager
(shared game state)
▲ ▲
│ │
SimulationManager │
│ │
Intro / PreTurn / PlayerTurn / PostTurn / Outro
│
┌────────────┼──────────────────┐
▼ ▼ ▼
ShowTurnStart SimulateWorld DialogPhase
Conditions (production, │
(dice, ticks) pollution, ...) DialogManager.StartDialog()
│ │ │
│ │ DialogProcessor
│ │ (cursor over DialogBlocks)
│ │ │
└────────────┴───────commands─────┘
▼
SimulationRenderer / UI
│
done ──► next phase
DialogManager.StartDialog() can also be called directly,
outside of any phase (menus, cutscenes, etc.)
</code--area>
</code--block></full--width--area>
</full--width--block><two--columns--block>
<two--columns--area><h2>Simulation Manager</h2><div><marked-text>GamePhase</marked-text> is the atomic unit: a ScriptableObject that runs asynchronously and can read/write <code>GameStateManager</code>. The manager awaits one phase before starting the next.</div><code--block data-language="rokojori-cs">
<code--area>public abstract class GamePhase : ScriptableObject
{
public abstract Task Execute(
GameState state,
SimulationRenderer renderer );
// set true from inside Execute to end the game early
public bool gameFinished;
}
</code--area>
</code--block></two--columns--area>
<two--columns--area><h3>Phase Lists &amp; the Turn Loop</h3><div>SimulationManager holds five ordered phase lists and runs them like this:</div><code--block data-language="rokojori-cs">
<code--area>public List&lt;GamePhase&gt; intro;
public List&lt;GamePhase&gt; preTurn;
public List&lt;GamePhase&gt; playerTurn;
public List&lt;GamePhase&gt; postTurn;
public List&lt;GamePhase&gt; outro;
async Task Run()
{
await RunPhases( intro ); // e.g. set player count
while ( ! anyPhaseFinishedGame )
{
await RunPhases( preTurn );
foreach ( var player in players )
await RunPhases( playerTurn );
await RunPhases( postTurn );
}
await RunPhases( outro );
}
</code--area>
</code--block><div><br></div><div>Any phase, in any list, can flag the game as finished. The manager checks this after each phase and jumps straight to <code>outro</code>, skipping whatever was left - it doesn't wait for the current list to finish.</div></two--columns--area>
</two--columns--block><two--columns--block>
<two--columns--area><h3>SimulationManager ↔ SimulationRenderer</h3><div>A phase doesn't touch Unity directly. It hands commands to the renderer and awaits completion before the manager moves on:</div><code--block data-language="rokojori-cs">
<code--area>GamePhase.Execute()
│
▼
renderer.Enqueue( command ) // animation, UI, VFX...
│
▼
SimulationRenderer executes it
│
▼
renderer reports done
│
▼
Execute() completes -&gt; next phase
</code--area>
</code--block></two--columns--area>
<two--columns--area><h3>Example Phases</h3><div><strong>ShowTurnStartConditions</strong> - start of a round: roll dice for events, apply resource ticks, set up the state a turn begins with.</div><div><br></div><div><strong>SimulateWorld</strong> - where systems like ProductionSystem/AllocationSystem/PollutionSystem run: each reads a slice of state and writes deltas back through GameStateManager, optionally querying a shared spatial field (e.g. a PollutionField) instead of linking entities directly, using a seeded random stream so results are reproducible.</div><div><br></div><div><strong>DialogPhase</strong> - a thin GamePhase that awaits <code>DialogManager.instance.StartDialog( start )</code> and completes. All the real logic lives in DialogManager/DialogProcessor below, so dialog works the same way whether or not it's running inside a phase.</div></two--columns--area>
</two--columns--block><two--columns--block>
<two--columns--area><h2>Dialog Manager</h2><div>The UI-facing singleton and the only entry point for running a dialog. Owns the actual UI wiring (text, portraits, choice buttons) and can be called directly from anywhere - not just from a DialogPhase.</div><code--block data-language="rokojori-cs">
<code--area>public class DialogManager : MonoBehaviour, IDialogDisplay
{
public static DialogManager instance
=> Unique&lt;DialogManager&gt;.instance;
public async Task StartDialog( DialogBlock start )
{
var processor = new DialogProcessor( this );
await processor.Run( start );
}
// called by DialogProcessor when a block needs to be shown
public async Task&lt;DialogAnswer&gt; Show( DialogBlock block )
{
// update UI (line/portrait, or choice buttons)
// and wait for the player, e.g. a button click
}
}
</code--area>
</code--block></two--columns--area>
<two--columns--area><h3>DialogProcessor</h3><div>The standalone engine. Owns the cursor, resolves branches/conditions against <code>GameStateManager</code> on its own, and only talks to the outside world through the <code>IDialogDisplay</code> it was given - it has no idea DialogManager exists.</div><code--block data-language="rokojori-cs">
<code--area>public class DialogProcessor
{
IDialogDisplay display; // e.g. DialogManager
public DialogProcessor( IDialogDisplay display )
{
this.display = display;
}
public async Task Run( DialogBlock start )
{
var current = start;
while ( current != null )
current = await current.Run( display );
}
}
</code--area>
</code--block></two--columns--area>
</two--columns--block><two--columns--block>
<two--columns--area><h3>Execution Model</h3><div>Running a block finishes it and returns the next one. By default that's just "the next block in sequence" - but a <code>PlayerChoice</code> or <code>DialogBranch</code> can redirect the cursor to any other block instead, based on the option picked or a <code>GameStateCondition</code>.</div><code--block data-language="rokojori-cs">
<code--area>DialogLine: "Want to buy stuff?"
PlayerChoice:
A) Attacker
B) Defenders
C) None
</code--area>
</code--block><div>Bigger dialogs work the same way, just with conditions picking the branch - e.g. checking a player's points and whether City A is still standing to decide whether the dialog continues with City A's boss or City B's.</div></two--columns--area>
<two--columns--area><h3>Dialog Blocks</h3><code--block data-language="rokojori-cs">
<code--area>DialogBlock
├── DialogLine // Locale, Speaker
├── PlayerChoice // ChoiceLine, Options[]
├── DialogBranch // Condition, Branch -> DialogBlock
├── DialogList // Blocks[], itself a DialogBlock
├── ReturnBlock // ends the dialog
└── CommentBlock // editor-only, no gameplay effect
</code--area>
</code--block><div><strong>DialogLine</strong> pairs a localized <code>Locale</code> with a <code>Speaker</code>; presentation (portrait, voice, animation) is handled by whatever implements <code>IDialogDisplay</code>.</div></two--columns--area>
</two--columns--block><two--columns--block>
<two--columns--area><h3>Conditions &amp; Actions</h3><div>Game-state reads/writes stay out of DialogBlock and live in two small hierarchies on top of GameStateManager:</div><code--block data-language="rokojori-cs">
<code--area>GameStateCondition
├── CompareInt
├── HasObject
└── ListCondition // All / AtLeastOne / None
GameStateAction
├── SetProperty
├── ModifyProperty
└── ActionList
</code--area>
</code--block><div>This keeps the dialogue graph data-driven - conversations can be built without writing code.</div></two--columns--area>
<two--columns--area><h3>Testability</h3><div>Because DialogProcessor only depends on the <code>IDialogDisplay</code> interface, DialogManager isn't the only thing that can drive it.</div><div><br></div><div>A debug/headless display can implement the same interface - auto-answering choices, logging the path taken - to run and check dialogs without the actual UI.</div></two--columns--area>
</two--columns--block><full--width--block>
<full--width--area><h2>Implementation Status</h2><div>Game State Management, Dialog Management, and Simulation Management now all have code under <code>Assets/Scripts/Rokojori/</code> (GameState/, Dialog/, Simulation/). This was written in one pass from this outline without a code review - see <a href="../../history/index.page">history/index.page</a> for the session entry.</div><div><br></div><div>Not built: <code>SimulateWorld</code> / <code>ShowTurnStartConditions</code> (only mentioned as examples, no code contract given) and any real UI for <code>DialogManager.Show</code> / <code>SimulationRenderer.Enqueue</code> - both currently placeholders (the former throws, the latter completes instantly), since <code>Assets/UI</code> is still empty.</div><div><br></div><h3>Assumptions Made Filling Gaps - Need Sign-off</h3><div>1. <strong>DialogList</strong> runs its <code>blocks</code> as a flat ordered sequence and ignores each child's own inherited <code>next</code> field while inside the list - the list's authored order is what drives flow. This means DialogBranch/PlayerChoice aren't meaningful as direct DialogList members under the current implementation (their branch targets get silently ignored). Alternative interpretations were possible; this was the simplest that kept DialogList useful as an authoring convenience for grouping content nodes.</div><div><br></div><div>2. <strong>PlayerChoice options</strong> gained an optional <code>action : GameStateAction</code> field, executed when that option is picked. The outline listed Conditions and Actions as a pair but never named which DialogBlock triggers an Action - this seemed like the natural, and only, gap to fill it.</div><div><br></div><div>3. <strong>HasObject</strong> is implemented against a <code>GameStateBoolVariable</code> flag, since there is no inventory/object system yet to check membership against.</div><div><br></div><div>4. <strong>SimulationManager.playerCount</strong> is a plain <code>int</code>, not a list of a "Player" type - no such type exists anywhere yet, and the outline's own turn-loop sample references <code>players</code>/<code>player</code> without ever defining what they are.</div><div><br></div><div>5. <code>GameStateVariable</code> uses a separate <code>key</code> field for save-data identity rather than the outline's literal <code>name</code> field, since <code>name</code> is already inherited from <code>UnityEngine.Object</code> and shadowing it would be confusing.</div></full--width--area>
</full--width--block></page--root>
<page--footer></page--footer>
</body></html>