Core Game Loop Additions
This commit is contained in:
@@ -19,7 +19,7 @@ permalink: gamevault/06-roadmap/milestones
|
||||
| **M5.5 — Game feel & identity** | Bridge "tech-demo → game": the **Husk** enemy (server AI, interpolated ghost), player death/respawn, combat juice (damage numbers/VFX/SFX/camera shake), a core HUD, and a sci-fi look pass — under the new fiction ([[Identity]], sci-fi frontier colony) | ✅ Done 2026-06-02 — runtime-validated on 6.4.7: Husks spawn(6)+replicate+chase+strike; death→respawn loop; HUD (health/cooldown/threat/downed); emissive dark-sci-fi look. EditMode **74/74**. ctx7-verified APIs. **Deepened same day:** auto-target on Husks, replicated respawn-invulnerability, and a `WaveSystem` threat director (escalating waves of 3 Husk variants — Grunt/Swarmer/Brute) replacing the flat sustain — runtime-validated (wave 1→2 escalation 4→6, distinct maxHP 30/15/80). [[DR-009_GameFeel_Identity_FirstBlood]], [[2026-06-02_GameFeel_Identity]], [[2026-06-02_GameFeel_Deepening]] |
|
||||
| **— 2026-06-03 Visual & controls polish —** | Non-milestone polish layered on M5.5 (no mechanical rework): HDRP→URP art import + reusable converter; a cohesive **Synty** sci-fi colony world (cosmetic SampleScene GameObjects) + **GabrielAguiar** combat VFX; **KBM mouse-cursor aim + gamepad aim** with last-actuation device auto-switch (rides the existing `PlayerInput.Aim` ghost field). | ✅ Done 2026-06-03 — [[DR-010_Art_Import_URP_Conversion_Visual_Upgrade]], [[DR-011_Synty_World_VFX_Integration]], [[DR-012_Aim_Controls_Cursor_Gamepad]] |
|
||||
| **— 2026-06-03 Pre-M6 cleanup —** | Loose-ends pass before M6: vault roadmap reconcile, Unity-template + orphaned-material removal, rate-limited turning, console/runtime health gate. | ✅ Done 2026-06-03 — [[2026-06-03_Pre_M6_Cleanup]] |
|
||||
| **M6 — Build/placement** | Server-authoritative grid build placement via RPC | ⬜ |
|
||||
| **M6 — The Aether Cycle (core loop)** | Reframed from "grid build placement" into the first vertical slice of the **core game loop**: Expedition (gather) → Defend (wave) → Build/Charge (spend), persistent base + procedural sorties, escalating toward a goal. Build placement is now Stage 3 of this milestone. | 🚧 In progress 2026-06-03 — **Stages 0–2 done + runtime-validated** on 6.4.7: **base/expedition split via coordinate-region + `GhostRelevancy`** (player transit despawns/re-grants the other region's ghosts; server==client); a **server phase-director** (Expedition→Defend→Build→Expedition auto-cycle, cycle 1→2, Husk `WaveSystem` only in Defend, escalation 4→6); and **resources + harvest** — a **global CycleDirector ghost** carrying the replicated `CycleState` + a shared resource **ledger** (relevant in every region, unlike the base storage), a procedural **expedition field** (8 resource-node ghosts seeded per cycle, region-scoped), and a tunnel-safe **harvest** sweep depositing into the ledger; client **HUD** shows phase + resource counts. Supersedes DR-008's "split requires streaming" framing. **Stages 3–4 (build placement/turret/ability-tiers, persistence/goal) = continuation.** — [[DR-013_M6_Aether_Cycle_Region_Split]], [[2026-06-03_M6_Aether_Cycle_CoreLoop]] |
|
||||
| **M7 — Automation** | Self-running tick-based production chains (deterministic offline catch-up) | ⬜ |
|
||||
|
||||
Promote items from [[Backlog]] here when committed.
|
||||
@@ -0,0 +1,106 @@
|
||||
---
|
||||
date: 2026-06-03
|
||||
type: session
|
||||
tags: [session, m6, core-loop, netcode, ghost-relevancy, design]
|
||||
---
|
||||
|
||||
# Session 2026-06-03 — M6 "The Aether Cycle": core-loop research, plan, and Stages 0–1
|
||||
|
||||
## Goal
|
||||
|
||||
Re-align M6 from the narrow "grid build placement via RPC" into **the first vertical slice of the actual core
|
||||
game loop**. Operator brief: a magic + sci-fi premise (start weak/amnesiac, a voice toward "THEM", harvest
|
||||
magical energy to build a base + charge abilities, procedural expeditions, periodic base-defense). Research
|
||||
good directions and **determine the core game loop in this slice**.
|
||||
|
||||
## Done
|
||||
|
||||
**Research + design (`/dots-dev`).** Three-stream research: codebase foundation map (M1–M5.5), co-op
|
||||
roguelite-base-defense loop precedent (Dome Keeper, Deep Rock, Hades, Risk of Rain, Vampire Survivors, Drill
|
||||
Core…), and magic+sci-fi narrative/progression (Warframe/Control/Bastion/Noita/Returnal). Synthesised **"The
|
||||
Aether Cycle"** — a Dome-Keeper two-phase loop: **Expedition** (gather, soft incursion timer) → **Defend** (a
|
||||
wave hits the base) → **Build/Charge** (spend resources on structures *and* ability tiers, one shared economy)
|
||||
→ repeat, escalating, toward a goal meter. Four operator decisions: persistent base + sorties; separate
|
||||
base/expedition scenes; multiple resource types; "The Awakening Engine" narrative lean. Plan approved.
|
||||
|
||||
**Stage 0 — region-relevancy world split (the netcode crux; DONE + validated).**
|
||||
- `RegionTag{byte Region}` + `RegionId`/`RegionMath` (`Simulation/World/RegionComponents.cs`); transit RPC
|
||||
(`RegionTransitRequest.cs`).
|
||||
- `RegionRelevancySystem` (Server, `GhostSimulationSystemGroup`): per-connection `GhostRelevancyMode.SetIsIrrelevant`,
|
||||
hides cross-region ghosts each tick (global/untagged ghosts stay visible for free). API verified on Netcode
|
||||
1.13.2 via `unity_reflect`.
|
||||
- `RegionTransitSystem` (Server): RPC → resolve player via `SourceConnection`→`NetworkId`→`GhostOwner`, flip
|
||||
`RegionTag`, teleport to region origin (expedition = base + (1000,0,0)).
|
||||
- Tagged players (`GoInGameServerSystem`), storage (`SharedStorageSpawnSystem`), Husks (`WaveSystem`) → Base.
|
||||
- **Validated headless:** transit→Expedition teleports the player to X=1000 and **despawns the base storage
|
||||
ghost on the client** (relevancy); transit→Base **re-grants** it; server==client, console clean.
|
||||
|
||||
**Stage 1 — macro phase director + wave gating (DONE + validated).**
|
||||
- `CycleState` ([GhostField] Phase/CycleNumber/PhaseEndTick — pre-annotated for the future CycleDirector ghost)
|
||||
+ `CyclePhase` consts + `CycleRuntime` (server-only) in `Simulation/World/CycleComponents.cs`.
|
||||
- `CyclePhaseSystem` (Server, `[UpdateBefore(WaveSystem)]`): Expedition (timed) → Defend (wave-driven) → Build
|
||||
(timed) → next cycle, all wrap-safe `NetworkTick` math. Gates `WaveSystem` via a one-line `CycleState` check.
|
||||
- **Validated headless:** full **Expedition→Defend→Build→Expedition** auto-cycle; **CycleNumber 1→2**; wave
|
||||
spawns **only** in Defend (husks=0 in Expedition); **escalation across cycles 4→6 Husks**; Husks tagged Base.
|
||||
|
||||
**Stage 2 — resources + harvest + cycle replication/HUD (DONE + validated).** Adversarially design-reviewed
|
||||
first via a 3-critic + synthesis workflow (it caught real bugs pre-code: the base-storage-ledger relevancy
|
||||
trap, a `GetSingleton<StorageEntry>` "multiple instances" collision, the harvest variable-frame dt-trap, a
|
||||
node double-destroy, the CycleState lazy-create hazard). Split into **2a** (CycleDirector global ghost +
|
||||
migrate `CycleState` onto it + HUD phase readout) and **2b** (resources/nodes/harvest/ledger + HUD counts).
|
||||
- **2a:** `CycleDirector.prefab` (dup UpgradePickup, mesh stripped, no RegionTag → global) carries `CycleState`
|
||||
+ a `StorageEntry` ledger buffer + `ResourceLedger` tag; `CycleDirectorSpawnSystem` (one-shot) spawns it;
|
||||
`CyclePhaseSystem` refactored atomically (`RequireForUpdate<CycleState>`, lazy-create deleted). **Validated:**
|
||||
exactly one `CycleState`, replicates to client, HUD shows `"DEFEND CYCLE 1"`; the global director stays
|
||||
relevant to an expedition player while the base storage despawns (the global-ledger thesis proven).
|
||||
- **2b:** `ResourceId` + `ResourceNode` ghost (`ResourceNode.prefab`, RegionTag{Expedition}); `ExpeditionFieldSystem`
|
||||
edge-spawns/despawns a seeded field per cycle; `ResourceHarvestSystem` (plain group after the predicted group)
|
||||
sweeps projectiles via the new `Projectile.LastStep` (written by `ProjectileMoveSystem`) → deposits to the
|
||||
global ledger (`StorageMath` reused). **Validated headless:** 8 nodes seed in expedition (round-robin A/O/B,
|
||||
invisible to the base player by relevancy); a hit deposits 5 Aether + decrements the node; full depletion
|
||||
despawns the node; 5 same-tick hits deposit all + destroy once (double-destroy safe); **tunnelling sweep**
|
||||
catches a node 3u past the projectile (point test would miss); field despawns on leaving Expedition; HUD reads
|
||||
`"AETHER 30 ORE 5 BIO 0"`. Burst ON, console clean.
|
||||
|
||||
**Visibility + playability pass (DONE + validated, operator-requested).** The systems worked but were invisible/
|
||||
unplayable in a real session (no in-world travel, void expedition, timer-only phases). Added: **walk-in gates**
|
||||
(`ExpeditionGate` baked entity + `ExpeditionGateSystem` server proximity transit — a glowing gate at the base
|
||||
deploys you to the expedition, a return gate brings you back) with **timer cap + early return** pacing
|
||||
(Expedition cap lengthened to ~60s; returning to base via the gate expires the timer → Defend); a **visible
|
||||
expedition place** (indigo ground plane + dark pillars at the expedition region in SampleScene); **bright glowing
|
||||
resource nodes** (M_Projectile material); and a **HUD clarity pass** (color-coded phase + countdown, BASE/ON-
|
||||
EXPEDITION location + gate hint, resource counts). **Validated via screenshots + headless:** stepping into the
|
||||
base gate deploys to the expedition (camera follows, 8 glowing nodes become visible by relevancy); stepping into
|
||||
the return gate comes back to base AND starts Defend early; HUD reads phase/countdown/cycle/resources/location.
|
||||
*Tooling gotcha:* `manage_gameobject create` `component_properties` silently dropped the enum/Vector3 fields
|
||||
(both gates baked with authoring defaults — the BaseGate worked only by coincidence); fixed with
|
||||
`manage_components set_property` + verified via the `mcpforunity://scene/gameobject/{id}/component/...` resource.
|
||||
|
||||
See [[DR-013_M6_Aether_Cycle_Region_Split]] for the full architecture + validated evidence.
|
||||
|
||||
## Decisions
|
||||
|
||||
- [[DR-013_M6_Aether_Cycle_Region_Split]] — M6 = "The Aether Cycle"; base/expedition split via coordinate-region
|
||||
+ `GhostRelevancy` (supersedes DR-008's streaming framing); server-authoritative phase director gating the
|
||||
Husk wave. Region-relevancy + phase-machine implemented and runtime-validated.
|
||||
|
||||
## Open / deferred (the Stage 3–4 continuation)
|
||||
|
||||
- ✅ **Stages 2a/2b done** (see above): client HUD + `CycleState` replication on the global CycleDirector ghost;
|
||||
multi-type resources + procedural harvest field + global ledger + HUD resource counts. Optional hardening:
|
||||
extract the harvest sweep into a pure `HarvestMath` + EditMode tunnelling/reproducibility tests (currently
|
||||
runtime-validated, consistent with how `ProjectileDamageSystem`'s sweep is covered).
|
||||
- **Stage 3 — Build placement + turret + ability tiers:** `BuildPlaceRequest` RPC + occupancy + `BuildPlacementMath`
|
||||
(unit-tested) + grid-snap preview; `Turret`+`TurretFireSystem` (auto-defend, reuse projectile path);
|
||||
`AbilityUpgradeRequest` spending the ledger. (The original M6 build slice.)
|
||||
- **Stage 4 — Persistence + goal meter:** host JSON save/restore (replayed through build/upgrade paths — new
|
||||
scope vs. DR-008); `GoalProgress` ghost ticked per cycle.
|
||||
- **Fiction reconciliation:** adopt Aether/Awakening-Engine naming into [[Identity]] (operator sign-off).
|
||||
- **Burst re-enable + editor restart:** Burst is currently **disabled** for the session (workaround for a stale
|
||||
Burst-cache exception when editing `WaveSystem` on an unfocused editor — see DR-013 gotcha). Restart Unity to
|
||||
clear the cache, then re-enable `Jobs ▸ Burst ▸ Enable Compilation` to restore full performance.
|
||||
|
||||
## Next
|
||||
|
||||
Checkpoint for operator feedback on the working core-loop skeleton, then continue Stage 2 (resources + harvest)
|
||||
— the gather half of the economy — followed by build placement (Stage 3) and persistence/goal (Stage 4).
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
id: DR-013
|
||||
title: M6 — "The Aether Cycle" core loop + base/expedition region split via GhostRelevancy
|
||||
status: accepted
|
||||
date: 2026-06-03
|
||||
tags:
|
||||
- decision
|
||||
- netcode
|
||||
- ghost-relevancy
|
||||
- world-architecture
|
||||
- core-loop
|
||||
- m6
|
||||
permalink: gamevault/07-sessions/decisions/dr-013-m6-aether-cycle-region-split
|
||||
---
|
||||
|
||||
# DR-013 — M6 "The Aether Cycle" core loop + base/expedition region split
|
||||
|
||||
## Context
|
||||
|
||||
M1–M5.5 + polish delivered a systems-complete prototype (predicted player, data-driven combat, co-op,
|
||||
physics/CC, home-base grid + shared storage, escalating Husk `WaveSystem`, death/respawn, VFX/HUD, Synty
|
||||
world, KBM/gamepad aim) — but with **no game loop**. The roadmap scoped M6 narrowly as "server-authoritative
|
||||
grid build placement via RPC."
|
||||
|
||||
A `/dots-dev` research + design pass (Dome Keeper, Deep Rock, Hades, Risk of Rain, Vampire Survivors, Noita,
|
||||
Warframe/Control/Bastion) reframed M6 as **the first vertical slice of the actual core game loop** — *The
|
||||
Aether Cycle* — reusing the ~70% of loop systems that already exist. The operator chose four directions:
|
||||
**persistent base + procedural sorties** (death = respawn, not wipe); **separate base/expedition scenes**;
|
||||
**multiple resource types** (Aether/ore/biomass); narrative lean **"The Awakening Engine"** (base = recharging
|
||||
hibernation-pod, Aether restores memory+power, a guiding voice toward "THEM"). Full plan: see
|
||||
[[2026-06-03_M6_Aether_Cycle_CoreLoop]].
|
||||
|
||||
The core loop is a **Dome-Keeper two-phase rhythm**: **Expedition** (gather resources from a procedural field,
|
||||
soft incursion timer) → **Defend** (a wave assaults the base; you + built structures hold) → **Build/Charge**
|
||||
(spend resources to place/upgrade structures *and* ability tiers — one shared economy) → repeat, escalating,
|
||||
with a long-arc goal meter toward THEM. Mechanically this is pure fulfilment of the locked [[Pillars]] (action
|
||||
ARPG + co-op base + automation + *persistent base + instanced/procedural expeditions*) — **no pillar changes**;
|
||||
only the fiction skin evolves (Aether/Awakening-Engine vs. [[Identity]]'s industrial-colony/Blight framing),
|
||||
which is reconcilable and tracked as a follow-up, not a mechanical rework.
|
||||
|
||||
## Decision
|
||||
|
||||
**The milestone is staged to de-risk the netcode world-split FIRST. Stages 0–1 are implemented + runtime-validated; Stages 2–4 are scoped for the continuation.**
|
||||
|
||||
1. **Base/expedition split = ONE server world, two spatial REGIONS at a coordinate offset, scoped per-connection by `GhostRelevancy` — NOT `SceneSystem` streaming.** This **supersedes DR-008's framing** that the split requires Option-C async subscene streaming (which DR-008 deferred). `RegionTag { byte Region }` (server-only, NOT a `[GhostField]`; `RegionId.Base=0` / `Expedition=1`); `RegionMath.RegionOrigin` puts the expedition at `baseCenter + (1000,0,0)`. A server `RegionRelevancySystem` (in `GhostSimulationSystemGroup`, before `GhostSendSystem`) sets `GhostRelevancyMode.SetIsIrrelevant` and, each tick, marks every region-tagged ghost **irrelevant to each connection whose player is in a different region**. `SetIsIrrelevant` (not `SetIsRelevant`) was chosen so **untagged/global ghosts stay relevant to everyone for free** — only cross-region ghosts are hidden. Verified API shape on the installed Netcode **1.13.2** via `unity_reflect` (the 1.10 published docs were closest): `GhostRelevancy` singleton with `GhostRelevancyMode` + `NativeParallelHashMap<RelevantGhostForConnection,int> GhostRelevancySet`; `RelevantGhostForConnection { int Connection; int Ghost }` (`Connection` = `NetworkId.Value`, `Ghost` = `GhostInstance.ghostId`).
|
||||
|
||||
2. **Region transit = `RegionTransitRequest { byte TargetRegion }` `IRpcCommand`** (mirrors the `StorageOpRequest` recipe — byte code, plain blittable, applied in the plain server `SimulationSystemGroup`, NOT the predicted loop). `RegionTransitSystem` resolves the sender's player via `ReceiveRpcCommandRequest.SourceConnection` → `NetworkId` → `GhostOwner`, flips its `RegionTag`, and teleports its `LocalTransform.Position` to the region origin. Players are tagged `RegionTag{Base}` on spawn (`GoInGameServerSystem`); the shared storage ghost (`SharedStorageSpawnSystem`) and Husks (`WaveSystem`) are tagged `RegionTag{Base}`.
|
||||
|
||||
3. **Macro-loop director = a server-authoritative state machine.** `CycleState { [GhostField] byte Phase; [GhostField] int CycleNumber; [GhostField] uint PhaseEndTick }` (+ `CyclePhase` byte consts Expedition/Defend/Build) — currently a **server-only singleton**, pre-annotated with `[GhostField]`s so it drops onto the runtime-spawned CycleDirector ghost unchanged when the client HUD is wired. `CyclePhaseSystem` (plain server `SimulationSystemGroup`, `[UpdateBefore(WaveSystem)]`) advances **Expedition (timed) → Defend (wave-driven) → Build (timed) → next cycle**, all on wrap-safe `NetworkTick` math (`TickUtil.NonZero` + `IsNewerThan`, never raw `uint <`). It **gates `WaveSystem`**: a one-line early-out (`if (TryGetSingleton<CycleState>(out var c) && c.Phase != CyclePhase.Defend) return;`) so the base-defense wave only spawns during Defend. Defend ends when the wave has run for this phase (`WaveState.WaveNumber > DefendStartWave`), is fully spawned, and no Husks remain (`CycleRuntime.DefendStartWave` is server-only bookkeeping, kept off the replicated struct).
|
||||
|
||||
4. **No new asmdefs.** New code under `…/World/` in Simulation (`RegionComponents`, `RegionTransitRequest`, `CycleComponents`) and Server (`RegionRelevancySystem`, `RegionTransitSystem`, `CyclePhaseSystem`); two one-line edits each to `GoInGameServerSystem` (tag player Base) and `WaveSystem` (Defend gate + tag Husk Base). The Server asmdef already references `Unity.NetCode` → `GhostRelevancy`/`GhostInstance` reachable with no asmdef edit.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Validated at runtime on 6.4.7 (single in-editor client), headless via `execute_code`:**
|
||||
- **Stage 0 (relevancy):** player spawns `RegionTag{Base}`; sends `RegionTransitRequest{Expedition}` → server player teleports to X=1000, region flips, **the base-region storage ghost despawns on the client** (`clientStorageGhosts` 1→0); transit back → storage **re-granted** (0→1), server==client position (no desync). Clean console (only the known tick-batching warning).
|
||||
- **Stage 1 (cycle):** full loop **Expedition → Defend → Build → Expedition** auto-advances on timers; **CycleNumber increments 1→2**; the wave spawns **only** in Defend (husks=0 in Expedition); **escalation persists across cycles** (wave 1 = 4 Husks → wave 2 = 6); Husks carry `RegionTag{Base}`.
|
||||
- **Delivers the "instanced/procedural expeditions" pillar without the streaming machinery DR-008 deferred** — region-relevancy reuses the existing runtime-ghost spawn path verbatim (no baked/prespawned ghosts → no prespawn section-ack/CRC handshake), and relevancy is a per-tick server-only write with no async-load race. Co-op drop-in is trivial (a connection without a spawned player is simply absent from the map and sees everything).
|
||||
- **Foundation for Stages 2–4** (the continuation): resources + harvest (multi-type, into the generalised `StorageEntry` ledger), build placement + turret + ability tiers (the original M6 RPC), persistence (host JSON, new scope vs. DR-008), goal meter — plus wiring the deferred **client HUD + cycle-state replication** (move `CycleState` onto a runtime-spawned CycleDirector ghost; the `[GhostField]`s are already in place).
|
||||
|
||||
## Open / deferred
|
||||
|
||||
- **Client HUD + `CycleState` replication** — deferred from Stage 1; bundle with the Stage 2 HUD (resource ledger readout). Needs the CycleDirector ghost prefab (duplicate `UpgradePickup.prefab`) + a baked spawner.
|
||||
- **Stages 2–4** — resources/harvest, build placement/turret/ability-tiers, persistence/goal. See the plan in the session log.
|
||||
- **Fiction reconciliation** — adopt the Awakening-Engine/Aether naming into [[Identity]] (currently industrial-colony/Blight); operator sign-off before rewriting the Identity doc. Light-touch in M6.
|
||||
- **Disk persistence** — the persistent-base run model now requires the save/load DR-008 deferred (Stage 4 = new scope).
|
||||
- **Teleport fidelity** — transit lands the CC character near (not exactly on) the region origin (collide-and-slide settling); the real game will land players on a designated pad/ring slot.
|
||||
|
||||
## Build gotcha recorded this session
|
||||
|
||||
- **Editing an existing `[BurstCompile]` ISystem's SystemAPI query set on an UNFOCUSED editor can leave a stale Burst binary** while the managed assembly recompiles with shifted source-gen query indices → a runtime `InvalidOperationException: "… required component type was not declared in the EntityQuery"` from an *unrelated* `GetSingleton<T>` in that system (the Burst stack reports the *old* line number — the tell). Root cause: Burst's async recompile doesn't complete on an unfocused editor (same family as the [[CLAUDE]] M2 Burst-cache gotcha). **Workaround used:** disable Burst for the session (`Jobs ▸ Burst ▸ Enable Compilation`; verified `BurstCompiler.Options.EnableBurstCompilation == false`) so every system runs the fresh managed source-gen. **Permanent fix = restart Unity** (clears the Burst cache) then re-enable Burst. Prefer a **focused** editor for Burst-affecting edits.
|
||||
|
||||
Mirrors the server-authoritative + deterministic + co-op pillars from [[Pillars]]; supersedes the streaming framing in [[DR-008_M5_HomeBase_BaseLayer_Storage]]; reuses the byte-RPC + runtime-ghost + tick-safe patterns from [[DR-008_M5_HomeBase_BaseLayer_Storage]] / [[DR-009_GameFeel_Identity_FirstBlood]].
|
||||
Reference in New Issue
Block a user