Skip to content

Combat Animation

The Combat Animation System manages montage selection and playback through UMontageManagerComponent. Montages are stored in weapon DataTables and looked up by gameplay tags. Animation notifies embedded in montages drive combat timing for hits, input windows, and effects.

Weapon animation is resolved along two orthogonal axes: WeaponHand (which arms hold equipment) selects the montage table and drives ABP arm-layer blending; WeaponSet (the weapon combination) drives the ALS locomotion overlay. See Two-Axis Animation Resolution.

Architecture

+------------------------+
|  Ability Activation    |
+------------------------+
           |
           v
+------------------------+
| IMontageManagerInterface|
| ::GetMontageByTag()    |
+------------------------+
           |
           v
+------------------------+
| UMontageManagerComponent|
+------------------------+
           |
           v
+------------------------+
|  DataTable Lookup      |
|  (FMontageAction rows) |
+------------------------+
| Row: LightAttack_1     |
|   ActionTag: InputTag.LMB
|   Montages: [Sword_L1] |
| Row: LightAttack_2     |
|   ActionTag: InputTag.LMB
|   Montages: [Sword_L2] |
+------------------------+
           |
           v
+------------------------+
| UAnimMontage returned  |
+------------------------+
           |
           v
+------------------------+
| UAbilityTask_PlayMontageAndWait |
+------------------------+
           |
           v
+-----------------------------+
|    Montage Playback         |
+-----------------------------+
|  Notify: ComboHitWindow     |---> Hit Tracing
|  Notify: ComboInputWindow   |---> Input Acceptance
|  Notify: CombatEffect       |---> VFX/Audio/Camera Impulse
|  Notify: WeaponTrail        |---> Trail Effects
|  Notify: IsInvincibleState  |---> I-Frames
+-----------------------------+

Why This Design?

Tag-Based Lookup

Abilities request montages by FGameplayTag rather than direct asset references. This allows: - Weapon swapping without ability changes - Data-driven animation assignment - Random selection from montage variants

DataTable Storage

Each weapon type has its own DataTable containing all its montages. This keeps data organized and allows weapon artists to iterate without touching code.

Notify-Driven Timing

All combat timing comes from animation notifies rather than hardcoded durations. This means: - Animators control game feel directly - Different montages can have different timing - Playback rate changes don't break combat windows

Two-Axis Animation Resolution

Weapon animation is split into two independent concerns rather than one "weapon type" concept. This lets the posture/overlay (driven by the weapon combination) vary independently from the montage moveset and arm blending (driven by which hands are occupied).

Axis What it represents Drives Source of truth
WeaponHand Which arms hold equipment (solo, shield-only, sword+shield, dual-wield) ABP arm-layer blending; montage-table lookup; per-hand combo/charge config Resolved at runtime; tags WeaponHand.Right/Left/Both/RightWithShield/ShieldOnly
WeaponSet The weapon combination (1H solo, 1H+shield, dual-wield, 2H, unarmed) ALS locomotion/idle overlay (and block overlay) UEquipmentComponent resolves a WeaponSet.* tag; UWeaponSetConfigDataAsset maps it to overlay tags
        Equipment changes (main hand / off hand)
                        |
        +---------------+----------------+
        |                                |
        v                                v
  WeaponSet axis                   WeaponHand axis
  (weapon combination)             (which arms occupied)
        |                                |
        v                                v
  UEquipmentComponent              ICombatDataProvider
  ::UpdateWeaponSetOnCharacter()   ::GetCurrentWeaponHand()
        |                                |
        v                                v
  UWeaponSetConfigDataAsset         FWeaponFragment
  Find(WeaponSet) -> OverlayMode    ::GetMontagesForHand(WeaponHand)
        |                                |  ::GetComboConfigForHand(WeaponHand)
        v                                |  ::GetChargeConfigForHand(WeaponHand)
  AlsCharacter::SetOverlayMode()         v
  (locomotion posture)             Montage table + combo/charge tuning
                                   (moveset) + ABP arm-layer blend

