Fighting Game · Architecture

Animation Pipeline & Input / Motion Parser

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

10sections16 minread1diagram

On this page

This page covers the two engine modules that sit between a player's fingers and a fighter's body: V2Input, which turns raw device events into a frame-stamped input history and recognizes fighting-game motions from it, and V2Animation, which describes how those motions and the combat results are expressed as poses — skeletons, an AnimBP layer stack, hit-reaction tables, IK, cloth, hair, and facial performance. The dividing line between them is sharp and worth stating up front, because it is also the line between "shipped, tested logic" and "specified, art-pending content": the input/motion side is real, deterministic C++ that runs in matches, while the animation side is largely a validated configuration layer plus a thin set of real notify-state bridge classes, awaiting the authored .uasset content and Unreal plugin wiring it is designed to drive.

Why build it this way? V2's whole combat core is rollback-deterministic (see Combat System — GAS, Frame Data & Determinism and Rollback Netcode & Tag-Team), and a motion parser that reads wall-clock time or floating-point analog history would poison that determinism. So the parser consumes only integer frame numbers and bitmask button state from a fixed-capacity ring buffer, and the same buffer feeds both live ability activation and a byte-exact replay encoder. The animation module, by contrast, never touches gameplay state — every facial, cloth, and gore struct is explicitly flagged bRollbackSafe / bGameplayInert / bCosmeticOnlyRollbackSafe — so presentation can be as expensive as the hardware allows without ever desyncing a match. This page is part of the "Deterministic Combat Core" set; the section hub is ../V2_ARCHITECTURE.md.

What ships, honestly#

Real and tested (runs in matches). The entire input pipeline is genuine, deterministic C++ with a 3,239-line automation spec (V2/ue/Source/V2Tests/Automation/Input.Module.spec.cpp). FV2_MotionParser recognizes 21 distinct fighting-game motions from frame history; FV2InputBufferRing is a real ring buffer that derives press/release edges from held-button masks; FV2InputReplayEncoder produces a byte-exact, CRC-hashed replay stream. This module is a dependency of V2Gameplay, V2UI, V2Netcode, V2Persistence, and V2Modes, and UV2_GameplayAbility consumes the buffer directly — so it is wired into the live game, not a stranded library. The three animation notify-state classes (UV2HitboxNS, UV2ArmorNS, UV2CancelWindowNS) are also real UAnimNotifyState subclasses with working logic that bridges montages into the combat hitbox system.

Specified and validated, but art-pending. Everything else in V2AnimationSK_V2_Fighter_Base and its archetype variants, ABP_V2_Fighter_Base's layer stack, the IK retargeters, the six motion-matching databases, the Mover modes, Chaos Cloth presets, hair grooms, MetaHuman/Live Link facial pipelines — exists as USTRUCT configuration with FSoftObjectPath references and capability booleans, plus builders, validators, and resolution logic, all exercised by Animation.Module.spec.cpp. What is not present: Content/ holds only 8 .uasset files, every one a DA_RegionVariant_* data asset, and zero skeletons, anim blueprints, retargeters, pose-search databases, cloth, or groom binaries — so the soft paths these structs carry (/Game/V2/Animation/...) do not resolve to authored content. Instead, Content/V2/Animation/ ships JSON contract files (SK_V2_Fighter_SkeletonCatalog.json, Mover_V2_CustomModes.json, ChaosCloth_V2_CostumePresets.json, Facial_V2_CapturePipeline.json, …) that mirror the C++ structs as specifications rather than art. And the V2Animation module links only Core/CoreUObject/Engine/GameplayTags/V2Combat/V2Corenot the Mover, PoseSearch, ChaosCloth, or MetaHuman runtimes.

Plugin-ready but not plugin-integrated. V2.uproject does enable the PoseSearch, Mover, and ChaosClothAsset plugins, so the project is staged for that runtime — but no C++ in this module calls into them yet. Treat the animation chapter of the architecture monolith as a build target the config layer encodes, not as shipped runtime. Where the prose below describes a struct's resolution logic (chooser-table lookup, retarget tolerance, cloth weight-class blending) that logic is real and tested; where it names a plugin or an asset, that is the specified-but-pending part.

