Weapons · Updated Mar 23, 2026

Weapon Pickup System

Weapon Pickup System

The pickup system handles two cases: a player picking up a weapon they don't carry (shows a prompt, waits for input), and a player walking into a pickup for a weapon they already have (auto-tops up ammo, no prompt).


#Class Hierarchy


AActor

  └── ASLPickupBase      (abstract — overlap detection, FX multicast, authority guard)

        ├── ASLWeaponPickup  (concrete — weapon grant, ammo top-up, pickup prompt)

        └── ASLAmmoPickup    (concrete — ammo-only pickup for a specific weapon type)


#ASLPickupBase

Lives in Public/World/SLPickupBase.h.

Handles everything domain-independent:

ResponsibilityDetail
ReplicationbReplicates = true, SetReplicateMovement(true) — all clients see the actor and its arc
Authority guardOverlap delegates only bound on the server
OnCollected(Character)Base: fires Multicast_OnCollected() then Destroy(). Subclasses override, call Super to trigger FX + destroy.
OnOverlapEnd(Character)Empty virtual. Subclasses override to react (e.g. hide prompt).
Multicast_OnCollectedNetMulticast Reliable — calls BP_OnCollected() on all clients.
BP_OnCollectedBlueprintImplementableEvent — implement pickup VFX/sound in BP.
BP_OnPickupLandedBlueprintImplementableEvent — fires on all clients when the arc completes. Use for landing dust/sound.

#Component Layout


BounceCollision  (USphereComponent — root)

  └── CollisionSphere  (USphereComponent — child, Trigger profile)

ProjectileMovementComponent

  • BounceCollision — blocks WorldStatic / WorldDynamic. Drives the launch arc. Disabled until LaunchPickup is called; disabled again after landing. Size this to fit the mesh in the BP subclass (default 35 cm).
  • CollisionSphere — Trigger profile, overlap-only. Bound for collection on server. Disabled while airborne, re-enabled on landing.
  • ProjectileMovementComponent — inactive (bAutoActivate = false) until LaunchPickup. Tune Bounciness and Friction directly on the component in BP defaults.

#Launch Arc — LaunchPickup(FVector Direction)

Call on the server after spawning a dropped pickup. Randomizes the horizontal direction within LaunchSpreadAngle degrees of Direction, then launches with LaunchHorizontalSpeed + LaunchVerticalSpeed. After MaxBounces the pickup settles and CollisionSphere is re-enabled.

PropertyDefaultPurpose
LaunchHorizontalSpeed400cm/s horizontal
LaunchVerticalSpeed500cm/s upward
MaxBounces2Bounces before settling
LaunchSpreadAngle45°Half-angle of random horizontal spread — prevents all pickups from piling up when a player drops multiple items at once

#ASLWeaponPickup

Lives in Public/World/SLWeaponPickup.h.

#Two spawn modes

ModeHow to set up
Map-placed / fresh spawnSet WeaponActorClass in BP defaults. GetWeapon() lazy-spawns an actor the first time it's called.
Dropped weaponCall SetWeapon(ExistingActor) (e.g. from drop logic). GetWeapon() returns it directly — ammo is already correct from WriteBackAmmoToWeaponActor.

ExistingWeapon is Replicated so clients always have a valid actor reference.

#GetWeaponData()

Returns the weapon's data asset without spawning the weapon actor:

  1. If ExistingWeapon is valid → returns ExistingWeapon->WeaponData
  1. Otherwise → reads WeaponData from the CDO of WeaponActorClass

Used to pass data to the pickup prompt without touching the weapon actor.

#OnCollected — two paths


Overlap enter (server only)

  └── GetWeapon() (lazy spawn if needed)

  └── Character already carries this weapon type?

        ├── YES — ammo at max? → early return (no effect)

        │         otherwise    → WeaponsComp::AddAmmo(...) → Super::OnCollected (FX + destroy)

        └── NO  — SetPendingPickup(this) on character

                  Client_ShowPickupPrompt(GetWeaponData()) on character

                  return without calling Super (pickup stays alive)

#AcceptPickup(Character)

Called on the server when the player presses interact. Performs the actual weapon grant:


Character->AddWeaponToInventory(Weapon);

Character->RequestEquip(Weapon);

Super::OnCollected(Character);  // FX multicast + Destroy

#OnOverlapEnd(Character)

Clears the pending pickup and hides the prompt if the leaving character is the one who triggered it:


if (Character->GetPendingPickup() == this)

{

    Character->SetPendingPickup(nullptr);

    Character->Client_HidePickupPrompt();

}


#Pickup Prompt Flow


Player enters radius (server overlap)

  └── ASLWeaponPickup::OnCollected

        └── [new weapon] SetPendingPickup(this) on character

        └── Character::Client_ShowPickupPrompt(WeaponData)

              └── [owning client] OnShowPickupPrompt(WeaponData)   ← BP event

                    └── HUDWidget::ShowWeaponPickupPrompt(WeaponData)  ← BP event



Player presses interact (owning client input)

  └── Character::Server_AcceptPickup()   ← Server RPC

        └── PendingPickup->AcceptPickup(Character)

              └── AddWeaponToInventory + RequestEquip + FX + Destroy



