Gameplay · Updated May 29, 2026

Melee

Halo-style melee — one punch, view-based sphere sweep, damage applied on the authority at the impact frame

Halo-style melee — one punch, view-based sphere sweep, damage applied on the authority at the impact frame determined by an AnimNotify. Fires are Local Predicted.


#What's Done (C++)

#USLGameplayAbility_MeleePlugins/SystemLinkCore/Source/SystemLinkCore/Public/AbilitySystem/Abilities/SLGameplayAbility_Melee.h

Base ability class. Blueprint subclasses handle montage, CDO config, and cosmetics.

PropertyDefaultNotes
MeleeDamage100Applied through USLDamageExecution (shield-first)
MeleeRange200cm forward from view origin
MeleeRadius55Sphere radius
MeleeDamageEffectSet to GE_Damage in the BP CDO

NetExecutionPolicy = LocalPredicted, InstancedPerActor.

Damage timing is timer-driven, NOT notify-driven. AnimNotifies are unreliable on the server (the pose may not tick for off-screen client characters; URO can skip the notify frame), which caused client melee to deal damage inconsistently — the AR especially, because its faster anim has a narrower notify window. So:

  • Authority starts a timer in ActivateAbility for MeleeImpactDelay (on the weapon data asset)
  • ApplyServerMeleeDamage() → sweep + ApplyMeleeDamage() + impact cue. Deterministic, no animation dependency. Timer is cleared in EndAbility (so a cancelled swing applies no damage).

  • Owning client still uses the MeleeImpact notify → PerformMeleeSweep() for PREDICTED FX
  • only (the client always renders itself, so its notify is reliable).

Ability END is C++-owned (both sides). ActivateAbility also starts a world timer for MeleeSwingDurationEndMeleeFromTimer()K2_EndAbility(). A plain world timer is NOT subject to ability-task prediction rollback, so it guarantees the Meleeing ActivationOwnedTag is removed even if a client-side rollback orphans the BP's Delay → EndAbility task. Without this, a stuck Meleeing tag permanently blocks melee AND fire on that client (it's in BlockAbilitiesWithTag and the fire abilities' ActivationBlockedTags). ActivateAbility also early-outs if the ability ended during Super (if (!IsActive()) return;) so a damage timer can't fire orphaned.

The BP's Delay → EndAbility is now redundant (C++ guarantees the end). Safe to leave it, or
remove it from BP_GA_SL_Melee to avoid two end paths. If kept, whichever fires first ends the
ability; the other no-ops (guarded by IsActive()).

PerformMeleeSweep() — owning-client FX only. Sphere-sweeps ECC_Pawn from the pawn's eye position along the aim (control rotation) direction. Character hit → OnMeleeHit + HitCameraShake. No character → ECC_Visibility wall sweep → OnMeleeWallHit + WallHitCameraShake. Does NOT apply damage. No-op on non-locally-controlled pawns.

FindMeleeTarget(OutHit) — shared sweep helper used by both the client FX path and the authority damage path. ApplyServerMeleeDamage() — authority timer callback: sweep + damage.

OnMeleeHit(Hit)BlueprintImplementableEvent BlueprintCallable. Fire impact sound, screen shake, hit marker here. Only fires on the locally controlled client.

MeleeImpactDelay lives on the weapon data asset (per-weapon, like MeleeSwingDuration). Set it
to match the frame the fist connects in that weapon's melee anim. Must be less than
MeleeSwingDuration or the ability ends (clearing the timer) before damage lands.

#Input — ASLPlayerController

MeleeAction (TObjectPtr<UInputAction>) bound on ETriggerEvent::Started. Dead-check guards the handler. Melee() fires SLTags::Events::Character::Melee to the pawn's ASC via HandleGameplayEvent.

IA_SL_Melee input action asset exists and is wired on the BP_SL_PlayerController CDO (R3 / right-stick-click on gamepad). ✅

#Tags — SLTags.h / SLTags.cpp

