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:
| Responsibility | Detail |
|---|---|
| Replication | bReplicates = true, SetReplicateMovement(true) — all clients see the actor and its arc |
| Authority guard | Overlap 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_OnCollected | NetMulticast Reliable — calls BP_OnCollected() on all clients. |
BP_OnCollected | BlueprintImplementableEvent — implement pickup VFX/sound in BP. |
BP_OnPickupLanded | BlueprintImplementableEvent — 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— blocksWorldStatic/WorldDynamic. Drives the launch arc. Disabled untilLaunchPickupis 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) untilLaunchPickup. TuneBouncinessandFrictiondirectly 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.
| Property | Default | Purpose |
|---|---|---|
LaunchHorizontalSpeed | 400 | cm/s horizontal |
LaunchVerticalSpeed | 500 | cm/s upward |
MaxBounces | 2 | Bounces before settling |
LaunchSpreadAngle | 45° | 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
| Mode | How to set up |
|---|---|
| Map-placed / fresh spawn | Set WeaponActorClass in BP defaults. GetWeapon() lazy-spawns an actor the first time it's called. |
| Dropped weapon | Call 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:
- If
ExistingWeaponis valid → returnsExistingWeapon->WeaponData
- Otherwise → reads
WeaponDatafrom the CDO ofWeaponActorClass
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)
| Method | Who calls it | Description |
|---|---|---|
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_Implementation | BlueprintImplementableEvent — wire to HUD in character BP |
OnHidePickupPrompt() | Client_HidePickupPrompt_Implementation | BlueprintImplementableEvent — wire to HUD in character BP |
GetPendingPickup() | ASLWeaponPickup::OnOverlapEnd, server guard | Returns the server-side pending pickup actor |
SetPendingPickup(Pickup) | ASLWeaponPickup::OnCollected / OnOverlapEnd | Sets 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)
| Method | Description |
|---|---|
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 (`SystemLink | HUD` category). The icon is whatever you generated with the Weapon Icon Generator. |
|---|
#AddAmmo — USLWeaponsComponent
Used by ASLWeaponPickup::OnCollected for the ammo top-up path.
void AddAmmo(ASLWeaponActor* Weapon, int32 Amount);
- Authority only
- If
Weaponis the currently equipped weapon → modifiesUSLAmmoAttributeSet.CurrentAmmoviaSetNumericAttributeBase(clamped toMaxAmmoby the attribute'sPostGameplayEffectExecute)
- If
Weaponis a carried but unequipped weapon → callsWeaponActor->SetCurrentAmmo(...)directly
- In both cases
OnAmmoChangedfires 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
| Property | Default | Purpose |
|---|---|---|
WeaponData | — | Targets a specific weapon type. Player must already carry a weapon with this data asset. |
AmmoAmount | 30 | How 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,CurrentAmmolives inUSLAmmoAttributeSeton the ASC — the weapon actor'sCurrentAmmofield still holds the value from equip time. Always read fromWeaponsComp->GetCurrentAmmo()for the equipped weapon, not the actor.
#Editor Setup
- Create
BP_AmmoPickup_AssaultRifle(parent:ASLAmmoPickup)
- Set
WeaponData = DA_AssaultRifleandAmmoAmountin Class Defaults
- Optionally implement
BP_OnCollectedfor 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_Interactinput action (Digital bool)
- [ ] Create
BP_WeaponPickup_AssaultRifle(parent:ASLWeaponPickup), setWeaponActorClass
- [ ] In character BP: implement
OnShowPickupPrompt→ callHUDWidget → ShowWeaponPickupPrompt
- [ ] In character BP: implement
OnHidePickupPrompt→ callHUDWidget → HideWeaponPickupPrompt
- [ ] In character BP: bind
IA_SL_Interact→Server Accept Pickup(only whenGet Pending Pickupis valid)
- [ ] In
WBP_SL_HUD(or a child): implementShowWeaponPickupPromptandHideWeaponPickupPrompt