Gameplay · Updated 2552.07.31.08.08

Melee

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.

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

#Target Lunge (2026-07-30)

Halo's melee feels close-range but generous because the character lunges at a detected target. Before this, the lunge was a fixed forward nudge — MeleeLungeSpeed along the actor's forward vector, identical whether an enemy was three metres away or nobody was there — so all the reach came from raw MeleeRange + MeleeRadius, which reads as a "reach attack" rather than a lunge.

#What it does now

  • Direction points at the acquired enemy, not straight ahead. Horizontal only: lunging at someone
  • above or below closes the ground gap instead of flinging you off the floor.

  • Speed interpolates with the gapMeleeLungeSpeed at zero distance, rising to
  • MeleeTargetLungeSpeed at MeleeLungeRange. A target already in your face keeps the old nudge; one at the edge of acquisition gets the full leap. Distance follows from speed under braking friction, so both grow together and there is no second knob to keep consistent.

  • No target = exactly the old behaviour. Null is the ordinary case, not an error.

#Tuning (per weapon, on USLWeaponDataAsset)

PropertyDefaultNotes
MeleeLungeSpeed600Also the floor with a target, and 0 disables lunging entirely
MeleeTargetLungeSpeed1600Values below MeleeLungeSpeed are ignored — acquiring an enemy must never slow you down
MeleeLungeRange500Deliberately longer than MeleeRange; the point is covering ground the swing cannot. 0 disables target lunging
MeleeLungeRadius90Wider than MeleeRadius on purpose — acquisition only chooses, it does not widen what the swing can hit

The acquisition sweep uses ECC_Pawn, matching the melee damage sweep rather than the shot channel, so the thing that gets lunged at and the thing that gets hit agree.

#Replication contract — read this before changing it

The target is chosen once, by the machine that owns the input. ASLPlayerController::Melee calls USLGameplayAbility_Melee::FindLungeTarget and puts the result in FGameplayEventData::Target; GAS forwards the trigger data to the server for the predicted activation. Each machine then derives the lunge vector from its own copy of that actor, so only the choice crosses the wire.

Letting both sides sweep independently would risk them disagreeing about whether a target exists at all — a 1600-speed leap against a 600-speed nudge, which reads as a rubber-band. This is the dash direction bug in a second place (Docs/Footguns.md → GAS — abilities that move the character).

Two consequences worth knowing:

  • LaunchCharacter is not replayed on a correction. FSavedMove_Character does not store
  • PendingLaunchVelocity. The lunge works because both machines apply a matching launch at nearly the same moment; the server's lands ~RTT/2 later, so a strafing target has moved and the two directions differ by a few degrees. Harmless at 600, worth watching at 1600. If it rubber-bands, the fix is a root motion source (as the dash did), not a smaller number.

  • Damage is never affected by a bad target. ApplyServerMeleeDamage still runs its own independent
  • authority-side sweep at the impact frame, so the lunge target and the damage target are fully decoupled. A Target that fails to replicate costs you a nudge instead of a leap — never a wrong hit.

#Debugging

sl.Melee.Debug 1 logs the target, direction and speed on every machine that decides a lunge; 2 also draws the acquisition sweep (yellow) and the resolved lunge (server red, client green).

Run it on both machines in a two-player PIE and compare. The failure this exists to catch is silent: if Target does not survive the trip, the server falls back to a plain forward lunge while the client leaps, and the symptom is "melee feels inconsistent" rather than anything that looks like replication.

#Known gaps

  • You can lunge off a ledge. Capsule collision stops you at walls, but nothing gates the lunge at
  • an edge. Left in deliberately — it is Doom-ish, and the Kill Z failsafe now handles the fall — but it is a design call, not an oversight.

  • MeleeImpactDelay is not gap-aware. It is a fixed 0.3 s, so a maximum-range lunge could in
  • principle land its damage sweep before you arrive. Watch for it at the far edge; the fix would be timing, not speed.

  • Corpses are valid lunge targets. ClassifyHit returns Hostile for any ASLCharacterBase that
  • is not the viewer, dead or alive. The reticle and the melee damage sweep already behave this way, so this is consistent rather than special — but it is worth a pass if bodies start sticking around.

#Test checklist

  • [ ] Lunge at an enemy ~4 m away is visibly faster and further than a swing at nothing
  • [ ] Swing with no enemy in front is unchanged from before
  • [ ] Enemy directly above/below does not launch you vertically
  • [ ] As a non-host client, lunging at the host gives the fast leap (not the 600 nudge) — this is
  • the FGameplayEventData::Target replication check, and it fails silently

  • [ ] sl.Melee.Debug 1 on host and client report the same target and similar speed
  • [ ] No rubber-band on a long lunge at a strafing target

#Future Enhancements

Keep MeleeRange / MeleeRadius dialed conservatively (~130 / ~40) so stationary melee reach feels right — the lunge closes the gap, so the damage sweep does not need to be long. See the "hitting from too far" tuning discussion: effective reach is MeleeRange + MeleeRadius + target capsule radius.