229 lines
16 KiB
Plaintext
229 lines
16 KiB
Plaintext
<!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: <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: 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. </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) when no reference exists.</div></div><div>One example is the LocaleManager: 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<GameStateVariable> 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<double> onChanged;
|
|
}
|
|
|
|
public class GameStateBoolVariable : GameStateVariable
|
|
{
|
|
public bool initialValue;
|
|
public EventSlot<bool> 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 & 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<GamePhase> intro;
|
|
public List<GamePhase> preTurn;
|
|
public List<GamePhase> playerTurn;
|
|
public List<GamePhase> postTurn;
|
|
public List<GamePhase> 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 -> 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<DialogManager>.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<DialogAnswer> 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 & 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>
|