Why Two Axes?

  • Shield disambiguation: shield-only and sword+shield share the WeaponSet_1H_Shield overlay but differ in arm blending — distinguished by the WeaponHand.ShieldOnly vs WeaponHand.RightWithShield tags.
  • Centralized overlay mapping: overlay activation lives in UEquipmentComponent + a single UWeaponSetConfigDataAsset, instead of each weapon fragment activating its own overlay. Adding/retuning a weapon set is a data edit, not per-weapon wiring.
  • Per-hand-context moveset: a versatile weapon can ship different montages, combo counts, and charge levels depending on whether it is solo, paired with a shield, or dual-wielded.
  • Single resolution path: tooltips and runtime both call GetCurrentWeaponHand(), so displayed and actual behavior cannot diverge.

Known issue: WeaponHand may not replicate correctly for left-hand weapons on clients (off-hand resolution can be wrong remotely).

WeaponHand-Keyed Montage Tables

FWeaponFragment stores montage tables as TMap<WeaponHand tag, UDataTable*> (MontageTables). A weapon only defines the entries it needs — a 2H hammer has one entry; a versatile sword may have several (right, shield, dual-wield). Lookup falls back to the first entry when the current hand context has no explicit mapping. Combo and charge configuration are stored the same way (TMap<WeaponHand, FComboConfiguration> / TMap<WeaponHand, FChargeConfiguration>) directly on the fragment — the standalone ComboConfiguration/ChargeConfiguration data assets are no longer the source of truth.

Montage Lookup Flow

This page documents the weapon-table route only: weapon attacks, combos, charges, hit reacts, dodges, and enemy abilities. Skills own their montage directly (SkillMontages on the ability CDO) and never reach the lookup below. See Montage Resolution.

How Abilities Get Montages

This is the tag-keyed table route: weapon attacks, charge, hit reacts, dodge, and every enemy ability. Player skills do not use it — they read their montage straight off the ability CDO's SkillMontages. See Montage Resolution.

Charge is split across both homes. The looping wind-up plays as an ability montage on UChargeWindupAbility (predicted through the stock PlayMontageAndWait pipeline, so ending the ability stops exactly the instance it started). Only the released charged swing goes through the tag-keyed table, via UChargeComponent::GetChargedAttackMontage() — keyed by the charge config's ChargeMontageTag, falling back to the weapon's heavy attack montage.

Ability needs attack montage
           |
           v
GetMontageForActionTag(InputTag.LMB, CombatMontageTable)
           |
           v
IMontageManagerInterface::GetMontageByTag()
           |
           v
UMontageManagerComponent::GetMontageByTag()
           |
           +---> Iterate DataTable rows
           +---> Find row where ActionTag matches
           +---> Select montage by index (or random)
           |
           v
Return UAnimMontage*

Montage Index Selection

MontageIndex Behavior
0, 1, 2... Select specific montage from array
INDEX_NONE (-1) Random selection from array

Combo systems typically use the combo count as the index, giving each hit its own montage.

DataTable Structure

FMontageAction Row Format

Column Type Purpose
ActionTag FGameplayTag Tag to match during lookup
Montages TArray Array of montage variants

Example DataTable (Sword)

Row Name ActionTag Montages
LightAttack_1 InputTag.LMB [Sword_Light_01, Sword_Light_01_Alt]
LightAttack_2 InputTag.LMB [Sword_Light_02]
LightAttack_3 InputTag.LMB [Sword_Light_03]
HeavyAttack_1 InputTag.RMB [Sword_Heavy_01]
ChargedAttack Input.Attack.Charged [Sword_Charged_01]
HitReact_Front Effects.HitFront [HitReact_Front_01]
HitReact_Back Effects.HitBack [HitReact_Back_01]

Weapon-Based Montage Selection

How PlayerCombatComponent Gets Montages

Weapon equipped
       |
       v
OnWeaponChanged() delegate fires
       |
       v
UPlayerCombatComponent::OnRightWeaponChanged()
       |
       v
UpdateCombatMontages()
       |
       +---> Get active weapon (right, then left)
       +---> Get FWeaponFragment from ItemManifest
       +---> Resolve current WeaponHand (GetCurrentWeaponHand)
       +---> GetMontagesForHand(WeaponHand) on the fragment
       +---> SetCombatMontage(DataTable)
       |
       v
Montages now available for lookup

