// docs/gameplay-moveset-system/action-modules.md
READ

Action Modules

Per-node Blueprint behaviour units with NetMode gating — the universal extension hook

Every combo-graph node — attack nodes, the root node, Reset Combo nodes, any future node type — has a Modules array. Designers attach Blueprint subclasses of UActionModule to a node, picking which FGameplayTag each module reacts to. While that node is the active combo cursor, the component dispatches matching events to the modules — locally on whichever side(s) the module's NetMode allows.

This is the universal hook for per-node game logic: damage, VFX, sound, camera shakes, HUD updates, AI perception, state-tag flips — anything that should react to a moveset event on a specific node.

Built-in events

The component dispatches these to modules attached to the node that currently owns the combo cursor:

Event tag Fired when
Moveset.Event.Ready After Apply Moveset finishes initialisation.
Moveset.Event.AttackStarted A new attack montage begins (after cursor sync).
Moveset.Event.AttackEnded An attack finishes naturally (montage timeout).
Moveset.Event.Hit A registered hitbox detected a hit (or ReportHit was called manually).
Moveset.Event.ComboAdvanced Chain transitioned to a new attack.
Moveset.Event.ComboDropped Chain ended (Reset Combo node, cancel, or timeout).
Moveset.Event.ComboPoint The Moveset: Combo Point AnimNotify fired.
Moveset.Event.Failed Initialisation failed (no MovesetComponent, etc.).

Authoring a module

Two configuration sites come together: the module class carries the reaction logic + NetMode + matching policy; the node entry says "for this event tag, run these module instances in order".

1. Make the module class

Right-click in Content Browser → Blueprint Class → search "Action Module" → create your subclass (e.g. BP_DamageModule). Open it.

Override the Handle event:

Handle (Owner, EventTag, Payload)
  ├─ Switch on EventTag
  │   case Moveset.Event.Hit            → ApplyDamage(Payload.HitResult.HitActor, 25.0)
  │   case Moveset.Event.AttackStarted  → SpawnTrailVFX
  │   case Moveset.Event.ComboAdvanced  → AddSuperMeter(0.05)

In Class Defaults, set:

  • NetMode
    • All (default) — fires on autoproxy + authority + simproxy.
    • Authority — server-only. Use for damage and any state mutation.
    • LocallyControlled — only the owning player's side. Use for HUD / prediction.
    • SimulatedProxy — only on remote viewers. Use for cosmetic-only effects you don't want the owner / authority to also fire.
  • bExactMatch — false by default; the dispatcher uses MatchesTag so a module subscribed to Moveset.Event.Hit also fires for child tags like Moveset.Event.Hit.Critical. Flip to true for strict equality.

2. Attach to a combo graph node

Open your UComboGraphAsset. Click any node (attack, root, reset). In the Details panel:

Modules: TArray<FActionModuleEntry>
  [0]
    EventTag: Moveset.Event.Hit
    Modules: [ BP_DamageModule, BP_HitVFXModule ]
  [1]
    EventTag: Moveset.Event.AttackStarted
    Modules: [ BP_StartupSoundModule ]

Each entry pairs an event tag with the modules that should run when the dispatched event matches it. Modules run in array order on whichever side(s) their NetMode allows.

NetMode strategy

The right NetMode depends on what the module does:

Module purpose NetMode Why
Damage application Authority Server is the only side that owns hit truth.
Hit VFX (sparks, blood) All Every viewer needs to see it.
Hit VFX gated to the attacker's side Authority + SimulatedProxy Skip the autonomous proxy, who already plays its own predicted VFX from OnHit.
Camera shake LocallyControlled Only the player feels it.
HUD ammo decrement LocallyControlled Owning client UI.
AI perception event Authority Authority drives world state.
Audio cue for nearby enemies All Spatial audio plays on every viewer.
State tag flip ("HitStun") Authority Replicated state must originate on auth.

If you find yourself wanting BOTH "fires on owning side immediately for prediction" AND "fires on authority for server-side state", split into two modules: one with LocallyControlled, one with Authority.

Sending custom events from your game code

You can emit your own event tags into the same routing system. Define your tag in Project Settings (e.g. Game.Combat.PerfectParry), then from C++ or BP:

FMovesetEventPayload Payload;
Payload.EventTag = FGameplayTag::RequestGameplayTag("Game.Combat.PerfectParry");
MovesetComponent->SendMovesetEvent(Payload.EventTag, Payload);

…and any module on the active combo node that subscribes to that tag fires. This is the recommended hook point from AnimNotifys, ability tasks, or other game systems that want to talk to per-node logic without coupling to specific attack classes.

The payload is fully extensible — fill in AttackTag, PreviousAttackTag, HitResult, FailureReason as appropriate for your event.

Lifetime caveat

Modules are subobjects of the graph asset (Instanced UPROPERTY). One module instance is shared across every actor running the same graph. Don't store per-actor state on the module itself — look up state through the UMovesetComponent* Owner argument passed to Handle.

Concretely: don't do this in BP_DamageModule:

Variable: int32 HitsLandedSoFar
Handle: HitsLandedSoFar = HitsLandedSoFar + 1

Every player on the server using this module would share the counter. Instead:

Handle:
  MyHits = Owner->GetActor()->FindComponentByClass<UMyCombatComponent>()->HitsLanded
  ApplyDamage(...)
  Owner->GetActor()->FindComponentByClass<UMyCombatComponent>()->HitsLanded = MyHits + 1

State lives on a per-actor component or in your character class. The module is pure logic.

Composition patterns

Damage + VFX + Sound on every Hit

One node entry, three modules:

Modules:
  [0] EventTag: Moveset.Event.Hit
      Modules: [ BP_DamageModule (Authority), BP_HitVFX (All), BP_HitSound (All) ]

The runtime calls them in array order. BP_DamageModule runs only on the server; the others run on every connected machine.

Status effect chain

A heavy attack that applies stagger if it lands during a parry window:

Attack Node: HeavyFinisher
  Modules:
    [0] EventTag: Moveset.Event.Hit
        Modules: [ BP_DamageModule (Authority), BP_TryApplyStagger (Authority) ]
    [1] EventTag: Moveset.Event.AttackStarted
        Modules: [ BP_AnticipationCameraShake (LocallyControlled) ]

BP_TryApplyStagger checks the hit target's tags and applies a stagger gameplay effect if the target had State.Parrying.

UI-only feedback on Combo Point

Attack Node: PerfectComboMoment
  Modules:
    [0] EventTag: Moveset.Event.ComboPoint
        Modules: [ BP_PerfectFlash (LocallyControlled), BP_PerfectSound (All) ]

The Combo Point notify fires from the montage at a designer-chosen frame (typically the end of an active hit window for "perfect-timing" feedback).