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_Melee — Plugins/SystemLinkCore/Source/SystemLinkCore/Public/AbilitySystem/Abilities/SLGameplayAbility_Melee.h
Base ability class. Blueprint subclasses handle montage, CDO config, and cosmetics.
| Property | Default | Notes |
|---|---|---|
MeleeDamage | 100 | Applied through USLDamageExecution (shield-first) |
MeleeRange | 200 | cm forward from view origin |
MeleeRadius | 55 | Sphere radius |
MeleeDamageEffect | — | Set 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
ActivateAbilityforMeleeImpactDelay(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
MeleeImpactnotify →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 MeleeSwingDuration → EndMeleeFromTimer() → 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.
MeleeImpactDelaylives on the weapon data asset (per-weapon, likeMeleeSwingDuration). 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
| Tag | String | Purpose |
|---|---|---|
SLTags::Events::Character::Melee | SLTags.Events.Character.Melee | Input → ability trigger |
SLTags::Events::Character::MeleeImpact | SLTags.Events.Character.MeleeImpact | AnimNotify fires this at the impact frame |
SLTags::States::Character::Meleeing | SLTags.States.Character.Meleeing | ActivationOwnedTag — blocks re-trigger and fire during the swing |
SLTags::Abilities::Melee | SLTags.Abilities.Melee | AbilityTag 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 toDA_SL_Unarmedin BP.
GetEquippedWeaponData()falls back toDefaultUnarmedDatawhenEquippedWeaponis 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; noWeaponMesh.
FirstPersonSkeletalMeshData.AnimLayerClass= FP unarmed layer (createABP_MC_FP_Unarmedif 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+MeleeImpactDelayfor the punch timing. No fire mode.
- Assign to
DefaultUnarmedDataonBP_SL_MasterChief's WeaponsComponent.
- Add a Melee state to
ABP_MC_Unarmed(+ the FP unarmed layer), driven bybIsMeleeing, 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)
| Asset | Location |
|---|---|
MC_Rifle_Melee | Content/SystemLink/Characters/MasterChief/Anims/Weapons/AssaultRifle/Melee/ |
MC_Rifle_Melee_Additive | same |
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
MeleeActiononBP_SL_PlayerControllerCDO → 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 ofBlockAbilitiesWithTagfor 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:
| Field | Value |
|---|---|
AbilityTags | SLTags.Abilities.Melee |
ActivationOwnedTags | SLTags.States.Character.Meleeing |
BlockAbilitiesWithTag | SLTags.States.Character.Meleeing |
| Trigger[0] Tag | SLTags.Events.Character.Melee |
| Trigger[0] Source | GameplayEvent |
MeleeDamageEffect | GE_Damage |
MeleeDamage | 100 |
MeleeRange | 200 |
MeleeRadius | 55 |
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_Unarmedneeds 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.
- SFX —
GC_SL_MeleeImpactcue (assign tag toMeleeImpactCueTag),OnMeleeHitpredicted 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 eachBP_GA_SL_MeleeCDO.
#Camera Shake
USLGameplayAbility_Melee exposes three TSubclassOf<UCameraShakeBase> fields, all fired in C++ on the owning client only (local cosmetic — no replication):
SwingCameraShake— fired atActivateAbility(swing start)
HitCameraShake— fired inPerformMeleeSweepwhen the sweep confirms a character hit
WallHitCameraShake— fired inPerformMeleeSweepwhen the swing connects with world geometry
(no character). Assign a harder "clang" shake, or reuse HitCameraShake for identical feel.
#Setup
- Create two Blueprints parented to
UCameraShakeBaseunderContent/SystemLink/Camera/Shakes/:
CS_SL_MeleeSwing and CS_SL_MeleeHit.
- On each, set the Root Shake Pattern to Wave Oscillator Camera Shake Pattern.
- Apply the values below.
- Assign both classes on the
BP_GA_SL_MeleeCDO.
#CS_SL_MeleeSwing (light, quick — sells the windup)
| Parameter | Value |
|---|---|
| Duration | 0.20 |
| Blend In Time | 0.03 |
| Blend Out Time | 0.10 |
| Rotation Amplitude Mult | 1.0 |
| Pitch — Amplitude / Frequency | 1.2 / 18 |
| Yaw — Amplitude / Frequency | 0.8 / 16 |
| Roll — Amplitude / Frequency | 0.6 / 14 |
| Location Amplitude Mult | 1.0 |
| X (fwd) — Amplitude / Frequency | 0.8 / 18 |
| Y (lat) — Amplitude / Frequency | 0.5 / 16 |
| Z — Amplitude / Frequency | 0 / 0 |
| FOV — Amplitude | 0 |
#CS_SL_MeleeHit (heavy, snappy onset — sells the impact)
| Parameter | Value |
|---|---|
| Duration | 0.35 |
| Blend In Time | 0.01 (near-instant punch) |
| Blend Out Time | 0.22 (slower release = weight) |
| Rotation Amplitude Mult | 1.0 |
| Pitch — Amplitude / Frequency | 3.0 / 22 |
| Yaw — Amplitude / Frequency | 1.8 / 18 |
| Roll — Amplitude / Frequency | 1.5 / 16 |
| Location Amplitude Mult | 1.0 |
| X (fwd kick) — Amplitude / Frequency | 3.5 / 20 |
| Y (lat) — Amplitude / Frequency | 1.5 / 18 |
| Z (vert) — Amplitude / Frequency | 2.0 / 20 |
| FOV — Amplitude / Frequency | 1.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)
- [ ]
bIsMeleeingdrives the TP swing anim visibly
- [ ]
PerformMeleeSweephits a character standing at ~150 cm, misses at ~300 cm
- [ ] Authority applies damage: target loses shield/health on hit
- [ ]
OnMeleeHitfires 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
MeleeRangeshort 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.