Two modules, one contract boundary#

V2Input and V2Animation are siblings under V2/ue/Source (the full module list is in Glossary & Module Topology). They do not depend on each other; they meet at the combat layer. Input produces an FV2MotionRecognition; combat (V2Gameplay / V2Combat) decides which move that activates; animation's notify states, riding on the resulting montage, hand hitbox keyframes back to combat. The flow:

flowchart LR DEV[Enhanced Input device event] --> SAMPLE[FV2InputSample\nframe + direction + button masks] SAMPLE --> RING[FV2InputBufferRing\n120-frame ring] RING --> PARSER[FV2_MotionParser\nRecognizeMotion / RecognizeAllMotions] PARSER --> REC[FV2MotionRecognition\nmotion + startFrame + endFrame] REC --> GAS[UV2_GameplayAbility\ninput-buffer window gate] GAS --> MONTAGE[GAS montage on ABP layer] MONTAGE --> NS[UV2HitboxNS / UV2ArmorNS / UV2CancelWindowNS] NS --> COMBAT[V2Combat hitbox keyframes] RING -. byte stream .-> ENC[FV2InputReplayEncoder\nV2IN / V2RM / V2RF] ENC -. deterministic replay .-> RING

The input model: frames, directions, button masks#

Everything starts with FV2InputSample (V2InputTypes.h:4412). It is deliberately tiny and integer-only: an int32 FrameNumber, an EV2InputDirection, and three uint64 button masks — ButtonsHeldMask, ButtonsPressedMask, ButtonsReleasedMask — plus a bFromReplay flag. Buttons are the 32-value EV2InputButton enum (light/medium/heavy punch and kick, grapple, block, sidestep, tag, and the ruleset-specific specials: Drive Impact, Focus Attack, Heat Burst, Rage Art, X-Ray, Blazin', Guard Impact, the four Soul Calibur commands, WWE/UFC contextual actions, …). MakeButtonMask is exactly uint64(1) << static_cast<uint8>(Button) (V2InputTypes.cpp:6241), so up to 64 inputs pack into one word and motion-plus-button tests are bitwise-ands.

Directions use numpad notation encoded so the value is the numpad key (EV2InputDirection: DownBack=1, Down=2, DownForward=3, Back=4, Neutral=5, Forward=6, UpBack=7, Up=8, UpForward=9, with Invalid=0 hidden). This is what makes the motion tables in MotionParser.cpp read like a frame-data notebook.

The ring is FV2InputBufferRing (V2InputBuffer.h, V2InputBuffer.cpp), default capacity 120 frames (two seconds at 60 Hz). The important detail is AddFrameSample: callers pass only the held mask for a frame, and the buffer computes the edges by diffing against the previous sample — ButtonsPressedMask = Held & ~PreviousHeld, ButtonsReleasedMask = PreviousHeld & ~Held (V2InputBuffer.cpp:31). Press and release detection is therefore a property of the buffer, not of the caller, which is why negative-edge and plink recognition can be pure functions of buffer contents. The buffer also exposes WasPressedWithin, WasReleasedWithin, and WasDirectionHeldFor, each scanning newest-to-oldest and breaking once it passes the window — the same windowed-scan shape the parser uses everywhere.

The motion parser#

FV2_MotionParser (V2MotionParser.h, MotionParser.cpp) is a static, stateless recognizer. Its surface is three calls:

  • RecognizeMotion(Buffer, Motion, Config, bFacingRight, TriggerButton) — test the buffer for one specific EV2MotionInput.
  • RecognizeAllMotions(...) — run a priority-ordered sweep of 21 motions and return every match.
  • RecognizeJustFrame(Buffer, RequiredFrame, TriggerButton) — the one frame-exact case, checked separately because it is keyed to a specific frame rather than a window.

EV2MotionInput enumerates 22 motions beyond None: the classics (QCF, QCB, DP, RDP, HCF, HCB, FullCircle/360, DoubleFullCircle/720, ChargeBack, ChargeDown, NegativeEdge, DoubleTapForward/Back/Down/ DownForward, Plink, JustFrame) and the execution-heavy techniques (TigerKnee, KoreanBackdashCancel, WaveDash, InstantWhileStanding, StepCancelSidewalk). RecognizeAllMotions iterates them most-complex-firstDoubleFullCircle before FullCircle, TigerKnee/HCF/HCB before the quarter-circles — so a 720 is never mis-reported as a 360 and a tiger-knee is never swallowed by the bare QCF inside it. This ordering is load-bearing, not cosmetic.

Tunable windows#

All timing comes from FV2InputParserConfig (V2InputTypes.h:4364), and the defaults are the ones the architecture promises: DefaultBufferWindowFrames = 6, TightBufferWindowFrames = 3, LenientBufferWindowFrames = 9. The parser-only windows are MotionHistoryFrames = 45 (the master look-back), ChargeFrames = 60 (a one-second hold at 60 Hz), DoubleTapWindowFrames = 12, BackdashCancelWindowFrames = 8, CrouchDashWindowFrames = 8, InstantWhileStandingWindowFrames = 2, StepCancelWindowFrames = 5, PlinkWindowFrames = 2, and JustFrameWindowFrames = 1. RecognizeMotion widens its collected window adaptively before matching: a 720 looks back 2 × MotionHistoryFrames; a charge looks back at least ChargeFrames + LenientBufferWindowFrames + 1; the Korean backdash and wave dash expand to cover their multi-beat sequences. Collection itself is CollectWindow, which copies only samples within the window and normalizes each direction for facing as it goes.

Worked example — a quarter-circle-forward + light#

Take the canonical buffer from the spec: frame 1 Neutral, frame 2 Down, frame 3 DownForward, frame 4 Forward with Light pressed. RecognizeMotion(QCF):

  1. CollectWindow gathers all four samples (within 45 frames) and mirror- normalizes them — facing right, they pass through unchanged.
  2. MatchDirectionalSequence first runs CollapseDirectionChanges, which drops neutrals and de-duplicates repeats, leaving [Down@2, DownForward@3, Forward@4].
  3. It walks the required sequence {Down, DownForward, Forward} using DirectionMatchesStep (which treats DownForward as satisfying a Forward step, so diagonal sloppiness is forgiven). StartFrame latches to 2, EndFrame to 4.
  4. EndFrame − StartFrame = 2 ≤ MotionHistoryFrames, so the recognition is valid with EndFrame = 4 — exactly what Qcf.EndFrame == 4 asserts. The light button is then matched in combat against the move's input tag, not by the directional parser (except where a motion explicitly carries a trigger, below).

FV2MotionRecognition::IsValid() is the gate every path funnels through: Motion != None && StartFrame != INDEX_NONE && EndFrame != INDEX_NONE && StartFrame <= EndFrame (V2InputTypes.cpp:7745).

Charges, circles, and double-taps#

  • Charges (RecognizeCharge) scan backward for a release sample in the target direction (Forward for ChargeBack, Up for ChargeDown), then count the contiguous held-charge run before it; the run must be ≥ ChargeFrames. The result is flagged bFromCharge. The spec builds 60 Back frames then a Forward+Light on frame 61 and asserts both validity and the charge flag.
  • Circles (RecognizeCircle) accumulate a 4-bit cardinal mask (forward/down/back/up) across collapsed direction changes; one full 0b1111 is a 360, two are a 720. Both then route through AttachTriggerButton, which extends the recognition to the frame a grapple/trigger button lands within DefaultBufferWindowFrames — so a 360-throw is only "complete" when the button confirms.
  • Double-taps (RecognizeDoubleTap) require a neutral reset between taps (the bCanRegisterTap latch), which is how a held direction is distinguished from a deliberate double tap — the spec verifies that two Forwards without an intervening neutral do not register as a dash.

Execution-heavy techniques#

The parser implements the genre's hard inputs as explicit multi-stage state walks, each emitting a descriptive flag on the recognition:

  • Korean backdash cancel (bCancelsBackdashRecovery) — back-tap, neutral, back-tap, crouch-cancel (DownBack/Down), back-tap, all within nested windows. The spec checks StartFrame == 1, EndFrame == 12, and that omitting the crouch cancel fails.
  • Wave dash (bChainsCrouchDash) — the Mishima f, n, d, df crouch-dash chained out of a forward dash.
  • Instant-while-standing (bFromInstantWhileStanding) — a d, df crouch dash, a return to neutral, then a trigger button inside the 2-frame IWS window.
  • Tiger knee (bTigerKneeJumpCancel) — a d, df, f, uf motion with the trigger button required on the final up-forward frame.
  • Step-cancel sidewalk, plink (two different buttons within PlinkWindowFrames, detected by non-overlapping press masks), and negative edge (a release mask within the buffer window) round out the set.

Facing normalization#

A fighting game flips command meaning when sides switch, and V2 handles this once, centrally. V2MirrorDirection (V2InputTypes.cpp:8271) swaps the back/forward component of every diagonal and cardinal (Back↔Forward, DownBack↔DownForward, UpBack↔UpForward; pure up/down/neutral unchanged). CollectWindow applies it to every sample when bFacingRight is false, so the recognizers themselves only ever reason in right-facing space. The spec proves this by feeding a Back-side dash to a left-facing fighter and confirming it recognizes as a forward dash, and the replay test mirrors player two's whole stream through the same function.

Per-fighter and per-move buffer windows#

The architecture's "default 6f, tight 3f, lenient 9f, configurable per fighter / per move" is implemented as FV2InputBufferWindowOverride plus FV2InputParserConfig::ResolveBufferWindowFrames (V2InputTypes.cpp:6200). Each override carries an optional FighterId and MoveId and a preset (Tight/Default/Lenient/Custom). Resolution picks the most specific matching override by a GetSpecificityScore of (FighterId ? 2 : 0) + (MoveId ? 4 : 0) — so a move-level rule beats a fighter-level rule beats the global default, and a Custom preset yields its explicit frame count. The spec nails every branch: Move.Asha.Jab → 3, Move.Asha.Shoryuken → 9, a fighter-wide Fighter.King → 9, a move-only JustFrameLink → 3, and a custom → 12. Validation rejects duplicate (fighter|move) keys and custom presets with a non-positive frame count.

The control schemes (EV2ControlScheme: Classic / Modern / Dynamic / EasyCombo) layer on top. The Classic policy requires full motion fidelity — RequiresMotionInput(QCF/DP/ChargeBack/JustFrame) and forbids simplification, single-button specials, and auto-combos, at DamageScalar = 1.0. The Modern policy exposes single-button specials and emulates motions: a Down + Special resolves to an emulated QCF with command tag Command.Modern.Special.QCF at DamageScalar = 0.8. This is the contract that lets accessibility-minded schemes coexist with execution mains in the same ruleset — see UI / HUD / VR / AR & Accessibility.

Deterministic replay encoder#

FV2InputReplayEncoder (V2InputReplayEncoder.h, V2InputReplayEncoder.cpp) is the "deterministic byte-stream input encoder for replays" the architecture promises, and it is fully real. It defines three little-endian formats, each with a four-byte magic and a version word:

  • V2IN — a bare sample stream (EncodeSamples/DecodeSamples). Each sample is a fixed 29 bytes: 4 (frame) + 1 (direction) + 3 × 8 (held/pressed/ released masks). Decode validates the magic, version, and that the cursor lands exactly on the end of the buffer.
  • V2RM — a full match stream (EncodeMatchStream): metadata (match, ruleset, stage, build/content versions, custom-rules and cosmetic hashes, timestamp, RNG seed, total frames) followed by per-player streams. Crucially it sorts player streams by slot then id, and each player's samples by frame number, before writing — so two semantically-identical match streams produce byte-identical output. The spec proves it: encoding {P2, P1} equals encoding {P1, P2}, and decode requires 2–16 players and re-validates.
  • V2RF — the on-disk replay file (EncodeReplayFile): a rich header (platform, region, tick rate, input/initial/final state hashes, compression codec, server-authoritative flag), a timestamped input-event stream, keyframe
    • delta state snapshots, and a compressed chunk index with per-chunk CRCs.

Determinism is sealed by HashEncodedStream, a FCrc::MemCrc32 over the bytes; because the byte layout is canonical, the hash is a stable fingerprint of a match's inputs. This is the same artifact the replay and competitive-integrity systems consume — see Game Modes — Training & Replay and Online Backbone & Competitive Integrity. Decoded samples are tagged bFromReplay = true so downstream systems can tell a replayed frame from a live one.

How input reaches combat#

The buffer is not a parser toy; UV2_GameplayAbility reads it directly. IsInputBufferWindowSatisfied(InputBuffer, CurrentFrame) (V2GameplayAbility.cpp:173) returns true only when the latest sample is within the ability's InputBufferWindowFrames of the current simulation frame — the frame-accurate "did the player press this recently enough to buffer it" check that makes links and cancels feel correct. The matching tag flow (TryConsumeInputBufferGetQueuedInputTagShouldConsumeInputBufferConsumeQueuedInputTag) runs against the ability system's simulation frame, keeping buffered activation inside the deterministic clock the rollback system re-simulates. The deeper frame-data and cancel-window mechanics live in Combat System — GAS, Frame Data & Determinism.

The animation configuration layer#

V2Animation encodes the architecture's skeleton/AnimBP/retarget/cloth/facial chapter as a tree of USTRUCTs aggregated by UV2AnimationConfigAsset (a UDataAsset whose ValidateConfiguration cross-checks every sub-table). The builders and validators are exercised end-to-end by Animation.Module.spec.cpp. Read this section as a specification with working resolution logic — the math is real, the assets it points at are not yet authored (see the honest-status note above).

  • Skeletons & retarget. BuildDefaultFighterSkeletonDefinitions yields 6 archetypes (Base + Heavyweight, Welterweight, Female, Monster, Child); the base is SK_V2_Fighter_Base, flagged bUE5MannequinCompatible with a required-bone list. FV2IKRetargetProfile defines 5 base→archetype retargeters with round-trip tolerance checks: DoesRoundTripDeltaPass accepts a 1.0 cm joint delta and rejects 1.01 cm — a real numeric gate the spec asserts in both directions.
  • AnimBP layer stack. FV2AnimBPStateLayerDefinition describes the six ordered layers of ABP_V2_Fighter_Base — Locomotion, UpperBodyAction (GAS montage slots FullBody/UpperBody/LowerBody/Hands), HitReaction, Stance, IK (foot / look-at / opponent-face, the last a no-op in free 3D), Additive (emotes/breathing/damage-tics) — with an enforced evaluation order. This is a description of the AnimBP graph for validation and tooling, not a compiled UAnimInstance.
  • Hit-reaction chooser tables. FV2AnimChooserTableDefinition ships four tables (HitReaction, Blocking, Parrying, WakeUp), each a 150-row matrix keyed on hit-reaction tag × stance tag × EV2HitReactionDamageTier (Light/Medium/Heavy/Launcher/Knockdown). FindRow performs the real lookup the spec checks for, e.g., a Soul-Calibur-weapon-stance wallsplat at Heavy tier.
  • Motion matching & Mover. Six FV2MotionMatchingConfig databases (World Tour, Tekken Force, Devil Within, Def Jam street, MyCAREER backstage, SC Chronicles) carry pose-history (8 samples over 0.6 s), foot-phase contact bones, and a trajectory model blending player-input and AI desire. Five FV2MoverModeBindings (Match2D/Ring/Cage/Street/ExplorationFree) and three FV2MoverAdoptionPolicys gate a Character-Movement fallback behind a cert-blocking rollback issue. These map onto the enabled PoseSearch/Mover plugins but are config, not calls.

Cloth, hair, accessories, facial#

FV2ChaosClothPreset carries an LOD chain (LOD0 cloth sim → LOD1 baked vertex animation → LOD2 disabled) and is resolved against FV2ChaosClothWeightClassTuning by ResolveChaosClothPresetForWeightClass — a real blend that, e.g., raises solver damping for heavyweights (the spec asserts > 0.70). Hair splits principal grooms from crowd cards; accessory chains (necklace/earrings/belt-tail/weapon-trinket) are bCosmeticOnlyRollbackSafe rigid-body chains. Facial covers MetaHuman Animator + Live Link Face (the latter bOfflineSolveRequired, bRequiresPerformerConsent, and explicitly not needing runtime Live Link plugins), a 15-entry phoneme→viseme map, six emotion additive layers driven by State.Emotion.* tags, and an audio-driven story-bark lipsync profile that names the cross-monorepo provider @psyche/avatar-lipsync. MK X-Ray gore (FV2ChaosFleshGorePreset) is ruleset-gated (bMortalKombatRulesetOnly), cinematic-gated, and runtime-limited. Every one of these structs is marked rollback-safe or gameplay-inert; the presentation/AV story continues in Presentation, AV & Signature Content.

The real bridge: animation notify states#

The one place V2Animation does real runtime work today is its notify states, which depend on V2Combat and turn an animation timeline into combat data:

  • UV2HitboxNSBuildKeyframeForWindow(Start, End) produces an FV2CombatBoxKeyframe, setting BoxType to Hitbox or Hurtbox from bAttack, clamping the frame window, OR-ing in the Unblockable attack property when bArmorPiercing, and calling Sanitize(). This is how a montage's active frames become the authoritative hit/hurt boxes the combat system reads.
  • UV2ArmorNSAbsorbsPayload / GetReducedDamage implement real armor: it absorbs while ArmorHitCount > 0 unless the payload is armor-piercing and the armor bBreaksOnArmorPiercing, and reduces damage by 1 − DamageReductionScalar (the spec: an 80-damage hit through 0.75 reduction yields 20).
  • UV2CancelWindowNSCanCancelWithTag(tag, bHitConfirmed, bBlocked) gates special/super/Drive/Heat/X-Ray cancels: it requires an exact tag match, then permits on-hit, on-block, or whiff cancels per its flags (whiff off by default).

These three are the genuine seam between the (pending) AnimBP/montage content and the (shipped) combat core.

Failure modes and edge cases#

  • Empty or invalid input. RecognizeMotion returns an invalid recognition immediately if the buffer is empty or Config.IsValidConfig() fails; every recognizer returns a default (invalid) FV2MotionRecognition rather than a partial one.
  • Ring overflow. AddSample evicts oldest-first to hold CapacityFrames; motions older than the (adaptively widened) collection window are simply never seen, which is the intended "you waited too long" behavior.
  • Held vs. tapped. Because press/release edges are diffed in the buffer, a held direction cannot masquerade as a double-tap, and a held button cannot re-trigger negative edge — the edge masks are zero on sustained frames.
  • Replay rejection. Decoders fail closed on wrong magic, wrong version, an out-of-range player count (must be 2–16), a sample count that would overrun the buffer, or a trailing-byte mismatch (Cursor != Bytes.Num()), so a truncated or tampered stream never decodes into a half-valid match.
  • Animation config coherence. UV2AnimationConfigAsset::ValidateConfiguration aggregates the per-table HasRequired* checks; a config missing, say, a weight-class tuning or a chooser table fails validation rather than silently shipping an incomplete fighter.