TagStringPurpose
SLTags::Events::Character::MeleeSLTags.Events.Character.MeleeInput → ability trigger
SLTags::Events::Character::MeleeImpactSLTags.Events.Character.MeleeImpactAnimNotify fires this at the impact frame
SLTags::States::Character::MeleeingSLTags.States.Character.MeleeingActivationOwnedTag — blocks re-trigger and fire during the swing
SLTags::Abilities::MeleeSLTags.Abilities.MeleeAbilityTag on the ability

#Anim State

FSLAnimState::bIsMeleeing — snapshotted every tick in BuildAnimSnapshots() from ASC->HasMatchingGameplayTag(SLTags::States::Character::Meleeing). Available on all clients because gameplay tags replicate. Drive the Melee state in ABP state machines on this bool.

#Unarmed as a Default Weapon (2026-05-29)

Unarmed used to be a null state — UnequipWeapon linked nullptr, reverting to the bare base ABP graph, so ABP_MC_Unarmed was orphaned and unarmed melee had no anim/values home. Unarmed now follows the weapon pattern: a USLWeaponDataAsset equipped as the default whenever no real weapon is held (no mesh, no fire mode — just an anim layer + melee values).

C++ (USLWeaponsComponent):

  • DefaultUnarmedData (USLWeaponDataAsset*, EditDefaultsOnly) — set to DA_SL_Unarmed in BP.
  • GetEquippedWeaponData() falls back to DefaultUnarmedData when EquippedWeapon is null, so
  • melee timing (MeleeSwingDuration, MeleeImpactDelay), obstruction values, and other per-weapon queries resolve to unarmed values instead of null.

  • OnRep_EquippedWeapon's null branch links the unarmed anim layer via the view drivers'
  • EquipWeapon (no mesh — AttachWeapon early-returns). Placed in OnRep (not UnequipWeapon) so it's consistent on the host and all clients — the null branch runs both when the server unequips and when EquippedWeapon replicates to null. (If linked in UnequipWeapon, OnRep's null branch would immediately undo it.)

