ifndef::imagesdir[:imagesdir: ./images] [[section-design-decisions]] == Architecture Decisions === ADR-1: Dynamic-only lighting for the demo * **Context**: The demo is a fixed daytime scene. Stationary lights demand a lighting build and produce "LIGHTING NEEDS TO BE REBUILT" warnings. * **Decision**: `r.AllowStaticLighting=False`, all lights `MOVABLE`. * **Consequences**: No bake step, no warning, simpler iteration. Slightly higher GPU cost per frame (no baked shadows) — acceptable for the demo scope. === ADR-2: Materials authored in Unreal, not imported from Blender * **Context**: FBX→Unreal import maps all Blender diffuse colours to white. Flat colour materials do not survive the pipeline. * **Decision**: Build all materials (asphalt, concrete, stucco, façade stone, road paint) directly in Unreal with procedural nodes. * **Consequences**: Materials cannot be previewed in Blender look-dev. Must round-trip to Unreal for visual check. But: full PBR control, world-space noise, no import surprise. === ADR-3: Complex-as-simple collision on terrain via C++ helper * **Context**: UE 5.8 removed `bUseComplexAsSimpleCollision`. Python cannot set `CollisionTraceFlag` on `StaticMesh`/`BodySetup` at all. The Interchange default is bbox-only simple collision, which placed an invisible lid 8 m above the street. * **Decision**: `UGigLineAssetTools::SetComplexCollisionAsSimple` (C++) forces `CTF_UseComplexAsSimple` and saves the asset. * **Consequences**: One C++ helper class. The flag must be saved on the asset (not just the map). No Python workaround exists. === ADR-4: Reset captures StartLocation on first grounded frame * **Context**: `BeginPlay` captures `StartLocation` at the PlayerStart, which is above grade (Z 600) so the capsule does not spawn embedded. Every reset dropped the player 1.4 m back into the air. * **Decision**: Repeating 0.25 s timer captures `StartLocation` once the character reports `IsMovingOnGround()`, then clears itself. * **Consequences**: Reset is correct. A one-shot timer at 2 s is wrong (fires after walking begins, records mid-stride position). === ADR-5: Core ticker for QA self-test, not world timer * **Context**: Pausing the game stops world timers. A timer-driven test freezes exactly when it proves pause works. * **Decision**: `FTSTicker::GetCoreTicker()` drives the `-qaselftest` harness. * **Consequences**: Test runs even while paused. Must be enabled explicitly (`-qaselftest`), never in normal play. === ADR-6: South = +Y, north = −Y in Unreal * **Context**: The street mesh established this convention: `Sidewalk_North` has Y range −1912..−1053, `Sidewalk_South` has +718..+1447. * **Decision**: Adopt it project-wide. Never place a building by assumption; always verify against a street mesh or neighbour. * **Consequences**: Matches Blender's north→+Y after the Y-flip in the `ue_metric_v1` export preset. === ADR-7: Headless Blender via bpy scripts, not MCP * **Context**: Blender skill packs assume an MCP server; the project does not run one. * **Decision**: All Blender work driven by Python scripts in `Tools/`. Skill packs used as method guides (checklists, naming, proportions) only. * **Consequences**: Scripts are version-controlled and repeatable. No live Blender session needed for CI.