This is V4's strategy game. The RTS cell is the top-down, click-to-command
surface where the loop is gather, build, tech, expand, and macro an army into
a fight — the genre where the camera floats above the battlefield, two
resources drive every decision, and a match is won by an economy three minutes
before it is won by a battle. It cares enormously whether two machines
re-running the same commands arrive at the same world, because RTS networking
ships only inputs and trusts every client to simulate the rest. V4 builds two
macro games on that single foundation: a StarCraft-style contest of three
asymmetric factions on a competitive ladder, and an Age-of-Empires-style
historical game of twelve civilizations climbing five ages toward a Wonder. Both
feed one deterministic C++ simulation (V4RTS) and one lockstep transport
(V4Netcode). For the full mode taxonomy, the roster, and the scope this cell
slots into, start at the hub: ../V4_features.md.
What ships, honestly#
The RTS simulation core is real, compiled, and pinned by domain-correct
tests. The cell module V4/ue/Source/V4RTS carries 11 .cpp / 11 .h of
genre logic — a 32.32 fixed-point number type, a seeded A* pathfinder that
self-audits its own determinism, an Ore/Crystal economy, a grid-footprint
builder, a timed production queue, a prerequisite-gated tech tree, an age-up
ladder, a three-state fog grid, control groups, and a Wonder-victory timer —
wired into GAS through four RTS-gated abilities in V4RTSAbilities. The numbers
are domain-specific, not placeholders: an 8-per-second gather rate, a 10-s
build and 5-s train, a population-scaled age-up cost, a 420-second
(seven-minute) Wonder defense. Three RTS specs under
V4/ue/Source/V4Tests/Private/V4RTSTests (EconomySpec, PathSpec, FogSpec)
plus the per-plugin AsymmetricRTSSpec and HistoricalRTSSpec assert
computed values — a gather of exactly 48, a Manhattan heuristic of exactly
7, a radius-one reveal of exactly 5 cells — the kind of check that fails
instantly against a hardcoded return.
Three honest qualifications, in the spirit of the architecture companion.
First, V4 has zero cooked .uasset content: factions, units, civs, tech
tiers, build orders, and maps are data definitions and JSON-described content,
not authored levels or meshes. UV4AsymmetricRTSCatalog::BuildMaps() returns
twenty FV4AsymRTSMapDefinition rows with player slots and team sizes — a real,
validated map manifest, not twenty playable terrains. Second, the cell is
a library of individually-tested components and subsystems, not yet a single
running match. Each spec exercises a piece in isolation (NewObject<> then
drive it); there is no in-tree "RTS GameMode actor" that composes resource →
build → train → fog → pathfinding → lockstep into one ticking loop, and unit
combat resolution is the thinnest part — UGA_AttackMove assigns a control
group, trained units arrive as gameplay tags, and per-unit HP/targeting leans on
the shared GAS attributes rather than a bespoke RTS battle simulator. Third,
the lockstep layer is a deterministic library, not a wired net driver (no
FSocket, no replicated UPROPERTY), and it hashes the input history, config,
and seed — folding the RTS sim's own unit positions into that digest is
V4RTS's job and is not yet wired or tested. The honest netcode accounting
lives in
../architecture/networking-determinism.md;
what follows is the strategy game on its own terms.
The RTS experience — two macro games on one sim#
Strip the genre to its verbs and the shared loop is small: send workers to a
resource, spend it to raise buildings, queue units out of those buildings,
research the tech that unlocks better units, push the fog back with scouts, and
trade armies until one economy collapses. V4 ships that loop twice, as two
GameFeatures plugins on top of the same V4RTS core.
StarCraft-style — three asymmetric factions and a ladder#
V4Mode_RTS_Asymmetric is the micro-heavy, build-order-driven competitive game.
UV4AsymmetricRTSCatalog::BuildFactions()
(V4/ue/Plugins/V4Mode_RTS_Asymmetric/.../V4AsymmetricRTSSystems.cpp:108)
authors three deliberately asymmetric factions, original IP standing in for the
Terran/Zerg/Protoss design space: Vanguard (an industrial mech faction with
MobileBases, RepairableUnits, ScanAndCover, hero
Vanguard.SovereignBattlecruiser), Swarm (a biomass faction with
CreepSpread, LarvaMorph, FastEconomy, hero Swarm.BroodMother), and
Choir (a psionic faction with WarpInProduction, ShieldedUnits,
ArchonMerge, hero Choir.ArchTemplar). BuildUnits() defines 24 launch
units, exactly eight per faction, each carrying flags that drive real
mechanics — bWorker, bCloaked, bDetector, bShielded, bRepairable — and
an Ore/Crystal cost (a Choir ArchTemplar costs 500/500, a Swarm Zergling
just 25/0). On top sit nine tech tiers (three per faction) and three
build-order wiki docs with timed steps — the Vanguard one-base-pressure opener
hits TrainWorker at 12 s, BuildBarracks at 42 s, a marine pair at 78 s, all
at the genre's worker counts.
The competitive backbone is UV4AsymmetricRTSLadder. BuildRanks() returns the
seven-rank ladder (Bronze → Silver → Gold → Platinum → Diamond → Master →
Grandmaster), and the matchmaking math is a real Glicko-2 implementation, not
a stub: ApplyGlicko2Result (:350) computes the q = ln(10)/400 constant,
the g(RD) deviation-scaling factor, the expected score E, the estimated
variance d², and the updated rating and deviation — then ApplySeasonalReset
pulls a player's rating halfway back to the 1500 baseline each season.
AsymmetricRTSSpec proves a win raises the rating, a placement match decrements
the five-match counter, and a reset restores it. Replays are input-only:
UV4AsymmetricRTSReplayLibrary::BuildInputStreamReplay sorts a command list by
frame and mints a public https://replays.v4.game/rts/asymmetric/… URL — the
"download any ladder match" promise as concrete code.
Age-of-Empires-style — twelve civilizations, five ages, Wonders#
V4Mode_RTS_Historical is the slower, population-heavy historical game.
UV4HistoricalRTSCatalog::BuildCivilizations() authors twelve launch
civilizations, each with unique units and a Wonder: Britons (Longbowmen,
Stonehenge), Mongols (Mangudai), Aztecs (Jaguar Warrior), Japanese (Samurai),
Vikings (Berserker), Mali (Camel Archer + Gbeto), Khmer (Ballista Elephant),
Ethiopians (Shotel Warrior), Yorùbá (Ile-Ife Cavalry + Ogboni), Bantu/Kongo
(Nkisi Mage + Kanga Spearman), Persians (War Elephant), and Aboriginal
Australians (Boomerang Skirmisher, Uluru). Crucially, every civ row carries
sensitivity-review metadata — bSensitivityReviewed, a CulturalReviewId,
and a non-empty RepresentationNote — and HistoricalRTSSpec fails the build
if any civ is missing it, encoding the §61 cultural-review gate as a test rather
than a promise.
The historical game runs a four-resource economy (Food/Wood/Gold/Stone, distinct
from the StarCraft cell's two-resource Ore/Crystal) and a five-age ladder
(Dark → Feudal → Castle → Imperial → Industrial). UV4HistoricalAgeModel
scales age-up cost with the number of bases, and
UV4HistoricalWonderVictoryComponent models the Wonder win exactly: you must
finish construction before the defense timer can start, then survive the
seven-minute (419 + 1 s in the spec) defense to win. The headline scale claim
is measured, not asserted:
UV4HistoricalPopulationModel::CaptureFourPlayerFFAProfile simulates a
four-player free-for-all at a 200-population cap — 4 × 200 = 800 units — over
250 frames and records real Average/P95/Max frame times, and the feel gate
passes only if P95 ≤ 16.7 ms and Max ≤ 22 ms. That is the "200 villagers
without stutter" requirement turned into a timed automation profile.
Base, unit, and economy systems#
The pieces below live in the shared V4RTS core, so both macro games inherit
the same simulation behavior. Each is a UActorComponent with
bCanEverTick = false — the sim is advanced explicitly by the match loop,
never by the engine tick, which is what keeps it lockstep-safe.
The Ore/Crystal economy#
UV4ResourceComponent (V4/ue/Source/V4RTS/Private/V4ResourceComponent.cpp)
holds integer Ore and Crystal and exposes AddResources, SpendResources,
and CanAfford over an FV4RTSResourceCost. Gathering is the load-bearing
formula: GatherFromPatch returns
min(Patch.Remaining, GatherPerSecond · DeltaSeconds · WorkerCount), floors the
result to whole resources, banks them into the right resource (Ore or Crystal by
the patch's bCrystal flag), and depletes the patch. With the default
GatherPerSecond = 8, EconomySpec pins the arithmetic: three workers on a
100-unit patch for two seconds gather exactly 48 (8 × 2 × 3), bank 48 Ore,
leave the patch at 52, and a 30-Ore spend leaves 18. That is a genuine
economic rate model — the thing a build order is timed against — not a counter
that ticks up by a constant.
Base building and the production queue#
UV4BuildComponent makes placement spatial. CanPlaceStructure walks every
cell of an FV4RTSBuildOrder's footprint against the FV4RTSGrid, rejecting
any cell that is out of bounds or blocked, and QueueStructure atomically
validates placement and spends the cost before enqueuing — if the resource
spend fails, nothing is placed. Units come from UV4TrainComponent: QueueUnit
charges the cost up front, and TickProduction(DeltaSeconds) drains real time
against the head order's TrainSeconds (default 5), completing it and
broadcasting OnUnitTrained with the finished unit's gameplay tag, then rolling
to the next queued order with any leftover delta. Both are FIFO queues with
honest partial progress — the macro player's "queue five workers and tab away"
loop.
Tech trees and age-up#
UV4TechTreeComponent is a real dependency graph, not a flat unlock list.
CanResearchTech checks four conditions in order: the tech exists, it is not
already unlocked, the requesting faction tag matches (so a Vanguard player
cannot research a Choir tech), every prerequisite tag is already unlocked,
and the cost is affordable; only then does ResearchTech spend and unlock. Age
progression is UV4AgeUpComponent over
EV4RTSAge { Dark, Feudal, Castle, Imperial }: TryAgeUp starts from a base
cost (500 Ore, 200 Crystal), scales it by population pressure (+4 Ore
and +2 Crystal for every point of population above 50), refuses to advance
past Imperial, and broadcasts OnAgeChanged. The cost-scaling is the design
rule — "age-up cost scales with population pressure" — implemented as arithmetic
on the live population count.
Fog of war and unit control#
UV4FogOfWarComponent implements the canonical three-state fog over a flat cell
array: EV4RTSFogState { Unexplored, Explored, Visible }. RevealCircle uses a
true squared-distance radius test (Δx² + Δy² ≤ r²), and BeginVisibilityFrame
demotes every Visible cell back to Explored at the top of each frame so
vision is recomputed from current unit positions — the standard "grayed memory
vs. live sight" model. FogSpec checks a radius-one reveal lights exactly the
five-cell plus-shape, the diagonal stays dark, and a frame reset returns every
cell to Explored. Selection is UV4UnitGroupComponent:
AssignGroup(Hotkey, Units) stores a control group keyed by number, filtering
out invalid actors on both write and read, with GetGroup and ClearGroup —
the Ctrl+1 muscle memory of the genre. The asymmetric plugin extends fog with
cloak/detector logic: CanSeeCloakedUnit returns true only when a cell is
Visible and a detector is in range.
Wonders and the co-op tower-defense variant#
UV4WonderComponent is the win-condition timer: StartWonderTimer arms it,
TickWonder accumulates defended time against RequiredDefenseSeconds = 420.0f
(seven minutes), and crossing it latches bVictoryReady and fires
OnWonderVictoryReady. A third plugin, V4Mode_RTS_TowerDefense, reuses the
same map and Wonder substrate for a co-op PvE variant: V4TowerDefenseSystems
authors FV4TDPlayerSlots where one Defender controls the Wonder and up to
three Attackers control the incoming waves, plus FV4TDWaveDefinition rows
carrying enemy budget, spawn-lane count, target pace, and boss-wave flags across
lane-typed maps — the "one player defends, one-to-three attack" asymmetric
tower-defense the catalogue describes.
The deterministic sim#
The RTS cell is the one place in V4 where the simulation must be
bit-identical across machines, because lockstep replicates only inputs. V4RTS
takes that seriously at the type level.
Fixed-point and seeded pathfinding#
FV4RTSFixed32 (V4/ue/Source/V4RTS/Public/V4RTSTypes.h:24) is a 32.32
fixed-point number — FractionalBits = 32, OneRaw = 1ll << 32 — with integer
add, subtract, and ordering operators and a ToIntegerFloor. It exists so the
simulation never touches a float where two machines might round differently.
UV4PathfindingSubsystem::FindPath is a real A* over an FV4RTSGrid that uses
fixed-point G/F scores and a Manhattan-distance Heuristic, and its
tiebreak is the determinism-critical detail: when two open-set nodes have equal
F, it orders them by V4CellSortKey = HashCombine(cellHash, Seed), so the
expansion order is reproducible from the match seed rather than pointer- or
insertion-dependent. The neighbor list is seed-sorted for the same reason.
AuditDeterministicPathfinding is a built-in self-test that runs the same query
twice under seed 314, asserts the paths match step-for-step, and checks that the
(0,0) → (3,4) heuristic equals exactly 7 cells — determinism verified in
code, not asserted in prose. BuildFlowFieldNextSteps derives a per-cell flow
field toward a goal for cheap group movement.
Lockstep, hashing, and desync recovery#
V4RTS.Build.cs depends on V4Netcode, where the lockstep transport lives.
UV4RTSLockstepSubsystem (a UGameInstanceSubsystem) advances on a fixed
integer step — 25 Hz, 40 ms — so the same input stream produces the same
frame count regardless of render rate. Each frame it gathers pending inputs,
runs them through a single canonical sort (by player id, then command, then
values, then target cell) so two clients that received inputs in different
network order hash them identically, then chains a determinism digest via
ComputeDeterministicHash. The load-bearing subtlety is FName hashing: a raw
GetTypeHash(FName) hashes the process-local name-table index (different per
machine), so the subsystem uses StableNameHash = FCrc::StrCrc32 over the
name string, making the digest content-based and machine-stable. Match RNG is
a named V4SplitMix64 seeded per match, and every draw is logged and
order-audited so non-deterministic iteration cannot silently consume the stream
differently on two peers. When a peer hash disagrees, RecoverFromDesync rolls
back Frame − RecoveryRollbackFrames (default 4) and applies the
authoritative snapshot; the whole input stream serializes to the input-only
replay (~1 MB per 30-minute match). The full, honestly-qualified treatment —
including why this is a deterministic library rather than a live net driver — is
in
../architecture/networking-determinism.md.
What's real, and what's thin#
Read adversarially, the maturity story is uneven but honest. Real and
tested: the fixed-point type, the self-auditing seeded A*, the gather/spend
economy, grid-footprint building, the timed production queue, the
prerequisite-and-faction-gated tech tree, population-scaled age-up, three-state
fog with cloak/detector, control groups, the Wonder timer, the Glicko-2 ladder,
the input-only replay, and the measured 800-unit FFA frame profile — each pinned
by a spec that checks a computed value. Thin or not-yet-wired: there is no
single in-tree match actor that ties these components into one running loop (the
specs drive them in isolation); unit combat resolution is the least-developed
system — UGA_AttackMove only assigns a control group and trained units are
gameplay tags, so there is no RTS-specific damage/targeting/death sim yet; the
lockstep digest hashes inputs and seed but not the sim's own unit state; and
every map, faction, and civ is a validated data manifest, not authored, cooked
content. The bones of the genre are real and correct; the flesh of a shippable
match — fought battles, a composed match loop, baked terrain — remains.
How the RTS cell ties to the shared engine#
Like every V4 cell, the RTS surface routes player intent through the Gameplay
Ability System, and its abilities are cell-gated so an RTS ability cannot
fire on a non-RTS pawn. The four abilities in V4RTSAbilities.cpp —
UGA_BuildStructure (a default 16×16 grid, 2×2 footprint), UGA_TrainUnit,
UGA_ResearchTech, and UGA_AttackMove — each set
RequiredCell = EV4RulesetCell::RTS, delegate to the components rather than
reimplementing them, and fail loud with a precise reason string
("build-unavailable", "train-unavailable", "research-unavailable",
"missing-tech-tree") instead of faking success. That is the same GAS spine
that carries an operator's attributes from a Wukong boss duel to an RTS hero
unit, and the same fail-loud seam the whole project favors.
The shared roster is the connective tissue: a named operator who is a hero in the Action-RPG cell appears here as an RTS hero unit, earning the same cross-cell Operator XP. For the souls-like sibling on the same GAS backbone, see ./cell-action-rpg-wukong.md; for the GAS layout, the perception model, the netcode authority, and the mode machinery every cell sits on, see ./shared-cross-cell-engine.md. For the engine-side, genre-by-genre view of this module's determinism and maturity, read the architecture companion, ../architecture/per-cell-deep-dives.md.
Related#
- The feature hub: ../V4_features.md
- Action RPG (Wukong) cell — the souls-like sibling on the same GAS spine and shared hero roster
- Shared cross-cell engine — the GAS spine, perception model, netcode authority, and mode machinery every cell sits on
- Architecture companions:
../architecture/per-cell-deep-dives.md
(the
V4RTSengine view) and ../architecture/networking-determinism.md (the lockstep transport, honestly qualified)