Fighting Game · Architecture

UI / HUD, VR / AR & Accessibility

A focused page within the Fighting Game Architecture documentation. The full map and every sibling page live in the Architecture hub.

10sections15 minread2diagrams1table

On this page

V2's interface layer is the everything-the-player-touches surface: front-end menus, the in-match HUD, the settings tree, and the accessibility and VR/AR support that sit on top of them. It serves every player — but it is engineered first for the ones who need it most, because console accessibility is cert-critical (Sony, Microsoft, and Nintendo each gate shipping on a required-feature list). What makes V2's interface unusual is how it is built: not as a pile of opaque Blueprint widgets, but as plain-C++ value types that validate themselves, paired with JSON contracts and Python gates. The canonical example is FV2UIAccessibilitySettings (V2/ue/Source/V2UI/Public/V2UITypes.h:9135), an eleven-field struct with a Normalize() method and an IsValidSettings() self-check that every HUD widget and accessibility subsystem reads from. The whole V2UI module is 85 implementation files (V2/ue/Source/V2UI/Private/*.cpp), and a single 3,894-line automation test, V2.UI.Module (FV2UIModuleSpec in V2/ue/Source/V2Tests/Automation/UI.Module.spec.cpp), exercises them with roughly 1,500 TestTrue/TestEqual assertions — HUD-scale clamping, camera-shake normalization, reduced-motion variants, the screen-reader profile, colour-blind palettes, and caption configuration all run headless, with no live match.

Why this shape? Because a HUD that binds directly to the Ability System Component cannot be tested without spinning up a match, and an accessibility claim that cannot be proven offline cannot be presented to a platform cert reviewer. By moving the decisions (what colour, what scale, how much shake) into deterministic structs and the enforcement into validators and gates, V2 makes presentation provable and — crucially — keeps every accommodation on the correct side of the rollback boundary. This page is a companion to the architecture catalogue; the hub is ../V2_ARCHITECTURE.md.

What ships, honestly#

Real and tested. The accessibility subsystems are genuine C++ with domain-specific algorithms and constants, each with a BuildDefault* factory, a self-validating profile, and assertions in the module spec: the colour-blind palette (V2ColorblindSafePalette.cpp, 17 indicator roles × 5 modes), the photosensitivity filter (V2PhotosensitivityFilter.cpp, a 3-flash-per-second cap), visual sound indicators (V2VisualSoundIndicators.cpp, directional off-screen cues with real screen-edge projection), the camera-shake intensity slider (V2CameraShakeIntensitySlider.cpp), the screen-reader bridge (V2ScreenReaderBridge.cpp), reduced motion, high contrast, font options, subtitle rendering, and more. The TypeScript services @v2/iris-accessibility and @v2/psyche-caption-streaming (apps/v2/{iris-accessibility,psyche-caption-streaming}/src/) are real and compose the shared @iris/accessibility and @psyche/caption-streaming libraries. VR/AR ships as a validated data + contract layer (V2/balance/data/vr-ar-support.json plus V2/ue/Content/V2/UI/VRARSupport_V2_Contract.json) with a CI gate. A family of 61 Python checkers (V2/ue/Tools/check-v2-* covering accessibility, HUD, caption, VR, colour-blind, photosensitivity, …) gate these surfaces in CI.

Spec-only / aspirational. The architecture monolith describes a CommonUI 5.5 activation tree under Content/UI/FrontEnd/ with W_HUD_Base, W_Settings_* panels, an IRulesetHUD/IModeHUD injection interface, and .uplugin HUDInjections. Those binary UMG assets do not exist in the tree. What ships under V2/ue/Content/V2/UI/ is JSON contracts (for example RulesetHUDOverlay_V2_Contract.json, HUDAccessibilityPresentation_V2_Contract.json), and the per-ruleset HUD is realised as the spec-driven C++ class UV2RulesetHUDOverlayWidget, not a literal UInterface. Treat the front-end .uasset activation tree, the HUD render budgets (≤0.6 ms/frame), and the named linters check-widget-tree-depth.py / check-a11y-coverage.py (neither is present) as design intent. The monolith's Source/V2Peripherals/ module does not exist either — peripheral/adaptive-controller logic lives in V2Input and V2/balance/data/hardware-peripherals.json.

Provider / platform-gated. Live OS screen-reader announcement (NVDA, JAWS, VoiceOver, TalkBack) is reached through a real, validated tree-export model, but the actual OS-runtime hookup is the integration seam. Per-platform VR rendering paths (PSVR2 foveation/eye-tracking, Quest async-timewarp, Vision Pro passthrough) are described in data and depend on platform SDKs not present in this tree. Platform accessibility certification and VR comfort review are external sign-offs.

The settings model: one struct, normalized, validated#

Everything downstream reads FV2UIAccessibilitySettings. Its fields and bounds are the contract:

Field Type Range / default
HUDScalePercent int32 75–150, default 100
HUDOpacity float 0.0–1.0, default 1.0
bReducedMotion bool default false
CameraShakeIntensityPercent int32 0–100, default 100
bScreenReaderAnnouncements bool default true
bLargeText / TextScale bool / float TextScale 0.5–3.0
PaletteMode EV2UIColorPaletteMode Default · Protanope · Deuteranope · Tritanope · Monochromacy
ContrastMode EV2UIContrastMode Standard · Medium · High
ReadableFontMode EV2UIReadableFontMode Default · AtkinsonHyperlegible · OpenDyslexic
LetterSpacingPercent int32 0–50

Normalize() snaps every field into range, and the module spec proves it: setting HUDScalePercent = 153 and CameraShakeIntensityPercent = 42 yields a clamped 150 and a 40 that rounds to the nearest five (UI.Module.spec.cpp:3589–3603), with GetCameraShakeIntensityMultiplier() resolving to 0.40. The three enums are exact: the palette modes line up one-for-one with the colour-blind catalogue, and the readable-font modes name the two bundled accessibility typefaces (Atkinson Hyperlegible, OpenDyslexic). Two sibling configs extend the model — FV2AccessibilityVisualConfig (Accessibility.Visual.V2, with MaxFlashesPerSecond, high-contrast HUD, photosensitivity-limiter, and opponent/hit-zone outlining flags) and FV2AccessibilitySubtitleCaptionConfig (Accessibility.Captions.V2, with sign- language locales, caption cues, subtitle background opacity, and speaker tags).

The settings screen that edits this lives in V2SettingsScreen.cpp. It is a spec-validated category tree (UV2SettingsScreen::ConfigureFromSettingsSpec), and its predicate methods are what the gates assert against: HasCameraShakeIntensitySlider() checks the row is a 0–100/step-5/default-100 slider; HasReducedMotionModeToggle() checks the description literally mentions "screen shake," "particle effects," and "camera transitions"; HasFontAccessibilityOptions() checks the readable-font choice exposes Default / AtkinsonHyperlegible / OpenDyslexic alongside a 0–50 letter-spacing slider; and HasOneHandedControls() / HasControlsRebind() cover motor remapping. Privacy rows require confirmation (HasPrivacySafeguards()), and locale selection covers OS-auto-detect plus an explicit text/voice language split.

The determinism boundary — the load-bearing idea#

The single most important architectural rule here: accessibility accommodations are presentation-only and must never alter the deterministic simulation. V2 is a rollback-netcode fighting game (see ./rollback-netcode-and-tag-team.md and ./combat-system-gas-frame-data-and-determinism.md), so a visual setting that changed even one frame of hit-stop would desync a match. The code enforces the wall explicitly.

flowchart TD Player[Player setting] --> Norm[FV2UIAccessibilitySettings::Normalize] Norm --> Shake[CameraShake resolution<br/>scale, never amplify] Norm --> Reduced[ReducedMotion state] Norm --> Palette[Colorblind palette resolve] Norm --> Photo[Photosensitivity filter ≤3 fps] Norm --> Caption[Caption session plan] Shake --> Present[Presentation / HUD render] Reduced --> Present Palette --> Present Photo --> Present Caption --> Present Sim[GAS + rollback simulation<br/>frame data, hit-stop] -. authored intensity only .-> Shake Present -. NEVER writes .-> Sim

The camera-shake slider is the clearest proof. ResolveShakeIntensity() takes a normalized settings struct, an authored intensity, and a source (CombatHit, EnvironmentalImpact, VehicleImpact, Cinematic, UIImpact), and returns ResolvedIntensity = AuthoredIntensity × multiplier. The validator FV2CameraShakeIntensityResolution::IsValidResolution() then refuses any result where ResolvedIntensity > AuthoredIntensity (it may only attenuate, never amplify), where 0% fails to fully disable shake, or where the resolution is not bGameplayDeterministic and bDoesNotAlterFrameData. The profile's bDoesNotAffectHitStopFrameData flag feeds those booleans, so the type system records that the slider scales the camera but leaves hit-stop frame data untouched. The slider is also marked bTournamentSafe.

The same principle governs EV2AccessibilityRankedEligibility (RankedAllowed / RankedDisabled). Visual-only accommodations — reduced motion, camera shake, colour-blind palette, high contrast — do not flip ranked eligibility. Motor assist macros that change gameplay outcomes (auto-combo, motion-shortcut, parry-window-extend, block-assist) do flip it, routing the player into assist-eligible queues. The interface layer therefore carries the policy that separates "I changed how the game looks" from "I changed how the game plays."

In-match HUD and per-ruleset injection#

The HUD base class is UV2HUDWidget (declared in V2ActivatableScreen.h); every overlay derives from it. Per-ruleset meters are injected through UV2RulesetHUDOverlayWidget::ConfigureFromRulesetHUDOverlaySpec(), which takes a FV2RulesetHUDOverlaySpec keyed by EV2ShaktiRulesetId (the Shakti fighting-ruleset identity — see ./combat-system-gas-frame-data-and-determinism.md) and a list of FV2RulesetHUDMeterSpec/EV2RulesetHUDMeterKind entries. The widget exposes HasMeterKind(), IsReducedMotionSafe(), and IsSpectatorSafe() — the last two are how a ruleset overlay proves it honours the reduced-motion accommodation and is safe to show in spectator/broadcast contexts. The module spec asserts RulesetHUDWidget->IsReducedMotionSafe() directly (UI.Module.spec.cpp:646).

A worked example is the MMA/UFC body-damage overlay. UV2MMABodyDamageIndicatorOverlay (a UV2HUDWidget) is configured from FV2MMABodyDamageIndicatorSpec; on a valid spec it calls ConfigureFromHUDEntry(OverlaySpec.ToHUDWidgetEntry()) and then surfaces GetVisualZoneTag(), GetVisualIntensity(), IsLimbBruisingVisible(), and IsBodyRednessVisible(). The overlay refuses to configure from an invalid spec (ConfigureFromOverlaySpec returns false and resets), which is the fail-loud pattern repeated across the module — a HUD widget either holds a validated spec or holds nothing.

HUD scaling and reduced motion flow through UV2UISubsystem (the LocalPlayer subsystem): SetHUDScalePercent(70) clamps to 75, SetHUDReducedMotionEnabled(true) returns a presentation with bReducedMotionVariantActive, and SetHUDHighContrastEnabled(false) toggles the high-contrast theme (UI.Module.spec.cpp:3442–3516). The HUD-scale slider is documented as 75–150 % in 5 % steps with an 18 px @ 1080p minimum-readable-size floor; the clamping is real and tested, while the design-time min-readable linter named in the monolith is not a shipped file.

Visual accessibility#

Colour-blind-safe palette. FV2ColorblindSafePalette::BuildDefaultCatalog() ships 17 indicator roles — Health, Guard, Stamina, Super, Warning, Danger, Success, Objective, QuestPrimary, QuestOptional, Interact, Focus, three Ping qualities, TeamAlly, TeamEnemy — and each role carries five mode-specific colours (Default plus four vision types) and a non-colour NonColorShapeTokenId and TextFallbackTokenId. Validation is strict: every colour is finite/normalized RGBA, the catalogue must declare bNoColorOnlyState, every entry must meet a MinimumContrastRatio4.5:1 (WCAG AA), and ProvidesModeSpecificAlternatives rejects any entry whose protan/deutan/tritan/achromatic colour merely repeats the default. The actual RGB values are the recognised Wong colour-blind-safe set (the 0.00, 0.45, 0.70 blue and 0.90, 0.62, 0.00 orange recur deliberately across roles). Because each role also has a shape and a text token, no V2 state is ever encoded by colour alone — the architecture's "widgets pull semantic colour, never raw hex" rule made structural.

Photosensitivity filter. FV2PhotosensitivityFilter enforces a hard cap of 3 flashes per second — the three-flash threshold from WCAG 2.3.1 / the Harding test — with per-target rules for RapidFlashing, StrobeEffects, HighContrastFlicker, LightningVFX, and the KO X-Ray. Each rule derives MinFlashIntervalSeconds = 1 / MaxFlashesPerSecond and is rejected if it exceeds 3 fps or drops below a 0.33 s interval. ApplyFilter() clamps an authored flash rate down to the cap and scales luminance/contrast deltas by per-target factors (e.g. strobe luminance × 0.25); the profile's own validator runs a self-test that an authored 12 fps strobe is filtered to ≤3 fps. The filter bAppliesBeforePostProcess and bRequiresContentReviewTag, and stays bGameplayDeterministic.

Reduced motion, high contrast, fonts, text scaling. Each is a real subsystem with the same profile pattern: V2ReducedMotionMode.cpp (Accessibility.ReducedMotionMode.V2, required-target coverage), V2HighContrastMode.cpp (459 lines), V2FontAccessibilityOptions.cpp (438 lines, the Atkinson/OpenDyslexic + letter-spacing model), V2TextScaling.cpp, V2ScreenMagnification.cpp, and V2ColorblindSimulation.cpp (a developer-side simulation shader for authoring review). Reduced motion is a visual-only accommodation: the architecture is explicit that enabling it does not disable ranked play.

Auditory accessibility#

Visual sound indicators. FV2VisualSoundIndicators turns off-screen audio into on-screen direction. Three cue types map to fixed shapes — Gunfire → Chevron, Footstep → Footprint, EnvironmentalHazard → WarningDiamond — and the geometry is real: ProjectDirectionToScreenEdge() normalizes a relative direction, scales it to the safe-area edge (0.5 − padding), and clamps it into the screen; BuildRenderItem() then sets rotation from atan2, scales the icon with loudness and proximity (0.75 + loudness·0.35 + (1−distanceRatio)·0.20, clamped 0.75–1.30), and fades opacity with distance. Every rule must set bWorksWhenAudioMuted, bNonColorShapeCoding, a distance ring, and a threat level; pulse rate is bounded 0.5–6 Hz and the profile shows at most 6 simultaneous indicators. This is the "audio cue → visual cue" promise made concrete and deterministic.

Caption streaming. buildV2PsycheCaptionStreamingSurface() (apps/v2/psyche-caption-streaming/src/psyche-caption-streaming.ts) plans a real-time caption session over five surfacesin-match-hud, spectator-overlay, replay-viewer, companion-second-screen, broadcast-observer — each with its own render target, position, alignment, priority, and line cap (SURFACE_RENDERING). It builds one channel per (surface × target language), capped at the underlying engine's maxChannels, registers speakers for attribution, and emits a V2PsycheCaptionSessionPlan. The netcode-safety guarantees are explicit fields: offRollback: true, mayInfluenceRollback: false, and deterministicImpact: 'none' with the policy string off-rollback-caption-ui-only — captions read presentation state and never the deterministic input stream. Validation is fail-loud rather than silent: buildReasonCodes() returns a non-empty list (which the cert gate treats as a block) when matchId is missing, no target language is supplied, accessibility mode is disabled, the in-match-HUD surface is absent, or no speaker is rostered.

flowchart LR Match[Match audio + speakers] --> Plan[buildV2PsycheCaptionStreamingSurface] Plan --> HUD[in-match-hud] Plan --> Spec[spectator-overlay] Plan --> Replay[replay-viewer transcript] Plan --> Comp[companion-second-screen] Plan --> Bcast[broadcast-observer] Plan -. offRollback · deterministicImpact none .-> Safe[(no gameplay input)]

@v2/psyche-caption-streaming composes the shared @psyche/caption-streaming runtime (libs/psyche/caption-streaming); audio-event duplication for hearing-impaired play is handled in V2AudioFeedbackDuplication.cpp, and the closed-caption authoring workflow in V2ClosedCaptionAuthoringTool.cpp.

Screen-reader bridge and the Iris service#

FV2ScreenReaderBridge::BuildDefaultTree() builds an exportable accessibility tree (Accessibility.ScreenReaderBridge.V2): a root Window node, a MainMenu list with Play and "Accessibility settings" buttons, a QuestTracker list whose active objective is a live region, a Notifications live region, and help text. Nodes carry stable ids, roles, labels, hints, focus order, and action metadata, and the validator enforces structural integrity — a root must be a Window, every non-root node must reference a registered parent, and the tree must cover the required roles (Window, Button, Text, List, ListItem, LiveRegion). Platform bindings are declared for three targets: UIAutomation (the Windows API NVDA and JAWS consume), NSAccessibility (macOS/iOS VoiceOver), and TalkBack (Android). It also converts quest objectives into nodes (BuildNodeFromQuestDescriptor) so the open-world quest UI is screen-reader addressable (see ./open-world-coop-and-special-modes.md). This is an honest model-and-binding layer: the tree, roles, and focus order are real and validated; the actual OS-runtime announcement is the platform seam the bindings target.

The TypeScript service @v2/iris-accessibility (buildV2IrisAccessibilitySurface) is the cross-client owner. It composes @iris/accessibility (dyslexia-friendly typography, cognitive-load reduction, screen-reader bridge, native-UE hooks) and the caption engine from @psyche/caption-streaming, and returns a surface that declares controlsScreenReaderBridge, controlsDyslexiaCognitiveAssistiveUi, companionAndMainClientBridge, nativeUeAccessibilityHooks, certGateBlocksRelease: true, and mayInfluenceRollback: false. Its event topics include v2.accessibility.cert-gate.failed — accessibility failures are first-class events, and the cert gate is wired to block release. Both services re-export through apps/v2/{iris-accessibility,psyche-caption-streaming}/src/index.ts.

VR / AR#

VR/AR ships as a validated catalogue, not a runtime module — there is no V2VR C++ module in V2/ue/Source. The source of truth is V2/balance/data/vr-ar-support.json (schema v2.ui.vr-ar-support-data.v1), mirrored by V2/ue/Content/V2/UI/VRARSupport_V2_Contract.json and validated by V2/ue/Tools/check-v2-vr-ar-support.py (plus check-v2-vr-camera-rig.py). The data describes:

  • Platforms. PSVR2 (4K-per-eye, 120 Hz, foveated rendering, eye tracking, DualSense Edge), Meta Quest 3 (native VR, hand tracking, per-frame async-timewarp, cross-buy with PC), Apple Vision Pro (mixed reality, passthrough, gaze-and-pinch, spatial Atmos audio), and PC VR (SteamVR / Oculus Rift S / Windows MR with per-platform render scale).
  • Comfort. Three comfort levels, turning vignette, snap-turn, per-player comfort selection, minimal HUD, virtual gauge glances, and physical 3D UI objects — the VR analogue of the flat HUD.
  • Mixed reality. Vehicles that drive on the real-world floor, fighting in a real room, an AR spectator companion that can track a friend's match on a TV or phone screen.
  • Cross-play. Non-ranked opt-in VR-vs-non-VR, with ranked separating VR pools (the input-fairness rule that also governs wheel-vs-gamepad — see ./online-backbone-and-competitive-integrity.md).
  • Certification. Per-platform comfort review plus epilepsy and motion-sickness compliance for Sony VR TRC, the Meta Quest Store, and the Apple Vision Pro App Store.
  • VR accessibility. Seated mode, a dedicated wheelchair-seated mode, one-controller mode, voice-activated menus, and customizable 3D-space subtitle positioning.

The ruleset-compatibility block marks racing and (toggleable) fighting as VR-eligible with motion-sickness considerations and per-family camera modes, which is how VR connects to ./racing-and-vehicle-architecture.md. The gate cross-checks the contract, the balance data, an engineering doc, and the CI workflow, and requires owner modules {V2UI, V2OnlineServices, V2Audio, V2Racing, V2Combat} — so even though there is no VR runtime in this tree, the support matrix is enforced rather than asserted.

Testing and gates#

Two layers verify the interface. The UE automation test V2.UI.Module runs headless in the editor context and asserts behaviour, not existence — scale clamping resolves to specific integers, camera-shake 42 → 40 with multiplier 0.40, reduced-motion produces an active variant, the HUD accessibility profile binds the screen-reader tree id Accessibility.ScreenReaderBridge.V2 and the HUD.Theme.HighContrast theme, and the caption/subtitle config validates across subtitles, closed captions, sign language, audio description, and opacity. The second layer is the 61-strong check-v2-* Python gate family (e.g. check-v2-colorblind-safe-palette.py, check-v2-photosensitivity-filter.py, check-v2-psyche-caption-streaming.py, check-v2-iris-accessibility.py, check-v2-vr-ar-support.py), which cross-reference the C++ owners, the JSON contracts, the balance data, and the docs. For how these gates fold into the release pipeline, see ./telemetry-performance-testing-and-release-gates.md; for the cert-blocking compliance view, see ./security-compliance-and-sister-monorepo-integration.md.