Standard of Iron has two formation layers because they solve two different spatial problems.
A unit layout places the soldiers inside one logical troop entity. An army formation places multiple troop entities relative to one another. The two layers can cooperate, but they do not share identity or ownership.
| Layer | Arranges | Primary owner | Identity |
| Unit layout | soldiers inside one troop entity | Game::Formation::UnitLayoutSystem |
UnitLayoutId |
| Army formation | multiple troop entities | ArmyFormationPlanner + ArmyFormationRegistry |
FormationDoctrineId + ArmyFormationIntent |
Keeping those layers separate lets a troop change its internal soldier arrangement without leaving its army group, and lets an army redeploy without redefining the geometry of every soldier inside every troop.
Four concepts coexist at runtime and should not be collapsed into one “formation state.”
A troop can therefore be:
at the same time.
That independence is central to both data authoring and runtime behavior.
The unit-layout system is defined primarily in game/formation/unit_layout.h.
Its job is to turn a stable logical soldier index and an authored style into a local offset and facing inside a troop entity.
UnitLayoutId is a compact interned handle. The invalid value is 0xFFFF.
A troop profile refers to generic layout names. The loaded layout library resolves those names to an interned style and can prefer a doctrine-qualified variant when one exists.
The runtime therefore does not need one giant enum containing every Roman, Carthaginian, Sepulcher, marching, defensive, and special layout.
UnitLayoutShape currently includes:
Ranks;Wedge;LooseOrder;Column;Cluster;Circle;Shell; andArc.The shape selects the broad geometry family. UnitLayoutStyle then tunes that family with authored parameters.
UnitLayoutStyleCurrent style data includes parameters for:
The result is a data-driven silhouette rather than a hard-coded faction branch.
UnitLayoutQueryA layout query carries the facts needed to resolve one soldier placement, including:
formed_ratio;UnitLayoutSystem::offset() resolves one query into local position/yaw. UnitLayoutSystem::compute() provides the same system at whole-unit scale.
The query is intentionally independent of the soldier's current world position. A layout is geometry, not a second movement system.
Internal layouts are deterministic for stable inputs.
A soldier's slot is derived from logical identity and layout/query parameters, which gives several useful properties:
Random-looking variation is derived from deterministic seed/index input rather than from wall-clock randomness.
FormationCombat::living_slot_indices() is the shared boundary for deciding which internal presentation slots remain occupied.
Combat and renderer-side consumers use the same living-slot information instead of independently estimating survivor positions from health.
That matters because a formation can be visually sparse after casualties without renumbering every survivor into a new position on every frame.
Troop formation profiles name generic layouts such as normal, marching, or defensive styles.
UnitLayoutLibrary::resolve(doctrine, generic_name) resolves in two steps:
<doctrine>.<generic>;This allows doctrine-specific silhouettes while preserving a generic fallback.
For example, two factions can both request a close-order infantry role while using different rank stagger, depth, arc, grouping, or shell geometry.
Formation data is loaded from:
assets/data/formations/
on top of built-in defaults.
The shipped content can therefore refine formation geometry without requiring a new C++ enum or renderer branch for every doctrine variant.
See assets/data/formations/README.md for the current file schema.
Unit layouts carry faction identity through geometry as well as material/colour.
The current data/model can express distinctions such as:
The important architectural point is that these differences are authored through the layout system. The renderer does not need a switch (nation) to decide where soldiers stand.
Army-scale formation state lives in game/formation/army_formation_types.h and is owned by ArmyFormationRegistry.
An army formation is a committed group of troop entities with one doctrine, one intent, one anchor/facing, one set of options, and one slot plan.
The current intent enum is:
enum class ArmyFormationIntent {
FactionDefault,
Line,
Column,
Defensive,
Assault,
Encirclement,
SiegeEscort
};
Intent describes the tactical deployment requested by the player or AI.
Doctrine describes how that intent is expressed by the faction/army.
Those concepts are separate so that two doctrines can both support Line while producing different role placement, frontage, depth, reserve structure, and internal troop layouts.
ArmyFormationOptions refines a doctrine template without replacing it.
Current option families include:
Balanced;StrongLeft;StrongRight;Split.ReformAtDestination;MaintainFormation.Front;Rear;Skirmish.CompositeByRole;SeparateContingents;CommanderDoctrine;MajorityDoctrine.Options also include:
This makes the player's fine-tuning controls and AI planner operate on the same data model.
Doctrine templates match troop role tags, not hard-coded troop IDs.
A troop's formation data can declare roles such as line infantry, spear infantry, shielded troops, centre, reserve, and other current tags.
The planner can then say “place troops with this role in this part of the formation” rather than “place troop type X at slot Y.”
That makes new troop types compatible with existing doctrine templates when their role data is authored correctly.
ArmyFormationA committed formation stores authoritative group state such as:
Member entities carry a formation-membership component containing the group/slot back-reference.
The registry remains the source of truth for group state. Group geometry is not reconstructed by scanning member positions and guessing which formation they were supposed to belong to.
ArmyFormationPlanner separates local geometry from world placement.
Conceptually:
members + doctrine + intent + options
│
â–Ľ
build_layout()
│
â–Ľ
local formation slots
│
├─ anchor
├─ facing
└─ terrain/navigation context
â–Ľ
place()
│
â–Ľ
world-space FormationPlan
plan() is the combined convenience path.
This split is important for interactive placement. Dragging a preview across the ground can reuse the same role/template layout while changing only world-space placement and terrain fitting.
The planner exposes a layout signature for inputs that affect local slot geometry.
Anchor and facing affect placement, not the local role/template layout itself. A placement UI can therefore avoid repeating expensive role/layout work when only the mouse position or facing changes.
The cache tests verify that the split path and full plan() path remain equivalent.
A perfect geometric formation may not fit the world at its ideal coordinates.
SlotTerrainFitter resolves each desired slot against walkable terrain and separation constraints.
Every slot is classified as:
Valid — ideal position is usable;Adjusted — a nearby valid position was found; orBlocked — no acceptable placement was found.The result belongs to the plan and can be shown before the player commits the order.
Terrain fitting searches outward from the ideal slot position and avoids assigning multiple troops to the same resolved location.
The planner therefore preserves two separate facts:
An adjusted slot is not necessarily an error. It means the shape can still be realized with a local nudge. A blocked slot means the planner could not place that member under the current rules.
FormationPhase currently contains:
Reforming;Formed;Disrupted;Opening;Traversing;Arrived.Phase describes the group runtime state, not merely the last command that was issued.
This is useful because formation behavior spans several distinct transitions:
ArmyFormationRuntime measures whether occupied, placeable slots are actually being held.
The current constants in army_formation_registry.cpp include:
1.35 × spacing;>= 0.8;<= 0.45.Cohesion is therefore a measured group property rather than an assumption that the formation is “formed” because a move command completed.
ArmyFormationRuntime::damage_taken_multiplier() is applied in the combat damage pipeline.
This lets army-scale cohesion affect combat while remaining independent from unit-level defensive layouts.
A troop can receive both:
DefensiveUnitLayoutService; andArmyFormationRuntime.The two effects compose because they describe different spatial layers.
ReformAtDestination lets troop entities route toward the new destination and assemble the target shape there.
This is robust through irregular terrain because the group does not try to preserve one rigid footprint through the entire route.
MaintainFormation keeps the group under formation runtime control as the center advances.
The runtime owns group movement-plan state, and route-follow speed applies ArmyFormationRuntime::move_speed_multiplier() together with the other movement modifiers that affect the troop.
Neither movement policy turns internal soldiers into navigation agents. The logical troop entity remains the navigation body.
Narrow passages introduce a temporary traversal layout inside a troop entity.
This is separate from the authored normal/defensive unit layout and separate from the army group's strategic route.
UnitTraversalLayoutSystem publishes TraversalLayoutFacts into movement state.
Current facts include information such as:
The position contract is layered:
This layering allows a shield formation to pass through a narrow opening without pretending that the doctrine/defensive state disappeared.
FormationCombat::soldier_spatial_anchors is the shared boundary for systems that need exact soldier-level coordinates.
Consumers such as combat geometry, RPG/direct-control targeting, weapon traces, casualty effects, and selection/presentation can use the same resolved anchor precedence instead of calculating independent soldier positions.
Ordinary RTS entity selection still operates on troop/entity identity; the soldier-level query is used where exact internal geometry is required.
Defence Mode is a unit-layout state, not an army formation.
Each logical troop keeps:
Defensive mode changes the internal soldier arrangement and exposes the related movement/combat modifiers through DefensiveUnitLayoutService.
Nation data selects:
Roman and Carthaginian infantry can therefore use different defensive geometry without creating a new army-scale intent for every internal stance.
UnitLayoutStateComponent carries the authoritative transition state.
Formation data supplies form_seconds and break_seconds for the transition. Gameplay bonuses and baked defensive presentation are tied to the active formed state according to the current runtime rules.
The component is serialized and included in render/presentation state so save/load and snapshot rendering agree about whether the unit is normal, forming, formed, or breaking.
The Roman defensive family uses a closed Shell-style arrangement with directional shield presentation for front, rear, flanks, and overhead positions.
The renderer applies the corresponding baked upper-body overlay poses while locomotion can continue through the lower-body authored movement path.
The important architecture point is that “testudo” is an internal soldier-layout/presentation state of one logical troop, not a second army object.
The Carthaginian defensive family uses an open Arc-style shield wall rather than the closed Roman shell.
Its authored geometry, poses, timings, and combat modifiers are data/runtime choices within the same defensive-layout system.
The player-facing planner uses the production ArmyFormationPlanner.
A placement preview is therefore a real formation plan with:
The preview is not a decorative approximation drawn independently of the actual committed plan.
Ground placement establishes the formation anchor. Orientation determines facing. The UI can expose frontage/depth/spacing and other options that modify the planner inputs.
The resulting troop footprints show where logical troop entities will be placed, not every internal soldier.
One troop can use the same positioning/facing gesture without registering a one-member army formation.
This keeps the useful “place and face this block” control while preserving the semantic boundary that an army formation is a group-level object.
The formation panel reads current planner/controller state and can show:
Unavailable intents can remain visible with an explanation instead of changing the meaning/order of intent selection according to the current roster.
FormationStatusBadge represents committed formation state after placement is over.
It can report:
This gives the player feedback on whether the group actually formed or became disrupted, not merely which intent was selected earlier.
AI and player groups share the same planner/runtime.
Game::Systems::AI::plan_ai_formation builds formation members from the AI snapshot and resolves the doctrine/intent through the production planner.
AI station/muster logic decides where a group should assemble, but it still relies on formation terrain fitting and slot availability to determine whether the candidate is usable.
This prevents the AI from bypassing the same terrain and role constraints the player sees.
See AI_ARCHITECTURE.md for station resolution and committed attack-wave behavior.
Army formations are authoritative game state and are serialized with the match.
The registry persists information required to reconstruct the committed group, including its doctrine, intent, options, anchor, members, slots, phase/cohesion-related state, and movement-plan state as defined by the current serializer.
Member back-references are restored consistently with the group registry.
Unit-layout transition state is also component state and survives save/load.
The save system does not infer formations from what the renderer happened to draw before the save.
FormationDataLoader applies authored formation data over the built-in defaults.
Validation covers the vocabulary/runtime references used by the planner and unit-layout system, including areas such as:
The content_validator links the same production formation/data code, which reduces the risk of the validator accepting a format the runtime interprets differently.
Formation behavior is covered at several levels.
| Area | Representative tests |
| Unit-layout geometry/determinism | tests/formation/unit_layout_test.cpp |
| Army planner and doctrine roles | tests/formation/army_formation_planner_test.cpp |
| Registry lifecycle/persistence | tests/formation/army_formation_registry_test.cpp |
| Movement policies | tests/formation/formation_movement_test.cpp |
| Data overlay/validation | tests/formation/formation_data_loader_test.cpp |
| Terrain/navigation fitting | tests/formation/formation_terrain_navigation_test.cpp |
| Cohesion and combat multiplier | tests/formation/formation_cohesion_test.cpp |
| Planner split/cache behavior | tests/formation/formation_planner_cache_test.cpp |
| Defensive unit layouts | tests/systems/defensive_unit_layout_test.cpp |
| Planner UI | tests/ui/qml/tst_formation_panel.qml |
| Status badge | tests/ui/qml/tst_formation_status_badge.qml |
| Input/placement behavior | tests/core/input_command_handler_test.cpp |
| Key binding | tests/ui/input_bindings_test.cpp |
Arena scenarios prefixed with unit_layout_ and army_formation_ exercise the same runtime visually with real rendering and scenario commands.
Formation bugs are easier to diagnose when the two formation layers are kept explicit.
Inspect:
UnitLayoutId;Inspect:
ArmyFormationOptions;Inspect:
That indicates a layer violation: Defence Mode should alter the internal unit layout and its modifiers, not create/replace the army-group identity.
Inspect FormationCombat::soldier_spatial_anchors, living-slot state, traversal state, and published presentation rather than adding another renderer-local offset rule.
The current formation system depends on these invariants:
ArmyFormationRegistry;Valid, Adjusted, or Blocked instead of silently stacking units;
| Concern | Source |
| Unit layout types/system | game/formation/unit_layout.* |
| Formation types/options | game/formation/army_formation_types.h |
| Army planner | game/formation/army_formation_planner.* |
| Registry/runtime/cohesion | game/formation/army_formation_registry.* |
| Formation service | game/formation/army_formation_service.* |
| Formation data loader | game/formation/formation_data_loader.* |
| Defensive layout runtime | game/systems/defensive_unit_layout_service.* |
| Traversal layout | game/systems/unit_traversal_layout_system.* |
| Movement facts | game/core/movement_facts.h |
| Authored formation data | assets/data/formations/ |
The architecture documented here describes the current unit-layout, army-group, traversal, defensive-state, AI, persistence, and UI contracts. Historical implementation stories are not needed to understand those contracts and are deliberately kept out of the reference article.