Player leaves radius without accepting (server overlap end)

  └── ASLWeaponPickup::OnOverlapEnd

        └── Character::SetPendingPickup(nullptr)

        └── Character::Client_HidePickupPrompt()

              └── [owning client] OnHidePickupPrompt()   ← BP event

                    └── HUDWidget::HideWeaponPickupPrompt()  ← BP event

#Why the prompt goes through the character

Pickup actors have no player connection — Client RPCs require an actor owned by the player's connection. The character is always owned by its player's connection, so all client communication routes through it.


#Character API (ASLCharacterBase)

MethodWho calls itDescription
Client_ShowPickupPrompt(WeaponData)ASLWeaponPickup::OnCollected (server)Fires OnShowPickupPrompt on the owning client
Client_HidePickupPrompt()ASLWeaponPickup::OnOverlapEnd (server)Fires OnHidePickupPrompt on the owning client
Server_AcceptPickup()Character BP input binding (client)Server uses PendingPickup to call AcceptPickup
OnShowPickupPrompt(WeaponData)Client_ShowPickupPrompt_ImplementationBlueprintImplementableEvent — wire to HUD in character BP
OnHidePickupPrompt()Client_HidePickupPrompt_ImplementationBlueprintImplementableEvent — wire to HUD in character BP
GetPendingPickup()ASLWeaponPickup::OnOverlapEnd, server guardReturns the server-side pending pickup actor
SetPendingPickup(Pickup)ASLWeaponPickup::OnCollected / OnOverlapEndSets or clears the pending pickup

PendingPickup is server-only (Transient, not replicated). The client never needs it — the character BP just calls Server_AcceptPickup() when interact is pressed.


#HUD Widget API (USLHUDWidget)

MethodDescription
ShowWeaponPickupPrompt(WeaponData)BlueprintImplementableEvent. Implement in BP to show the prompt, read WeaponData->WeaponDisplayName and icon from the data asset.
HideWeaponPickupPrompt()BlueprintImplementableEvent. Implement in BP to dismiss the prompt.
WeaponDisplayName is an FText field on USLWeaponDataAsset (`SystemLinkHUD` category). The icon is whatever you generated with the Weapon Icon Generator.

#AddAmmoUSLWeaponsComponent

Used by ASLWeaponPickup::OnCollected for the ammo top-up path.


void AddAmmo(ASLWeaponActor* Weapon, int32 Amount);

  • Authority only
  • If Weapon is the currently equipped weapon → modifies USLAmmoAttributeSet.CurrentAmmo via SetNumericAttributeBase (clamped to MaxAmmo by the attribute's PostGameplayEffectExecute)
  • If Weapon is a carried but unequipped weapon → calls WeaponActor->SetCurrentAmmo(...) directly
  • In both cases OnAmmoChanged fires and the HUD updates automatically


#ASLAmmoPickup

Lives in Public/World/SLAmmoPickup.h. A standalone pickup that adds ammo to a specific weapon in the player's inventory — no prompt, no new weapon grant.

#Properties

PropertyDefaultPurpose
WeaponDataTargets a specific weapon type. Player must already carry a weapon with this data asset.
AmmoAmount30How much ammo to add (clamped by MaxAmmo on the weapon's fire mode).

#OnCollected flow


Overlap enter (server only)

  └── FindCarriedWeaponByData(WeaponData) — must carry this weapon type

  └── Resolve current ammo correctly:

        bIsEquipped? → WeaponsComp::GetCurrentAmmo() (live GAS attribute)

                      : MatchingWeapon->CurrentAmmo (actor state, not yet in attribute set)

  └── CurrentAmmo >= MaxAmmo? → early return (no effect)

  └── WeaponsComp::AddAmmo(MatchingWeapon, AmmoAmount)

  └── Character::Client_ShowAmmoPickupNotification(WeaponData)

  └── Super::OnCollected → Multicast FX + Destroy

The equipped/unequipped split is critical. While a weapon is equipped, CurrentAmmo lives in USLAmmoAttributeSet on the ASC — the weapon actor's CurrentAmmo field still holds the value from equip time. Always read from WeaponsComp->GetCurrentAmmo() for the equipped weapon, not the actor.

#Editor Setup

  • Create BP_AmmoPickup_AssaultRifle (parent: ASLAmmoPickup)
  • Set WeaponData = DA_AssaultRifle and AmmoAmount in Class Defaults
  • Optionally implement BP_OnCollected for pickup FX
  • Place in the level anywhere you want a loose ammo pickup

#Editor Setup

See EditorTasks.md for step-by-step Blueprint wiring tasks.

Quick checklist:

  • [ ] Create IA_SL_Interact input action (Digital bool)
  • [ ] Create BP_WeaponPickup_AssaultRifle (parent: ASLWeaponPickup), set WeaponActorClass
  • [ ] In character BP: implement OnShowPickupPrompt → call HUDWidget → ShowWeaponPickupPrompt
  • [ ] In character BP: implement OnHidePickupPrompt → call HUDWidget → HideWeaponPickupPrompt
  • [ ] In character BP: bind IA_SL_InteractServer Accept Pickup (only when Get Pending Pickup is valid)
  • [ ] In WBP_SL_HUD (or a child): implement ShowWeaponPickupPrompt and HideWeaponPickupPrompt