Weapons · Updated Jun 19, 2026
GASidearmMode — Editor Implementation Guide
GASidearmMode — Editor Implementation Guide
Implements the hold-LT sidearm draw. While active, applies SLTags.States.Weapon.SidearmActive which drives the anim BP pose and gates primary / sidearm fire. Pure Blueprint ability — no C++ base class needed (unlike Melee/Fire which have complex server logic).
⚠ Implementation status (updated 2026-06-14): the draw WINDUP is live; the draw ANIMATION is not.
BP_GA_SL_SidearmModenow runs the full §3 windup graph — it appliesSidearmDrawingfor
SidearmDrawDuration(data asset, 0.5s) withOnEndAbilitycleanup, andSidearmDrawingis in
BP_GA_SL_Pistol_PrimaryFire.ActivationBlockedTags (added 2026-06-14 via the bridge — it was
previously absent, not merely inert), so firing is now blocked during the draw window. What's
still missing is the draw clip: visually the sidearm pops out via the Sidearm Blend pose-in (no
authored windup anim yet). bIsSidearmDrawing (§4) is true during the window — gate a Draw state on
it once the clip exists.
Anim-state flags (2026-06-14):FSLAnimStatecarriesbIsSidearmDrawingandbIsSidearmHolstering
(see §4). Both are PURELY COSMETIC anim drivers, latched in BuildAnimSnapshots off the rising
(draw) / falling (holster) edge of the replicated SidearmActive tag — so observers see them too. They
are decoupled from the SidearmDrawing gameplay tag: that tag (managed by the §3 windup graph) does
the fire-block on the owning client + server only, while the anim flags ride SidearmActive for
all-client reach. See §4.2.
Holster is PURELY COSMETIC + DONE ✅ (2026-06-15) — it carries no gameplay state (the player is back
on the primary the instant SidearmActive drops: fire routing, primary-fire block, and swap gating all
return that frame), so there is no holster tag, no ability change, and no controller change.
bIsSidearmHolsteringis latched inBuildAnimSnapshotsoff the falling edge ofSidearmActiveand held
for the per-weapon WeaponData->SidearmHolsterDuration (default 0.5s; falls back to the character's
SidearmHolsterCosmeticTime, 0.3s). Both theSidearm Blendpose pin (OR'd withIs Sidearm Holstering)
| > and the mesh visibility (`UpdateSidearmMeshVisibility(active | holstering)`) hold through the tail, so the |
|---|
pistol pose + mesh stay visible until the holster anim ends. Currently reuses the draw clip in reverse. See §4.2.
Melee interaction — PISTOL-WHIP (2026-06-11, supersedes the earlier auto-holster approach): while
the sidearm is drawn, melee is a pistol-whip — the pistol stays out and the swing uses the pistol's
own melee values.SLGameplayAbility_Melee::GetWeaponData()returnsGetActiveFireWeaponData()(the
sidearm when SidearmActive, else the main weapon), so melee damage/range/radius/shakes come from
DA_SL_Pistolduring the whip — identical to before when no sidearm is drawn.GA_SidearmModekeeps
running through the swing (ActivationBlockedTags blocks only new activation, not the running
ability), so SidearmActive persists and the pistol stays visible; pistol fire is still blocked
mid-swing by Meleeing. Anim (TODO): add a pistol-whip melee state gated on
bIsMeleeing && bIsSidearmActive(Always Reset on Entry + Inertialization,MeleeImpactnotify).
Editor step: BP_GA_SL_Melee.CancelAbilitiesWithTag must be empty (the earlier auto-holster
put SLTags.Abilities.SidearmMode there — remove it). The holster-then-main-melee + auto-redraw
approach was dropped: too many chained animations, didn't flow together.
#1. Create the Ability Blueprint
- Location:
Content/SystemLink/AbilitySystem/Abilities/Sidearm/BP_GA_SL_SidearmMode
- Parent class:
USLGameplayAbility(the project base, not the engine base)
- Net Execution Policy:
Local Predicted
- Instancing Policy:
Instanced Per Actor
#2. CDO Settings
| Field | Value |
|---|---|
AbilityTags | SLTags.Abilities.SidearmMode |
ActivationOwnedTags | SLTags.States.Weapon.SidearmActive |
ActivationBlockedTags | SLTags.States.Character.Dead, SLTags.States.Character.Meleeing, SLTags.States.Weapon.Busy, SLTags.States.Weapon.RefireLock |
Triggers[0] Tag | SLTags.Events.Weapon.SidearmDraw |
Triggers[0] Source | GameplayEvent |
Left-hand mutual exclusion with grenades: the States.Weapon.Busy block above also stops the sidearm
draw while a grenade is being thrown (the throw owns Busy). The reciprocal half — blocking the throw
while the sidearm is drawn — lives on the grenade ability (SidearmActivein itsActivationBlockedTags).
Both share the left hand; see Docs/GrenadeSystem.md → Left-hand mutual exclusion.
#3. BP Activation Graph
WINDUP GRAPH BUILT & VERIFIED (2026-06-14). The draw-windup graph below is implemented in
BP_GA_SL_SidearmModeand reviewed. It manages theSidearmDrawingtag (gates fire mid-draw);
SidearmActiveis still the auto-appliedActivationOwnedTag. The instant-draw note in the top
callout refers to the animation (no draw clip authored yet) — the tag windup is live.
The ability manages the SidearmDrawing tag + draw delay. Mesh visibility is handled by USLWeaponsComponent (§5). Animation is driven by the ABP responding to bIsSidearmActive / bIsSidearmDrawing (§4).
Event Activate Ability
→ CommitAbility
→ Branch (Condition = CommitAbility Return Value) // early-out if commit failed
True →
Get Avatar Actor from Actor Info → (Actor)
Make Gameplay Tag Container from Tag (SidearmDrawing)
→ Add Loose Gameplay Tags (Actor, Tags, Should Replicate = FALSE)
→ Delay (SidearmDrawDuration, from GetSidearmWeapon → WeaponData)
→ Remove Loose Gameplay Tags (Avatar Actor, SidearmDrawing, Should Replicate = FALSE)
[ability sits active until LT released]
Event OnEndAbility // fires on normal end AND cancel
→ Get Avatar Actor from Actor Info → (Actor)
→ Make Gameplay Tag Container from Tag (SidearmDrawing)
→ Remove Loose Gameplay Tags (Actor, SidearmDrawing, Should Replicate = FALSE)
The ability sits active while LT is held. LT release triggers Cancel Abilities With Tags from the input binding (§6), which ends the ability and removes SidearmActive automatically.
Why the OnEndAbility Remove is mandatory — stranded-tag lockup. The player can release LT
during theDelay, which cancels the ability before the inlineRemoveruns. Without the
OnEndAbilitycleanup theSidearmDrawingloose tag is added but never removed → it sits on the ASC
forever → pistol fire is blocked permanently (it's in the fire ability's ActivationBlockedTags).
Only triggers on a fast tap-during-draw, so it's easy to miss in casual testing. OnEndAbility fires
on both normal end and cancel, so removing there closes the hole. The inline post-Delay Remove is
kept as belt-and-suspenders for the normal case (draw window ends, fire unblocks while the sidearm
stays out); the double-remove is safe because loose tags are ref-counted and clamp at zero.
KeepShould Replicate = FALSEon every Add/Remove.GA_SidearmModeis Local Predicted, so the
graph runs on the owning client (blocks the predicted fire) and the server (blocks the
authoritative fire) — exactly the two machines where fire activates. Simulated proxies never fire
your pistol, so replicating buys nothing for gameplay. And Should Replicate = TRUE is actively
wrong here: it routes through the authority-only replicated-loose-tag path, which misbehaves when the
predicting client runs it.
Observers and the draw animation (resolved 2026-06-14). Because the loose tag doesn't reach
simulated proxies, observers would never play the draw windup off it — confirmed in PIE (a client
watching another player saw the snap-in but not the draw clip). Fix: the anim flag bIsSidearmDrawing
is now a cosmetic rising-edge latch off the replicatedSidearmActivetag inBuildAnimSnapshots
(mirrors the holster tail), held for the sidearm's WeaponData->SidearmDrawDuration. Every machine —
incl. observers — seesSidearmActiveflip and plays the draw. TheSidearmDrawinggameplay tag is
unchanged and still does the owning-client/server fire-block. Do NOT replicate the gameplay tag.
ActivationOwnedTag = SidearmActive applies automatically on activation and is removed automatically
on end — no manual management needed beyond the SidearmDrawing windup tag above.
Draw delay risk: GA_SidearmMode is Local Predicted — the delay runs independently on
client and server. SeeDocs/Risks.md RISK-001. KeepSidearmDrawDuration>= 0.4s.
#4. Animation Architecture
The sidearm ABP state machine (SidearmStateMachine inside ALI_SL_Sidearm) has these states:
| State | Entry Condition | Exit Condition |
|---|---|---|
| Draw | bIsSidearmDrawing true (cosmetic windup) | bIsSidearmDrawing false |
| Idle | After Draw completes | SidearmState.bIsFiring true, or bIsSidearmActive false |
| Fire | SidearmState.bIsFiring true | SidearmState.bIsFiring false |
| Holster | bIsSidearmHolstering true (cosmetic tail) | bIsSidearmHolstering false |
Two separate ABPs per sidearm — do not conflate:
| ABP | Skeleton | Drives | Set via |
|---|---|---|---|
ABP_MC_FP_<Sidearm> / ABP_MC_TP_<Sidearm> | Character skeleton | Left arm pose | AnimLayerClass on data asset |
ABP_<Sidearm>_Weapon | Weapon skeleton | Weapon mesh animations | WeaponMeshAnimClass on data asset |
ALI_SL_Sidearm is implemented by the character arm ABPs, NOT the weapon mesh ABP. Linked into FPMesh/TPMesh via LinkAnimClassLayers from UpdateSidearmMesh when SidearmWeapon changes.
Anim state sourcing — BuildAnimSnapshots() reads SidearmActive/Firing from GAS tags, then derives the cosmetic draw/holster flags off the SidearmActive edges (see §4.2 — observers need this).
S.bIsSidearmActive = ASC->HasMatchingGameplayTag(SLTags::States::Weapon::SidearmActive);
S.SidearmState.bIsFiring = ASC->HasMatchingGameplayTag(SLTags::States::Weapon::Firing);
// Draw + holster flags are cosmetic, latched off the SidearmActive rising/falling edge (NOT the
// SidearmDrawing loose tag, which doesn't replicate to observers):
if (!bPrevSidearmActive && S.bIsSidearmActive) // rising → draw window
SidearmDrawEndTime = Now + (SidearmWeapon ? SidearmWeapon->WeaponData->SidearmDrawDuration
: SidearmDrawCosmeticFallback);
if (bPrevSidearmActive && !S.bIsSidearmActive) // falling → holster tail
SidearmHolsterEndTime = Now + SidearmHolsterCosmeticTime;
bPrevSidearmActive = S.bIsSidearmActive;
S.bIsSidearmDrawing = S.bIsSidearmActive && Now < SidearmDrawEndTime;
S.bIsSidearmHolstering = !S.bIsSidearmActive && Now < SidearmHolsterEndTime;
For early testing, use instant state blends instead of authored draw/holster animations.
The GAS ability and input can be proved before the ABP content is final.
#4.2 Draw + holster anim flags (cosmetic, edge-latched)
The draw and holster animations are cosmetic. Both flags are latched in C++ in BuildAnimSnapshots off the edges of bIsSidearmActive, which comes from the replicated SidearmActive tag — so every machine, including observers, plays them with no extra replication. This is deliberately decoupled from the SidearmDrawing gameplay tag (the fire-block), which is loose and owning-client/server-only.
bIsSidearmDrawing— latched on the rising edge, held for the sidearm's
WeaponData->SidearmDrawDuration (falls back to SidearmDrawCosmeticFallback, EditDefaultsOnly, 0.5s, if the weapon/data can't resolve). Tracks the gameplay windup window so the anim and the fire-unblock line up.
bIsSidearmHolstering— latched on the falling edge, held for the sidearm's
WeaponData->SidearmHolsterDuration (EditDefaultsOnly, 0.5s; falls back to ASLCharacterBase::SidearmHolsterCosmeticTime, default 0.3s, when the weapon/data can't resolve). Holster has no gameplay side at all — releasing LT drops SidearmActive immediately and the player is back on the primary that frame (fire routing, primary-fire block, and swap gating all return that frame).
- Why off
SidearmActiveand notSidearmDrawing: theSidearmDrawingloose tag doesn't reach
simulated proxies, so observers never saw the draw windup when the flag read that tag (PIE-confirmed 2026-06-14). SidearmActive is a replicated ActivationOwnedTag, so its edges are visible everywhere.
- Keeping pose + mesh alive through the tail (DONE 2026-06-15): both
bIsSidearmActiveand
bIsSidearmHolstering must drive the cosmetic consumers, or they cut off the instant SidearmActive
| drops. Two consumers were extended to `bIsSidearmActive | bIsSidearmHolstering`: |
|---|
- Pose: the
Sidearm Blendnode'sIs Sidearm Activepin is wired through an OR with
Is Sidearm Holstering (Break SLAnim State). Without this the node crossfades the sidearm branch out over BlendTime (0.1s) the moment active drops — the holster clip would barely show.
- Mesh: BuildAnimSnapshots calls `UpdateSidearmMeshVisibility(bIsSidearmActive | bIsSidearmHolstering)` |
|---|
(SLCharacterBase.cpp:658) so the pistol mesh stays visible until the holster anim ends.
- To author: add non-looping Draw and Holster states to
SidearmStateMachine, entered on
bIsSidearmDrawing / bIsSidearmHolstering and exited when they clear. "Always Reset on Entry" is REQUIRED on both (+ Inertialization), and the exit must be an explicit NOT bIsSidearm{Drawing/Holstering} transition — NOT an automatic time-remaining rule. Missing this on the Holster state caused BUG-022 (pumping the draw froze the character in the holster pose at the clip's last frame). Tune SidearmDrawDuration / SidearmHolsterDuration (per-weapon on the data asset) to roughly the clip lengths.
- Holster currently reuses the draw clip played in reverse and feels good; set
SidearmHolsterDuration to match SidearmDrawDuration in that case. A dedicated draw/holster clip pair is optional polish — the Sidearm Blend node's built-in 0.1s crossfade covers it visually until then.
#4.1 Sidearm Blend node (custom C++ AnimGraph node)
The sidearm pose is composited onto each locomotion state with a reusable custom AnimGraph node, Sidearm Blend (right-click an AnimGraph → System Link → Sidearm Blend). It replaces the hand-built Layered Blend Per Bone + Blend Poses by bool pair that used to live in every state.
Why a C++ node and not an Animation Layer: an Animation Layer can only be instanced once per ABP (UE error: "layers can be used only once in an animation blueprint"), so it can't be called from idle/walk/run/crouch in the same ABP. A custom AnimGraph node has no such limit — every placement is its own instance. It's the only "define once, reuse in many states / many ABPs, pose-in/pose-out" option in UE.
What it does (per placement):
bIsSidearmActivetrue →LayeredBlend(Base = SidearmPose, Overlay = LowerPose from BlendBone down),
crossfaded in from DefaultPose over BlendTime.
bIsSidearmActivefalse → returnsDefaultPose(holstered pass-through).
- For a left-hand sidearm,
BlendBone = clavicle_r: the right-arm branch is taken from
LowerPose (the arm holding the lowered primary), while the body + left hand stay on the sidearm.
Pins / settings:
| Pin / setting | Wire to | Notes |
|---|---|---|
Sidearm Pose (pose) | the sidearm locomotion blendspace for that state | the base |
Lower Pose (pose) | the weapon-lowered pose input | overlaid on the BlendBone branch |
Default Pose (pose) | the holstered pass-through pose | used when not active |
Is Sidearm Active (bool pin) | Break SLAnim State → Is Sidearm Active | |
Blend Bone (details) | clavicle_r | root of the overlaid branch; children inherit |
Blend Time (details) | 0.1 | draw/holster crossfade seconds (built in — covers it until authored draw/holster exists) |
Source / build:
- Runtime:
FAnimNode_SidearmBlend—SystemLinkCore/Public+Private/Animation/AnimNode_SidearmBlend.*
(ships in packaged builds).
- Editor:
UAnimGraphNode_SidearmBlendin a newSystemLinkCoreEditormodule (UncookedOnly,
so it does NOT ship). Adding this module means a one-time regenerate project files before building.
- In use as of 2026-06-08 in
ABP_TP_Pistol→Sidearm CrouchandSidearm Run Walk; stamp it into
Sidearm Idle and the rifle/shotgun ABPs to retire the remaining two-node pairs.
#5. Mesh Visibility and Asset Assignment
Mesh concerns belong to USLWeaponsComponent, not the ability.
Asset assignment — OnRep_Sidearm (fires on all clients when SidearmWeapon changes): UpdateSidearmMesh handles: mesh asset, socket attachment, WeaponMeshAnimClass on sidearm components, and AnimLayerClass linked into FPMesh/TPMesh.
Visibility — ASC tag callback in USLWeaponsComponent: WeaponsComponent binds RegisterGameplayTagEvent(States::Weapon::SidearmActive) on the ASC. Shows the correct mesh for the current view — FP mesh in FP view, TP mesh in TP view:
Tag added (FP view) → FPSidearmMesh shown, TPSidearmMesh hidden
Tag added (TP view) → TPSidearmMesh shown, FPSidearmMesh hidden
Tag removed → both sidearm meshes hidden
Also update ApplyViewMode in BP to swap sidearm mesh visibility when the player switches FP/TP — check AnimStateSnapshot.bIsSidearmActive to only show the correct mesh when active.
This fires on all clients because SidearmActive is a replicated GAS tag — no extra replication needed.
Gotcha: RegisterGameplayTagEvent does not fire for a tag already active at bind time.
After binding,BindSidearmTagCallbackmanually checksGetTagCountand calls
OnSidearmTagChanged if the tag is already set — handles clients that join mid-draw.
Also:GetPawnASC()may return null duringOnRep_EquippedWeaponfor simulated proxies —
UpdateSidearmMeshprovides a second retry point. Seefeedback_gas_tag_callback_patterns.md.
#6. Wire LT Input in BP_SL_PlayerController
Bound via C++ in SetupInputComponent to SidearmAction (input action: IA_SL_SidearmMode):
- Started →
StartSidearmDraw()→HandleGameplayEvent(ASC, SLTags.Events.Weapon.SidearmDraw)
- Completed / Canceled →
StopSidearmDraw()→CancelAbilities(SLTags.Abilities.SidearmMode)
#7. Update Primary Fire Abilities
Add SLTags.States.Weapon.SidearmActive to ActivationBlockedTags on:
BP_GA_SL_AssualtRifle_PrimaryFire
BP_GA_SL_Shotgun_PrimaryFire
This ensures RT cannot fire the primary weapon while the sidearm is drawn.
#8. Create the Sidearm Fire Ability
Duplicate BP_GA_SL_AssualtRifle_PrimaryFire → rename BP_GA_SL_Pistol_PrimaryFire.
| Field | Value |
|---|---|
ActivationRequiredTags | SLTags.States.Weapon.SidearmActive |
ActivationBlockedTags | SLTags.States.Character.Dead, SLTags.States.Weapon.SidearmDrawing |
Triggers[0] Tag | SLTags.Events.Weapon.PrimaryFire (same RT trigger as primary) |
#8.1 How fire is routed to the sidearm (C++, implemented 2026-06-08)
The fire pipeline (USLGameplayAbility_Fire + Server_ProcessShots) was equipped-weapon-centric. It now reads an "active fire weapon" so the sidearm fires its own data/ammo/muzzle, while sway/obstruction/IK keep using the equipped (primary) weapon. Added to USLWeaponsComponent:
IsSidearmActive()—SidearmActivetag on the owner's ASC (the single source of truth).
GetActiveFireWeapon()— sidearm whileSidearmActive, elseEquippedWeapon.
GetActiveFireWeaponData()/GetActiveFireMode(bPrimary)/GetActiveFirstPersonWeaponMesh().
The fire ability's call sites (CanActivate, ExecuteFire, EndAbility, muzzle, cosmetics, stop-cue) and Server_ProcessShots's damage lookup now go through these. Because the active weapon is driven by the SidearmActive tag — and the sidearm fire ability requires it while the primary blocks on it — the active weapon always matches whichever fire ability is running. When no sidearm is drawn, every "active" accessor returns the equipped weapon, so primary fire is unchanged.
Ammo (option A — actor-owned): the sidearm is a separate, droppable slot, so it does NOT use the ASC ammo attribute. The fire ability branches on IsSidearmActive(): the ammo gate checks the sidearm actor's replicated CurrentAmmo, and the decrement does Sidearm->SetCurrentAmmo(CurrentAmmo - 1) on authority (one per trigger pull). HUD reads the sidearm actor's OnAmmoChanged directly. The equipped weapon keeps its GE/attribute-set ammo path untouched.
#8.2 Editor tasks (after the C++ builds)
BP_GA_SL_Pistol_PrimaryFire— duplicate of the AR fire BP, CDO per the §8 table. Set
RequiredWeaponClass = the sidearm weapon actor class, ActivationRequiredTags = SidearmActive, ActivationOwnedTags = States.Weapon.Firing, same PrimaryFire trigger.
- Block primary while drawn — add
States.Weapon.SidearmActivetoActivationBlockedTagson
BP_GA_SL_AssualtRifle_PrimaryFire and BP_GA_SL_Shotgun_PrimaryFire. (The C++ CanActivate also blocks it via the active-weapon class check — this tag is the explicit, readable belt-and-suspenders.)
- Grant
BP_GA_SL_Pistol_PrimaryFireviaAS_AbilitySet_Default.
- Sidearm weapon data asset — fill
PrimaryFireMode:RoundsPerMinute,FireMode
(SingleShot/FullAuto), BaseDamage, DamageMultipliers, MuzzleSocketName (must exist on the pistol mesh), FireCueTag / ImpactCueTag, and MaxAmmo. AmmoDecrementEffect is NOT needed for the sidearm.
- Fire cosmetics — pistol
GC_SL_Pistol_PrimaryFirecue +OnLocallyPredictedShotFiredFP
muzzle/sound (reuse AR temporarily if pistol assets aren't ready).
- HUD sidearm ammo indicator (separate task) — bind
SidearmWeapon->OnAmmoChangedto a fixed
indicator so the count is visible; the C++ above already keeps CurrentAmmo correct + replicated.
SidearmDrawing in ActivationBlockedTags prevents firing during the draw delay. RT fires whichever ability the current tag state allows — primary when holstered, sidearm when active and draw complete.
#9. Grant via Ability Set ✅
BP_GA_SL_SidearmMode added to AS_AbilitySet_Default at Index [3]. BP_GA_SL_Pistol_PrimaryFire — add when created (same set or dedicated sidearm set).
#10. Assign Default Sidearm in Loadout ✅
DefaultSidearmClass on the loadout data asset → BP_SL_WeaponActor_Pistol.
#11. Test Checklist
Input + tags:
- [ ] LT held →
SidearmActivetag applied (showdebug abilitysystem)
- [ ] LT held →
SidearmDrawingtag applied, removed afterSidearmDrawDurationseconds
- [ ] LT held during draw → RT does NOT fire sidearm (
SidearmDrawingblocks it)
- [ ] LT held after draw completes → RT fires sidearm ability
- [ ] LT held → RT does NOT fire primary (
SidearmActiveblocks it)
- [ ] LT released →
SidearmActivetag removed, meshes hidden
- [ ] LT released → RT fires primary again
Visibility:
- [ ] FP view: only
FPSidearmMeshvisible when drawn
- [ ] TP view / remote client: only
TPSidearmMeshvisible when drawn
- [ ] Switch FP↔TP while drawn → correct mesh shown, other hidden
Edge cases:
- [ ] Melee while sidearm drawn → NOT possible (
MeleeingblocksGA_SidearmMode)
- [ ] Death while drawing →
GA_SidearmModecancelled (Deadblocks it)
- [ ] Multiplayer:
bIsSidearmActivedrives TP pose on all clients
#12. Sidearm Ammo HUD (pistol icon + count)
C++ done (2026-06-15, container approach — mirrors the ammo strip). Rather than a fixed USLAmmoWidget bound on the HUD, the HUD has a container panel that C++ populates at runtime with a widget created from the sidearm's WeaponData->AmmoWidgetClass — the same per-weapon ammo widget class the strip uses. So each sidearm can bring its own ammo widget, and there's one source of truth for the ammo widget class.
Wiring (all in SystemLinkCore):
USLAmmoWidget::OnWeaponSet(const USLWeaponDataAsset*)— BIE fired fromSetWeapon. BP uses it to set the
icon from WeaponData->WeaponIcon / WeaponIconMaterial. (Ammo count stays on OnAmmoChanged.) Required here because the sidearm widget is created standalone, NOT via the strip's CreateSlotWidget (where strip slots usually set their icon).
USLHUDWidget::SidearmAmmoContainer—meta=(BindWidgetOptional)UPanelWidget(any panel type). Optional:
HUDs without a sidearm display omit it.
USLHUDWidget::HandleSidearmChanged— clears the container,CreateWidgets
NewSidearm->WeaponData->AmmoWidgetClass, AddChilds it, and calls SetWeapon(NewSidearm). Tracks the created widget in SidearmAmmoWidgetInstance (exposed via GetSidearmAmmoWidget()).
USLHUDWidget::InitializeSidearmIndicator(Character)— bindsASLCharacterBase::OnSidearmChanged(new
delegate, broadcast from OnRep_Sidearm) → rebuild on swap. Called from ASLPlayerCharacter::InitializeLocalPlayerHUD after InitializeAmmoStrip.
- Visibility policy: persistent — the container is shown whenever the player has a sidearm (drawn OR
holstered); hidden only when there is no sidearm (dropped on death, pre-respawn). Off actor validity, NOT the draw state.
Remaining (editor):
- The sidearm's ammo widget is its
DA_SL_Pistol.AmmoWidgetClass(the sameUSLAmmoWidgetsubclass the
strip uses). Ensure that widget implements OnAmmoChanged(Current, Max) → count text AND OnWeaponSet(WeaponData) → icon from WeaponData->WeaponIcon (the icon MUST come from OnWeaponSet here, since the standalone sidearm widget never goes through the strip's CreateSlotWidget).
- In
WBP_SL_HUDWidget, add a panel (Overlay/SizeBox/Border/NamedSlot — anyUPanelWidget) named
SidearmAmmoContainer (case-sensitive, matches the BindWidgetOptional), positioned/sized where the sidearm count should sit. Leave it empty — C++ fills it at runtime.
- Set
DA_SL_Pistol.WeaponIcon(Weapon Icon Generator) if not already set.
- ⚠ Header change (new BindWidget container + UPROPERTYs) → full rebuild + editor restart (Live Coding
won't surface the new SidearmAmmoContainer bind name).
- Test: indicator shows the pistol icon + ammo at all times once a sidearm is held; fire → count decrements
live; pick up a different sidearm → widget rebuilds for the new pistol; die (sidearm drops) → container hides until respawn re-equips one.
#13. Reticle Swap on Draw
Swaps the crosshair to the sidearm's reticle while it's drawn, back to the primary's when holstered.
C++ done (2026-06-15):
USLWeaponsComponent::GetActiveReticleClass()—BlueprintPure. Returns the sidearm's
PrimaryReticleClass while SidearmActive, else the equipped weapon's; falls back to the equipped weapon's reticle when the active weapon has none set, so a missing sidearm reticle never blanks the crosshair.
USLWeaponsComponent::OnSidearmActiveChanged(bool)— fires on the gameplay draw/holster edge (edge-correct
as of 2026-06-15: it edge-detects on the SidearmActive tag, not the cosmetic visibility flag), so the swap-back happens the instant you holster.
USLHUDWidget::SwapReticle(ReticleClass, PC)— already existed; reticle swap is BP-only (no C++ caller).
BP location note: the weapon-delegate/reticle bindings live in WBP_SL_HUDWidget (the
USLHUDWidgetBP subclass) — that's where the existingOnEquippedWeaponChanged → reticlebinding is —
NOT inBP_SL_PlayerHUD(that's theAHUDactor,ASLPlayerHUD, which just creates the widget). Earlier
revisions of this doc wrongly said BP_SL_PlayerHUD.
BP wiring — in WBP_SL_HUDWidget, alongside the existing OnEquippedWeaponChanged reticle binding. The reticle must react to BOTH the draw/holster toggle AND the sidearm actor changing — otherwise picking up a different sidearm while it's drawn leaves the old reticle up (the SidearmActive tag doesn't toggle on an actor swap, so OnSidearmActiveChanged never fires). This mirrors the normal reticle, which swaps on the weapon actor changing (OnEquippedWeaponChanged); OnSidearmChanged is the sidearm-slot equivalent.
Make a reusable function RefreshReticle → SwapReticle( WeaponsComp → GetActiveReticleClass , Get Owning Player ), and bind all three events to it:
WeaponsComp→ Assign OnSidearmActiveChanged →RefreshReticle(draw/holster). Ignore thebActive
bool — GetActiveReticleClass re-evaluates the tag each call.
WeaponsComp → Get Owner → Cast To SLCharacterBase→ Assign OnSidearmChanged →RefreshReticle
(sidearm actor swapped — e.g. picked up a different pistol, drawn or holstered).
WeaponsComp→ OnEquippedWeaponChanged →RefreshReticle(main weapon equip — one source of truth).
GetActiveReticleClass() returns the sidearm's reticle while SidearmActive, else the equipped weapon's, so the single RefreshReticle node is correct for every case. Redundant calls are cheap: SwapReticle no-ops when the reticle class is unchanged (early-out added 2026-06-15, so no flicker on same-type re-equip or a holstered sidearm swap).
GOTCHA (cost a debug session 2026-06-15): the per-tick reticle driver (Event Tick → OnSpreadChanged
/OnTargetDetected) must push into the current active reticle, becauseSwapReticlereplaces the
reticle widget on every swap. Either set your cached reticle variable from SwapReticle's return value
insideRefreshReticle, or point the tick'sTargetpins atself → Get Reticle(theBlueprintPure
returning ActiveReticle). A reticle ref cached once at init goes stale after the first swap → spread +
red-target silently stop driving (looks like both features are broken for all weapons).
Data:
- Author a
USLReticlesubclass for the pistol (e.g.WBP_SL_Reticle_Pistol; duplicate an existing one).
- Set
DA_SL_Pistol.PrimaryReticleClassto it.
Note: the reticle keys off BOTH OnSidearmActiveChanged (draw/holster) and OnSidearmChanged (actor swap), whereas the ammo indicator (§12) is persistent and keys off OnSidearmChanged validity only — the indicator doesn't care about the draw toggle, the reticle does.