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 V2Animation —
SK_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/V2Core — not 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:
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 specificEV2MotionInput.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-first — DoubleFullCircle 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):
CollectWindowgathers all four samples (within 45 frames) and mirror- normalizes them — facing right, they pass through unchanged.MatchDirectionalSequencefirst runsCollapseDirectionChanges, which drops neutrals and de-duplicates repeats, leaving[Down@2, DownForward@3, Forward@4].- It walks the required sequence
{Down, DownForward, Forward}usingDirectionMatchesStep(which treatsDownForwardas satisfying aForwardstep, so diagonal sloppiness is forgiven).StartFramelatches to 2,EndFrameto 4. EndFrame − StartFrame = 2 ≤ MotionHistoryFrames, so the recognition is valid withEndFrame = 4— exactly whatQcf.EndFrame == 4asserts. 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 forChargeBack, Up forChargeDown), then count the contiguous held-charge run before it; the run must be≥ ChargeFrames. The result is flaggedbFromCharge. 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 full0b1111is a 360, two are a 720. Both then route throughAttachTriggerButton, which extends the recognition to the frame a grapple/trigger button lands withinDefaultBufferWindowFrames— so a 360-throw is only "complete" when the button confirms. - Double-taps (
RecognizeDoubleTap) require a neutral reset between taps (thebCanRegisterTaplatch), 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 checksStartFrame == 1,EndFrame == 12, and that omitting the crouch cancel fails. - Wave dash (
bChainsCrouchDash) — the Mishimaf, n, d, dfcrouch-dash chained out of a forward dash. - Instant-while-standing (
bFromInstantWhileStanding) — ad, dfcrouch dash, a return to neutral, then a trigger button inside the 2-frame IWS window. - Tiger knee (
bTigerKneeJumpCancel) — ad, df, f, ufmotion 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
(TryConsumeInputBuffer → GetQueuedInputTag → ShouldConsumeInputBuffer →
ConsumeQueuedInputTag) 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.
BuildDefaultFighterSkeletonDefinitionsyields 6 archetypes (Base + Heavyweight, Welterweight, Female, Monster, Child); the base isSK_V2_Fighter_Base, flaggedbUE5MannequinCompatiblewith a required-bone list.FV2IKRetargetProfiledefines 5 base→archetype retargeters with round-trip tolerance checks:DoesRoundTripDeltaPassaccepts a 1.0 cm joint delta and rejects 1.01 cm — a real numeric gate the spec asserts in both directions. - AnimBP layer stack.
FV2AnimBPStateLayerDefinitiondescribes the six ordered layers ofABP_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 compiledUAnimInstance. - Hit-reaction chooser tables.
FV2AnimChooserTableDefinitionships four tables (HitReaction, Blocking, Parrying, WakeUp), each a 150-row matrix keyed on hit-reaction tag × stance tag ×EV2HitReactionDamageTier(Light/Medium/Heavy/Launcher/Knockdown).FindRowperforms the real lookup the spec checks for, e.g., a Soul-Calibur-weapon-stance wallsplat at Heavy tier. - Motion matching & Mover. Six
FV2MotionMatchingConfigdatabases (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. FiveFV2MoverModeBindings (Match2D/Ring/Cage/Street/ExplorationFree) and threeFV2MoverAdoptionPolicys gate a Character-Movement fallback behind a cert-blocking rollback issue. These map onto the enabledPoseSearch/Moverplugins 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:
UV2HitboxNS—BuildKeyframeForWindow(Start, End)produces anFV2CombatBoxKeyframe, settingBoxTypeto Hitbox or Hurtbox frombAttack, clamping the frame window, OR-ing in theUnblockableattack property whenbArmorPiercing, and callingSanitize(). This is how a montage's active frames become the authoritative hit/hurt boxes the combat system reads.UV2ArmorNS—AbsorbsPayload/GetReducedDamageimplement real armor: it absorbs whileArmorHitCount > 0unless the payload is armor-piercing and the armorbBreaksOnArmorPiercing, and reduces damage by1 − DamageReductionScalar(the spec: an 80-damage hit through 0.75 reduction yields 20).UV2CancelWindowNS—CanCancelWithTag(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.
RecognizeMotionreturns an invalid recognition immediately if the buffer is empty orConfig.IsValidConfig()fails; every recognizer returns a default (invalid)FV2MotionRecognitionrather than a partial one. - Ring overflow.
AddSampleevicts oldest-first to holdCapacityFrames; 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::ValidateConfigurationaggregates the per-tableHasRequired*checks; a config missing, say, a weight-class tuning or a chooser table fails validation rather than silently shipping an incomplete fighter.
Related#
- Combat System — GAS, Frame Data & Determinism
— what an
FV2MotionRecognitionactivates, and the hitbox keyframes notify states feed - Rollback Netcode & Tag-Team — why the integer-frame, bitmask input model exists
- Game Modes — Training & Replay and
Online Backbone & Competitive Integrity
— consumers of the
V2RM/V2RFreplay byte streams - UI / HUD / VR / AR & Accessibility — control schemes, hold-to-toggle, one-handed, and QTE-alternative input options
- Presentation, AV & Signature Content — cloth, hair, gore, and facial performance presentation
- Build, Cook, Assets, Data & Production
and
Telemetry, Performance, Testing & Release Gates
— where the pending animation
.uassetcontent and the automation specs fit - Glossary & Module Topology and V2 Product Promise, under the hub ../V2_ARCHITECTURE.md