The PlayerCombatComponent owns this flow - it reacts to weapon changes rather than being told which montages to use. This decouples the equipment system from montage management. The selected table is the entry in the fragment's MontageTables map matching the current WeaponHand, falling back to the first entry.

Priority Order

  1. Right-hand weapon fragment (main hand), table chosen by current WeaponHand
  2. Left-hand weapon fragment (off-hand)
  3. nullptr if unarmed (abilities use default/unarmed montages)

Animation Notifies

Combat montages contain these notify types:

Combat Timing Notifies

Notify Type Purpose
ComboHitWindow NotifyState Enable/disable hit tracing
ComboInputWindowState NotifyState Accept combo input
ComboInputBufferWindowState NotifyState Buffer input for next attack
ComboComplete Notify (instant) Reset the chain immediately (authority only)

Effect Notifies

Notify Type Purpose
CombatEffect Notify Trigger VFX/audio/camera impulse at specific frame
WeaponTrail NotifyState Spawn and update trail effect

State Notifies

Notify Type Purpose
IsInvincibleState NotifyState Grant i-frames via GAS tag
BlockAbilityInput NotifyState Prevent new ability activation
MovementAnimationCancel NotifyState Allow movement to cancel anim

Action Rotation and the ALS Rotation Lock

An attack steers the character to face its target through UTargetingComponent's action rotation override, which drives actor yaw directly. ALS drives yaw too, so the two must never run at once — AAlsCharacter::RefreshGroundedRotation early-returns while LocomotionAction is set, and that tag (Als.LocomotionAction.Attacking) is the only thing standing between them.

The tag is a single slot with no owner identity, so it cannot be shared. While an action rotation is live, UTargetingComponent owns it: it sets the tag on start and re-asserts it every tick. A single set is not enough, because Als Set Locomotion Action notify states end at the end of their montage's blend-out — during a combo that lands after the next attack has already claimed the tag, and ALS's equality check cannot tell the two claims apart, so the finished attack clears a lock that is still in use.

Consequences worth knowing before you author or debug:

  • The montage notify is advisory. Several player attack montages carry no Als Set Locomotion Action notify at all and behave identically. Do not add one expecting it to control facing, and do not remove one expecting facing to break.
  • The component backs off only for a different locomotion action — mantle, roll, ragdoll, get-up. Those outrank an attack's facing and carry their own rotation handling. Every release is equality-guarded, so no owner ever clears a tag it did not set.
  • The ability opens the override from the payload, and must release it. UMeleeAbility::ActivateAbility calls UPlayerCombatComponent::OpenAttackRotation(Intent) — each machine opens its own override toward the yaw and target the swing payload carried — and releases the returned handle in EndAbility. The handle is what stops a finished combo step from cancelling the step that replaced it. UDodgeAbility does the same; a new ability that steers the character must too.
  • A press that activates nothing has nothing to unwind. The input path is resolve-only: it builds the intent and fires the gameplay event, so if hit react, stun or death beat the input, no override was ever opened. The activation counts those calls return are not a rotation-unwinding mechanism.
  • Each machine owns its own override; none of this state replicates. The interpolation runs from wherever that character actually is, so the start rotation is necessarily local, and LocomotionAction is per-machine in ALS too. Opens and releases therefore cross the wire as RPCs in both directions — including the ones the tick decides on. A replicated flag over local interpolation state is the trap here: the far end overwrites a machine that has already released, and it resumes with a stale start and a finished alpha, snapping straight to the stored target. Sub-degree in most attacks, violent after a break-away, and it looks like client and server fighting over rotation. Simulated proxies take rotation from movement replication and sit this out.

Symptom of getting this wrong: yaw oscillating at zero velocity after an attack, which reads as a fast idle because the ABP's lean and turn-in-place react to the jitter.

Combat Feel / Impact Notifies