Editor — create DA_SL_Unarmed:

  • ThirdPersonSkeletalMeshData.AnimLayerClass = ABP_MC_Unarmed; no WeaponMesh.
  • FirstPersonSkeletalMeshData.AnimLayerClass = FP unarmed layer (create ABP_MC_FP_Unarmed if it
  • doesn't exist — FP melee needs its own layer with a Melee state; leaving it empty falls back to the base FP graph which has no melee state).

  • MeleeSwingDuration + MeleeImpactDelay for the punch timing. No fire mode.
  • Assign to DefaultUnarmedData on BP_SL_MasterChief's WeaponsComponent.
  • Add a Melee state to ABP_MC_Unarmed (+ the FP unarmed layer), driven by bIsMeleeing, same
  • pattern as AR/Shotgun.

Unarmed is a default equipped state, not an inventory weapon — it's never in CarriedWeapons,
not cyclable, not droppable. bHasEquippedWeapon becomes true while unarmed (it's "equipped"),
so sway/lag/obstruction apply to the bare arms — tune those on DA_SL_Unarmed if the bare-arm
behavior looks off.

#Animation Assets (Lyra library, already in project)

AssetLocation
MC_Rifle_MeleeContent/SystemLink/Characters/MasterChief/Anims/Weapons/AssaultRifle/Melee/
MC_Rifle_Melee_Additivesame
MC_Rifle_Melee (Lyra source)…/Library/Animations1/Lyra/Locomotion/Rifle/
MC_Pistol_Melee / _Additive…/Lyra/Locomotion/Pistol/
MC_Shotgun_Melee / _Additive…/Lyra/Locomotion/Shotgun/

#Still To Do (Editor)

#1. IA_SL_Melee Input Action — ✅ done (2026-05-24)

  • Asset at Content/SystemLink/Inputs/Actions/IA_SL_Melee (Digital bool)
  • Mapped in IMC_Default
  • Gamepad: Right Stick Click (R3) — Doom Eternal style, not RB
  • Wired to MeleeAction on BP_SL_PlayerController CDO → Combat category

#2. BP_GA_SL_Melee Ability Blueprint — ✅ done (2026-05-24)

Location: Content/SystemLink/AbilitySystem/Abilities/Melee/BP_GA_SL_Melee. Parent class: USLGameplayAbility_Melee.

Implementation notes:

  • Used ActivationBlockedTags (Dead + Meleeing) instead of BlockAbilitiesWithTag for the re-trigger guard — same effect, more direct
  • Activation graph: ActivateAbility → CommitAbility → Branch(True) → WaitGameplayEvent(MeleeImpact, OnlyTriggerOnce, OnlyMatchExact) → Event Received → PerformMeleeSweep; Async Task → Delay(0.6) → EndAbility

CDO settings:

FieldValue
AbilityTagsSLTags.Abilities.Melee
ActivationOwnedTagsSLTags.States.Character.Meleeing
BlockAbilitiesWithTagSLTags.States.Character.Meleeing
Trigger[0] TagSLTags.Events.Character.Melee
Trigger[0] SourceGameplayEvent
MeleeDamageEffectGE_Damage
MeleeDamage100
MeleeRange200
MeleeRadius55

Activation graph:


Event Activate Ability

  ├── CommitAbility

  ├── WaitGameplayEvent(SLTags.Events.Character.MeleeImpact)

  │     └── On Event Received → PerformMeleeSweep

  └── Delay(swing duration e.g. 0.5s) → EndAbility

Implement OnMeleeHit: play impact sound, camera shake, hit marker.

#3. Grant via Ability Set — ✅ done (2026-05-24)

Added BP_GA_SL_Melee to Content/SystemLink/Characters/AS_AblitySet_Default (note the existing "Ablity" typo — leave as-is for now).

#4. Block Fire During Swing — ✅ done (2026-05-26)

Added SLTags.States.Character.Meleeing to ActivationBlockedTags on the fire abilities (BP_GA_SL_AssualtRifle_PrimaryFire, BP_GA_SL_Shotgun_PrimaryFire, or their shared parent BP_GA_SL_PrimaryFireBase).

#5. ABP Melee State — ✅ done for AR + Shotgun (2026-05-29)

Per weapon-layer ABP (ABP_MC_FP/TP_AssaultRifle, ABP_MC_FP/TP_Shotgun):

  • Melee state in the weapon-layer state machine, non-looping melee anim, Always Reset on Entry.
  • Transitions on AnimStateSnapshot.bIsMeleeing (in/out), Inertial Blend + Inertialization node.
  • Remaining: ABP_MC_Unarmed needs the same Melee state (+ an unarmed melee anim).

#6. AnimNotify on Melee Anim — ✅ done

SLAnimNotify_SendGameplayEvent(SLTags.Events.Character.MeleeImpact) placed at the impact frame on the FP + TP melee anims. Note: the notify now only drives owning-client predicted FX. Authority damage is timer-driven via MeleeImpactDelay (see the damage-timing section) — set that per weapon.

#7. Remaining polish (editor + assets)

  • Unarmed — C++ done (unarmed-as-default-weapon, see "Unarmed as a Default Weapon" above).
  • Editor: create DA_SL_Unarmed, wire DefaultUnarmedData, add Melee state to ABP_MC_Unarmed (+ FP unarmed layer), author an unarmed punch anim.

  • SFXGC_SL_MeleeImpact cue (assign tag to MeleeImpactCueTag), OnMeleeHit predicted hit
  • sound, OnMeleeWallHit wall thud.

  • Camera shakes — create CS_SL_MeleeSwing / CS_SL_MeleeHit (values above), assign to the
  • three shake fields on BP_GA_SL_Melee.

  • Range tuning — drop MeleeRange→~130, MeleeRadius→~40 on each BP_GA_SL_Melee CDO.

#Camera Shake

USLGameplayAbility_Melee exposes three TSubclassOf<UCameraShakeBase> fields, all fired in C++ on the owning client only (local cosmetic — no replication):

  • SwingCameraShake — fired at ActivateAbility (swing start)
  • HitCameraShake — fired in PerformMeleeSweep when the sweep confirms a character hit
  • WallHitCameraShake — fired in PerformMeleeSweep when the swing connects with world geometry
  • (no character). Assign a harder "clang" shake, or reuse HitCameraShake for identical feel.

#Setup

  1. Create two Blueprints parented to UCameraShakeBase under Content/SystemLink/Camera/Shakes/:
  2. CS_SL_MeleeSwing and CS_SL_MeleeHit.

  1. On each, set the Root Shake Pattern to Wave Oscillator Camera Shake Pattern.
  1. Apply the values below.
  1. Assign both classes on the BP_GA_SL_Melee CDO.

#CS_SL_MeleeSwing (light, quick — sells the windup)

ParameterValue
Duration0.20
Blend In Time0.03
Blend Out Time0.10
Rotation Amplitude Mult1.0
Pitch — Amplitude / Frequency1.2 / 18
Yaw — Amplitude / Frequency0.8 / 16
Roll — Amplitude / Frequency0.6 / 14
Location Amplitude Mult1.0
X (fwd) — Amplitude / Frequency0.8 / 18
Y (lat) — Amplitude / Frequency0.5 / 16
Z — Amplitude / Frequency0 / 0
FOV — Amplitude0

#CS_SL_MeleeHit (heavy, snappy onset — sells the impact)

ParameterValue
Duration0.35
Blend In Time0.01 (near-instant punch)
Blend Out Time0.22 (slower release = weight)
Rotation Amplitude Mult1.0
Pitch — Amplitude / Frequency3.0 / 22
Yaw — Amplitude / Frequency1.8 / 18
Roll — Amplitude / Frequency1.5 / 16
Location Amplitude Mult1.0
X (fwd kick) — Amplitude / Frequency3.5 / 20
Y (lat) — Amplitude / Frequency1.5 / 18
Z (vert) — Amplitude / Frequency2.0 / 20
FOV — Amplitude / Frequency1.5 / 18 (optional subtle punch)

Notes:

  • Rotation amplitude is in degrees, frequency in Hz (oscillations/sec).
  • The hit shake's low blend-in + longer blend-out is what makes it feel like a thud rather than a
  • rattle. Keep blend-in tiny.

  • If it feels too strong, scale down the Rotation/Location Amplitude Multipliers rather than
  • editing every axis — one knob tunes the whole shake.

  • Waveform can stay Sine for both. Perlin Noise reads more "organic/handheld" if you ever want a
  • less rhythmic feel.


#Test Checklist

  • [ ] Melee input fires the ability in PIE (single player)
  • [ ] bIsMeleeing drives the TP swing anim visibly
  • [ ] PerformMeleeSweep hits a character standing at ~150 cm, misses at ~300 cm
  • [ ] Authority applies damage: target loses shield/health on hit
  • [ ] OnMeleeHit fires on owning client (FX / shake)
  • [ ] Melee does NOT fire while dead
  • [ ] Fire ability is blocked during swing (ActivationBlockedTags)
  • [ ] Melee cannot be re-triggered mid-swing (BlockAbilitiesWithTag)
  • [ ] PIE two-player: swing anim visible on the non-owning client, damage applied server-side

#Future Enhancements

#Melee Lunge (Halo-style)

Currently PerformMeleeSweep is a stationary sphere sweep — all the reach comes from raw MeleeRange + MeleeRadius numbers, which is why long ranges feel like "reach attacks" rather than melee. Halo's melee feels close-range but generous because the character lunges toward a detected target on swing.

Plan when implemented:

  • On melee activate, do an early target-detection sweep (reuse the existing sweep, or a forward
  • trace) to find a lunge target within a max acquisition range.

  • If a target is found, interp the character toward it over the swing duration (root motion or
  • CharacterMovement launch / interp), stopping at melee distance.

  • Keep MeleeRange short for the actual damage sweep (the lunge closes the gap; the sweep doesn't
  • need to be long).

  • Authority drives the actual position for damage; owning client predicts the lunge for feel.
  • Gate the lunge so it doesn't yank the player through walls (respect capsule collision) or off
  • ledges.

Until then, keep MeleeRange / MeleeRadius dialed conservatively (~130 / ~40) so stationary melee reach feels right. See the "hitting from too far" tuning discussion — effective reach is MeleeRange + MeleeRadius + target capsule radius.