These four notifies are pure game feel — they sell the weight of a swing (hyper-armor, the attacker's step-in, a camera lean, a deeper freeze-frame). They are authored on the montage by the animator and, unlike knockback distance, do not scale with item tier. See Where Per-Attack Feel Lives.

Notify Type Purpose
UAnimNotifyState_HyperArmor NotifyState Server-only: adds the loose State.HyperArmor tag over the window so the attacker shrugs off flinch + knockback but not poise. Author over active frames only; leave wind-up/recovery unarmored so trading hits stays fair.
UAnimNotify_AttackFollowThrough Notify (instant) Additive forward ConstantForce root-motion push on the attacker after contact, so they press in behind a launched enemy. Distance ~60uu, Duration ~0.18s. Net-guarded to IsLocallyControlled() || HasAuthority(); simulated proxies skip it and receive the resulting movement via replication.
UAnimNotify_CameraSwingGesture Notify (instant) Local-player-only camera boom impulse via UDynamicCameraComponent::AddCameraImpulse. LocalDirection is projected to the ground plane, Strength ~25uu. Author across the windup/strike, NOT the contact frame (already saturated by the impact impulse + hitstop). Only manifests in PIE — Persona preview has no gameplay camera.
UAnimNotify_HitStopWeight Notify (instant) Stamps a per-attack hitstop multiplier on the owner's UCombatComponent (SetHitStopIntensity, Weight default 1.0). Consumed by the next landed hit to scale its freeze-frame (a finisher freezes harder than a poke). Place at the start of the hit-detection window so it is set before the trace fires; cleared on a whiff by ComboHitWindow NotifyEnd.

Where Per-Attack Feel Lives

Impact "weight" is split between two homes by what kind of number it is. Balance numbers (which scale with the item) live in data on the weapon fragment; feel/timing numbers (which do not) live as notifies on the montage. Keeping them separate means an animator can retune the feel of a swing without touching balance, and a designer can rebalance an item without re-authoring animation.

Lever Home Owner Scales with item tier?
Knockback distance Weapon fragment ComboConfig (per-step arrays, indexed by combo step) Designer / balance Yes
Follow-through (step-in push) UAnimNotify_AttackFollowThrough on the montage Animator / feel No
Hit-stop weight UAnimNotify_HitStopWeight on the montage Animator / feel No
Camera swing gesture UAnimNotify_CameraSwingGesture on the montage Animator / feel No

Authoring caveats: - Motion-warp notifies must target the name AttackTarget. That is the name the combat component registers the warp target under, and it is set before the montage starts so the notify sees a valid target at its NotifyBegin. A notify authored against any other target name warps nowhere and fails silently — a recent pass found 47 montages whose notifies carried a mismatched name, so every one of their swings played with dead motion warping. - Hyper-armor: arm only the active frames, never wind-up or recovery — an always-armored attack is unfair to trade against. - Follow-through needs its net guard; without it the push double-applies on simulated proxies (it is additive on top of replicated movement). - Camera swing must not sit on the contact frame — that frame is already saturated by the impact camera impulse + hitstop, so the lean should read as anticipation/follow-through across the windup/strike instead. - Hit-react anims must be root-locked (in-place / root motion off) so their baked root motion does not fight the enemy's knockback root-motion source.

Notify Timeline Example

Attack Montage Timeline:
|----Startup----|-----Active-----|----Recovery----|
     0.0s           0.2s              0.5s         0.8s
      |              |                 |             |
      |              |<--HitWindow-->  |             |
      |              0.2s         0.4s |             |
      |                                |             |
      |                   |<--InputWindow---------->|
      |                   0.35s                 0.75s|
      |                                              |
      |<-----------BufferWindow-------------------->|
      0.1s                                       0.7s|
      |                                              |
      |        CombatEffect (swing audio)            |
      |             0.15s                            |
      |                                              |
      |<----WeaponTrail---->                         |
      0.1s              0.45s                        |

Hit React Montages

Directional hit reactions use the hit direction to select the correct montage:

Hit received with direction
           |
           v
UCombatComponent::GetDirectionalHit()
           |
           v
IMontageManagerInterface::GetHitReactMontage(Direction)
           |
           +---> Map direction to tag:
           |       Front -> Effects.HitFront
           |       Back  -> Effects.HitBack
           |       Left  -> Effects.HitLeft
           |       Right -> Effects.HitRight
           |
           v
GetMontageByTag(DirectionTag, 0, DefaultMontages)
           |
           v
Return appropriate hit react montage

Key Contracts

IMontageManagerInterface

Method Parameters Returns Purpose
GetMontageByTag() ActionTag, Index, DataTable UAnimMontage* Look up montage
GetHitReactMontage() HitDirection UAnimMontage* Get directional hit react
GetDefaultMontageTable() - UDataTable* Get character's default table

ICombatDataProvider (animation-relevant)

Method Parameters Returns Purpose
GetCurrentWeaponHand() - FGameplayTag Shared WeaponHand resolution used by montage/combo/charge lookup and tooltips

FWeaponFragment (animation-relevant)

Method Parameters Returns Purpose
GetMontagesForHand() WeaponHand tag UDataTable* Montage table for the hand context (first-entry fallback)
GetComboConfigForHand() WeaponHand tag FComboConfiguration* Per-hand combo tuning
GetChargeConfigForHand() WeaponHand tag FChargeConfiguration* Per-hand charge tuning

UWeaponSetConfigDataAsset

Member Type Purpose
WeaponSetConfigs TMap WeaponSet tag → overlay config
Find() FGameplayTag → FWeaponSetConfig* Resolve overlay/block-overlay tags for a weapon set

UMontageManagerComponent Properties

Property Type Purpose
DefaultMontages UDataTable* Fallback montage table
OwnerCharacter AEternalCharacter* Cached owner reference

Combat Effect Notify Properties

Property Type Purpose
EffectTag FGameplayTag Which effect to trigger
SocketName FName Spawn location socket
LocationOffset FVector Offset from socket
IntensityMultiplier float Effect intensity scale
bTriggerCameraShake bool Trigger camera shake
bTriggerTimeEffects bool Trigger hit stop/slow-mo
DirectionalImpulseStrength float Camera push in owner's forward direction (0 = disabled)
ZoomImpulseStrength float Camera zoom push-in strength (0 = disabled)

Source References

Concept File Line
UMontageManagerComponent class Source/ProjectEternal/Public/Combat/Components/MontageManagerComponent.h 20
IMontageManagerInterface Source/ProjectEternal/Public/Combat/Components/MontageManagerComponent.h 10
FMontageAction struct Source/ProjectEternal/Public/Data/MontageAction.h 10
GetMontageByTag() Source/ProjectEternal/Private/Combat/Components/MontageManagerComponent.cpp 30
UpdateCombatMontages() Source/ProjectEternal/Private/Combat/Components/PlayerCombatComponent.cpp 279
OpenAttackRotation() / warp target setup Source/ProjectEternal/Private/Combat/Components/PlayerCombatComponent.cpp 607-700
Ability-side rotation open / release Source/ProjectEternal/Private/Abilities/MeleeAbility.cpp 133, 637
Charged swing montage lookup Source/ProjectEternal/Private/Combat/Components/ChargeComponent.cpp 225
Charge wind-up montage (ability-owned) Source/ProjectEternal/Public/Abilities/ChargeWindupAbility.h 26
ComboHitWindow notify Source/ProjectEternal/Public/Combat/AnimNotifies/ComboAnimNotifies.h 10
CombatEffect notify Source/ProjectEternal/Public/Combat/AnimNotifies/AnimNotify_CombatEffect.h 15
WeaponTrail notify Source/ProjectEternal/Public/Combat/AnimNotifies/AnimNotifyState_WeaponTrail.h 10
Action rotation override Source/ProjectEternal/Private/Combat/Components/TargetingComponent.cpp 66
ALS rotation lock (early-return) Plugins/ALS/Source/ALS/Private/AlsCharacter.cpp 1517
HyperArmor notify state Source/ProjectEternal/Public/Combat/AnimNotifies/AnimNotifyState_HyperArmor.h -
AttackFollowThrough notify Source/ProjectEternal/Public/Combat/AnimNotifies/AnimNotify_AttackFollowThrough.h -
CameraSwingGesture notify Source/ProjectEternal/Public/Combat/AnimNotifies/AnimNotify_CameraSwingGesture.h -
HitStopWeight notify Source/ProjectEternal/Public/Combat/AnimNotifies/AnimNotify_HitStopWeight.h -
Whiff hitstop-weight clear (ComboHitWindow NotifyEnd) Source/ProjectEternal/Private/Combat/AnimNotifies/ComboAnimNotifies.cpp -
WeaponSet overlay config Source/ProjectEternal/Public/Combat/Data/WeaponSetConfigDataAsset.h -
WeaponSet → overlay resolution Source/ProjectEternal/Private/EquipmentManagement/Components/EquipmentComponent.cpp -> UpdateWeaponSetOnCharacter() -
GetCurrentWeaponHand() Source/ProjectEternal/Public/Interface/CombatDataProviderInterface.h -
WeaponHand montage tables / per-hand config Source/ProjectEternal/Public/Inventory/Items/Fragments/ItemFragment.h -> FWeaponFragment -
WeaponHand / WeaponSet tags Source/ProjectEternal/Public/EternalGameplayTags.h -

Recent Changes

Date Change Impact
2026-08-06 Doc reconciled with the payload-driven attack flow: the attack ability now opens its rotation override from the swing payload (OpenAttackRotation(Intent)) and releases it in EndAbility; the input path is resolve-only. Charge documented as split — wind-up loop is an ability montage on UChargeWindupAbility, only the released charged swing uses the tag-keyed table. ComboComplete resets the chain immediately on authority (no timer). Motion-warp target-name authoring rule added. Removes the stale "input layer releases the override" rationale (a press that activates nothing never opened one) and documents the mismatched motion-warp target name that left 47 montages warping nowhere
2026-07-26 Action rotation state no longer replicates; opens and releases mirror by RPC in both directions, including tick-decided releases. Fixes a client/server rotation fight visible when breaking off a locked target. The replicated flag over local interpolation state let the far end reopen an override that had already released, snapping yaw to the stored target
2026-07-26 Action rotation and the ALS rotation lock documented. UTargetingComponent now owns Als.LocomotionAction.Attacking for the life of an override and re-asserts it each tick; abilities release via a handle; the input layer releases when no ability activated. Fixes yaw oscillation after a combo (two writers on actor rotation). The Als Set Locomotion Action notify is now advisory — montages without one behave identically, so facing no longer depends on montage authoring
2026-06-17 Combat-feel / impact notifies added (commit 4df2921ab): UAnimNotifyState_HyperArmor (server-only loose State.HyperArmor over active frames — suppresses flinch + knockback, not poise), UAnimNotify_AttackFollowThrough (additive forward push, net-guarded), UAnimNotify_CameraSwingGesture (local-only camera boom impulse, PIE-only), UAnimNotify_HitStopWeight (per-attack hitstop multiplier on UCombatComponent, consumed by next hit, whiff-cleared by ComboHitWindow NotifyEnd). Animators can author swing weight (hyper-armor, step-in, camera lean, freeze-frame) per montage. Feel/timing lives on the montage; only knockback distance scales with item tier (weapon fragment ComboConfig)
2026-03-26 Hand-aware combat config: combo/charge configuration moved onto FWeaponFragment as TMap<WeaponHand, FComboConfiguration/FChargeConfiguration>; ICombatDataProvider::GetCurrentWeaponHand() added as shared resolution path (tooltip + runtime). Shield-only resolves to WeaponSet_1H_Shield with WeaponHand.ShieldOnly for ABP distinction. Combo counts, multipliers, and charge levels can be tuned per hand context; standalone Combo/Charge config data assets no longer the source of truth
2026-03-25 WeaponSet system: weapon animation split into two axes — WeaponHand (drives ABP arm blending + montage-table lookup) and WeaponSet (drives ALS overlay via UWeaponSetConfigDataAsset). Fragment montage tables became TMap<WeaponHand, DataTable>. Overlay activation moved from per-weapon fragment to UEquipmentComponent. Centralized, data-driven overlay resolution; weapons only define the montage entries they need. Known issue: WeaponHand may not replicate correctly for left-hand weapons on clients
2026-03-24 Added WeaponSet tags and WeaponSetConfig data asset for weapon-set → overlay tag mapping. Foundation for centralized overlay resolution
- PlayerCombatComponent now owns montage updates: calls UpdateCombatMontages() internally on weapon change via OnWeaponChanged() rather than being told which montages to use. Inverts dependency; decouples equipment system from montage management
- Right-hand weapon takes priority when determining active montages, falling back to left-hand, then clearing if unarmed. Predictable main-hand-first moveset selection