Weapons · Updated Jun 16, 2026
Weapons System
Weapons System
Each weapon in SystemLink is represented by two objects working together:
ASLWeaponActor— the persistent world actor. Holds a reference to its data asset and ownsCurrentAmmo. Exists in the world at all times, hidden when carried and unequipped.
USLWeaponDataAsset— the config. Defines fire modes, meshes, montages, IK, sway, lag, and all per-weapon tuning.
No reload. SystemLink uses a single ammo pool per weapon —CurrentAmmois the only counter, no reserve, no magazine.MaxAmmois set deliberately high (e.g. AR = 600, Shotgun = 60) so reload is unnecessary. Pickups refillCurrentAmmodirectly, clamped toMaxAmmo.Docs/WeaponReloadPipeline.mdis preserved as a "what if" reference only — see the banner at the top of that file.
USLWeaponsComponent is the weapon "brain" on the character — it reads config from the actor's data asset and drives everything from equipping to shot validation.
See WeaponActor.md for the full weapon actor lifecycle, ammo model, and API.
Adding a new weapon? Follow NewWeaponChecklist.md — end-to-end steps for tags, meshes, sockets, data asset, abilities, cues, animations, HUD, and pickup. Assault Rifle is the canonical reference.
#Data Asset — USLWeaponDataAsset
One data asset per weapon (e.g. DA_AssaultRifle). Contains everything needed to equip and fire the weapon.
#First & Third Person Mesh Data — FSLWeaponSkeletalMeshData
Two instances on every data asset: FirstPersonSkeletalMeshData and ThirdPersonSkeletalMeshData.
| Field | Purpose |
|---|---|
WeaponMesh | Skeletal mesh asset for this view |
AttachSocketName | Socket on the character mesh to attach the weapon to (e.g. hand_r) |
AnimLayerClass | Anim layer linked into the character mesh ABP for arm poses and IK |
WeaponMeshAnimClass | Standalone ABP set directly on the weapon mesh (bolt cycling, fire anims). Set to null for static/no-anim weapons — the previous weapon's class is explicitly cleared on equip. |
EquipMontage | Animation montage to play when this weapon is equipped (FP montage on FirstPersonSkeletalMeshData, TP montage on ThirdPersonSkeletalMeshData) |
RightHandBoneName | Reference bone for left-hand IK BoneSpace conversion (typically hand_r) |
LeftHandIKEffectorSocketName | Weapon mesh socket used as the left-hand IK end-effector target |
LeftHandIKJointTargetSocketName | Optional weapon mesh socket for the IK pole vector / joint target |
LeftHandIKAlpha | 0–1 blend weight for left-hand IK |
#Fire Modes — FSLWeaponFireMode
Two instances on every data asset: PrimaryFireMode and SecondaryFireMode. Both are the same struct.
| Field | Purpose |
|---|---|
bEnabled | Whether this fire mode is active |
FireType | Hitscan or Projectile |
FireMode | FullAuto or SingleShot |
FireAbilityClass | GAS ability class granted to the ASC when this weapon is equipped |
RoundsPerMinute | Full-auto fire rate; drives the repeat timer interval in the ability |
MaxAmmo | Maximum ammo capacity for this fire mode. ASLWeaponActor initializes CurrentAmmo to this value on spawn. |
BaseDamage | Damage applied per shot (used by both authority paths) |
MaxRange | Hitscan trace length in Unreal units |
AmmoDecrementEffect | GAS UGameplayEffect applied per shot on authority to decrement USLAmmoAttributeSet.CurrentAmmo by 1. Leave unset for infinite-ammo fire modes. |
MuzzleSocketName | FP weapon mesh socket used for muzzle location (cosmetics + projectile spawn) |
ProjectileClass | Actor class to spawn (Projectile fire type only) |
FireCueTag | Gameplay Cue tag executed on every shot to drive replicated cosmetics — muzzle flash, sound, shell eject. Must start with GameplayCues.. Each weapon/fire mode can specify a different tag for independent VFX/SFX. |
RecoilPitchKick | Degrees of upward camera kick per shot |
RecoilYawRange | Max random yaw deviation per shot (±this value) |
RecoilRecoverySpeed | Interp speed at which accumulated recoil returns to zero (shared by camera rotation and viewmodel position) |
RecoilMaxAccumulation | Max pitch accumulation in degrees (prevents runaway recoil) |
RecoilKickbackDistance | Viewmodel kick distance in cm per shot — weapon moves away from the camera |
RecoilLateralRange | Max random lateral viewmodel shift in cm per shot (±this value) |
#Equip Fields
| Field | Purpose |
|---|---|
EquipAbilityClass | Per-weapon equip ability class (Server Initiated). Granted and activated by SetPendingEquipWeapon on the server. The trigger tag is read directly from its CDO — no separate tag field needed. Revoked on the next equip or unequip. |
EquipCueTag | Gameplay Cue tag executed on equip to drive replicated cosmetics (holster sounds, attachment clanks). Must start with GameplayCues.. |
See WeaponEquip.md for the full equip flow, replication model, and step-by-step Blueprint implementation.
#Other Data Asset Fields
| Field | Purpose |
|---|---|
SwaySettings | Per-weapon sway tuning (idle/movement sway curves) |
LagSettings | Per-weapon lag tuning (look-input rotational lag) |
FPWeaponBlockingTraceDistance | Forward trace distance for wall-proximity viewmodel pull-back |
PrimaryReticleClass | HUD reticle widget class shown in primary fire mode |
SecondaryReticleClass | HUD reticle widget class shown in secondary fire mode |
AmmoWidgetClass | HUD ammo display widget class shown when this weapon is equipped |
SpreadSettings | Movement-based crosshair spread tuning (shared across fire modes) |
WeaponIconMaterial | Optional UMaterialInterface* used to tint or stylize the weapon's HUD icon at runtime |
#Weapons Component — USLWeaponsComponent
Attached to ASLCharacterBase. Replicated. Does not tick by default — tick is enabled only when recoil recovery is active.
#Responsibilities
- Holds
FSLWeaponViewDriverFPandFSLWeaponViewDriverTPfor per-view attachment and anim class linking
- Manages FP/TP mesh attachment, anim layer linking, and equip/unequip state
- Grants and revokes fire abilities on the ASC when the weapon changes
- Initializes
USLAmmoAttributeSetfrom the weapon actor on equip; writes ammo back on unequip
- Broadcasts
OnAmmoChanged(CurrentAmmo, MaxAmmo)to the HUD whenever ammo changes
- Drives recoil — applies kick on fire, recovers toward zero each tick
- Accumulates client-predicted shots and flushes them to the server for validation
- Exposes sway, lag, IK, and obstruction calculations for animation blueprints
- Broadcasts
OnEquippedWeaponChangedto the HUD for reticle swaps
- Calls
OnAttachedToCharacter/OnDetachedFromCharacteron the weapon actor for per-weapon visual setup
#Initialization
Called by the character in BeginPlay:
WeaponsComponent->InitializeWeaponMeshes(TPCharMesh, FPCharMesh, TPWeaponMesh, FPWeaponMesh);
#HUD Integration
The weapons component exposes BlueprintAssignable delegates that drive all weapon-related HUD updates. Bind them in WBP_SL_HUDWidget (the USLHUDWidget BP subclass) — that is where the live weapon/reticle bindings actually are. Note BP_SL_PlayerHUD is the AHUD actor (ASLPlayerHUD); it only creates the widget and does NOT hold these bindings.
#Reticle
OnEquippedWeaponChanged(ASLWeaponActor* NewWeapon) — fires on all clients when the equipped weapon changes.
Bind it to call HUDWidget → SwapReticle(NewWeapon → WeaponData → PrimaryReticleClass, PC). The reticle lives in ReticleContainer (center-screen overlay in WBP_SL_HUD) and is an instance of USLReticle. Drive it each tick via:
OnSpreadChanged(float NewSpread)— fed fromWeaponsComponent → GetCurrentSpread(PlayerSpeed)
OnTargetDetected(bool bIsHostile)— fed fromWeaponsComponent → CheckForEnemyTarget(...)
#Ammo Display — Multi-Slot Strip
The HUD shows a stacked weapon-slot strip (Halo 3 style): the equipped weapon is on top (full size, full opacity), secondary weapons below (smaller, dimmed). All carried weapons show their live ammo counts simultaneously.
Class hierarchy:
USLAmmoWidget(C++) — base class for the per-weapon ammo display. OneBlueprintImplementableEvent:OnAmmoChanged(int32 CurrentAmmo, int32 MaxAmmo)— update counter text, drive low-ammo feedback
WBP_SL_Ammo_<WeaponName>(Blueprint) — per-weapon visual implementation; set onDA_SL_<WeaponName>.AmmoWidgetClass
USLAmmoStrip(C++) — base class for the strip container. Manages slot lifecycle, delegate binding, andRebuildSlots. BP subclass implements two hooks:CreateSlotWidget(WeaponActor, bIsActive) → UUserWidget*— create and return a slot widget (typically anUSLAmmoWidgetsubclass viaAmmoWidgetClass; callSetWeapon(WeaponActor)to subscribe it to ammo updates)SetSlotActive(SlotWidget, bIsActive)— drive scale/opacity/animation per slot state
WBP_SL_AmmoStrip(Blueprint) — subclass ofUSLAmmoStrip; implements the two BIE hooks above
Initialization: ASLPlayerCharacter::InitializeLocalPlayerHUD calls HUDWidget→InitializeAmmoStrip(Character) on every possession. The strip auto-binds to OnCarriedWeaponsChanged (rebuild on inventory change) and OnEquippedWeaponChanged (rebuild with new weapon on top). No wiring needed in BindWeaponDelegates.
Live ammo updates: Each slot's USLAmmoWidget is bound directly to its ASLWeaponActor::OnAmmoChanged delegate via SetWeapon(WeaponActor). Shots decrement via PostGameplayEffectExecute mirror writes; ammo pickups call WeaponActor::SetCurrentAmmo directly — both broadcast the delegate, both update the strip immediately.
#Timed Notifications
USLHUDWidget::ShowTimedNotification(WidgetClass, Data, Duration) places a transient notification in NotificationContainer and auto-dismisses it after Duration seconds (default 3.0). Any previously active notification is replaced immediately. Use this for ammo pickup feedback, weapon pickup confirmations, and any other short-lived HUD messages.
FSLNotificationData carries: Message, SubMessage, WeaponData, Icon, and Sound. Build one with USL_BlueprintLibrary::MakeNotificationDataFromWeapon(WeaponData, Message, SubMessage) — it auto-fills Icon from WeaponData->WeaponIcon and defaults SubMessage to the weapon display name if left empty.
Ammo pickup notifications route through Client_ShowAmmoPickupNotification(WeaponData) on ASLCharacterBase (server → owning client RPC) → OnAmmoPickupNotification(WeaponData) BlueprintImplementableEvent on the character → character BP calls ShowTimedNotification.
#Equip Flow
RequestEquip(WeaponActor) is called on the server with an ASLWeaponActor reference. SetPendingEquipWeapon finalizes all state immediately — no coordinator ability, no anim-notify dependency. EquippedWeapon on USLWeaponsComponent (now an ASLWeaponActor*) is the replicated source of truth for all machines including bots. The per-weapon equip ability (Server Initiated) handles animation only.
See WeaponEquip.md for the full flow, replication model, and Blueprint implementation guide.
#World Pickups
See WeaponPickup.md for the full pickup system design, prompt flow, and editor tasks.
Two paths on overlap:
- Player already carries this weapon type — ammo tops up silently via
USLWeaponsComponent::AddAmmo, pickup destroys itself
- New weapon — prompt shown to owning client via
Client_ShowPickupPrompt → OnShowPickupPromptBP event; player presses interact to callServer_AcceptPickup, which grants the weapon and destroys the pickup
#Fire Flow
#Authority path (listen server host)
Input → SLTags::Events::Weapon::PrimaryFire sent to ASC
└── GA_PrimaryFire activates (InstancedPerActor)
└── CommitAbility (applies SLTags::States::Weapon::Firing)
└── ExecuteFire()
└── Gets camera view point and fire direction
└── Calls OnLocallyPredictedShotFired(MuzzleLoc, FireDir) — BP event for local cosmetics (FP muzzle flash, sound, TP cosmetics for the shooting player)
└── Calls USLWeaponsComponent::ApplyRecoilKick
└── Calls ASC::ExecuteGameplayCue(FireMode.FireCueTag, CueParams) — replicated to all clients
└── Line traces or spawns projectile, applies damage directly
└── FullAuto: sets timer at RPM interval, repeats ExecuteFire()
└── SingleShot: ends ability immediately
#Remote client path
Input → SLTags::Events::Weapon::PrimaryFire sent to ASC (locally predicted)
└── GA_PrimaryFire activates
└── ExecuteFire()
└── Calls OnLocallyPredictedShotFired — local FP + TP cosmetics for the shooting player
└── Calls ApplyRecoilKick
└── Prediction trace: if hit → calls OnProjectileHitPredicted(Hit) for immediate impact FX
└── Collects FSLShotInfo per pellet → after the pellet loop, calls USLWeaponsComponent::QueueShotsForValidation(PendingPellets)
└── Buffered until MaxUnvalidatedShots threshold or ShotValidationInterval timer
└── Flushes via Server_ProcessShots RPC (single RPC per trigger pull, even for shotgun blasts)
└── Server re-traces, applies damage, calls Multicast_OnShotsConfirmed
└── All clients receive OnShotsConfirmed — play confirmed impact VFX/audio
#Gameplay Cues
Gameplay Cues drive replicated cosmetics — effects that all clients need to see (TP muzzle flash, fire sound for remote players, shell ejection, etc.). Local cosmetics for the shooting player go in OnLocallyPredictedShotFired instead.
#Fire Cue Pattern
- Each fire mode on the weapon data asset has a
FireCueTag(FGameplayTag, filtered toGameplayCues.*)
ExecuteFirecallsASC::ExecuteGameplayCue(FireMode.FireCueTag, CueParams)on authority only
- GAS replicates the cue execution to all clients
- The matching
GC_Blueprint (GameplayCueNotify_Burst) handles muzzle flash, sound, and any other per-shot effects
CueParams.Location= muzzle world position,CueParams.Normal= fire direction
#Equip Cue Pattern
- The weapon data asset has an
EquipCueTag(FGameplayTag, filtered toGameplayCues.*)
- The per-weapon equip ability Blueprint calls
ExecuteEquipCue(WeaponData)(provided byUSLGameplayAbility_Equip) at the desired point in the animation
- GAS replicates the cue to all clients
- The matching
GC_Blueprint (GameplayCueNotify_Burst) handles equip sounds and any visual effects
#Adding a new weapon's fire cue
- In Project Settings → GameplayTags, add the tag (e.g.
GameplayCues.Weapon.AssaultRifle.PrimaryFire)
- Create a
GameplayCueNotify_BurstBlueprint inContent/SystemLink/AbilitySystem/Cues/Weapons/<WeaponName>/
- Set its Gameplay Cue Tag to the new tag
- Wire up Burst Particles and Burst Sounds — see the Authoring a Fire Cue section below for the required local-player guard
- Open the weapon's data asset and set
PrimaryFireMode.FireCueTagto the same tag
#Authoring a Fire Cue (hard rule)
ASC->ExecuteGameplayCue multicasts to every client including the shooter. The shooter already played their FP muzzle flash and fire sound inside OnLocallyPredictedShotFired (zero-latency, locally predicted). If the GC blueprint plays those effects again on the local player, every shot double-plays.
Every fire GC_ Blueprint must early-return when the instigator is locally controlled. First thing inside OnExecute / OnBurst:
Event OnExecute (Cue Parameters)
│
├── Instigator = Cue Parameters → Instigator (cast to Pawn)
├── [IsValid(Instigator) && Instigator->IsLocallyControlled()] → Return
│
└── (continue with muzzle flash, fire sound, shell eject…)
This guard does not apply to ImpactCueTag blueprints — impacts are different. On the owning client, OnProjectileHitPredicted runs ONLY when the local trace confirms a hit; the server cue path then covers everyone else. The two paths don't overlap on the same impact for the same client. (If you find them overlapping, that's a bug in the GC blueprint, not the dispatch.)
The same hard rule applies if you add a StopFireCueTag blueprint — the local player already handled "stopped firing" feedback inline.
#Adding a new weapon's equip cue
- In Project Settings → GameplayTags, add the tag (e.g.
GameplayCues.Weapon.AssaultRifle.Equip)
- Create a
GameplayCueNotify_BurstBlueprint inContent/SystemLink/AbilitySystem/Cues/Weapons/<WeaponName>/
- Set its Gameplay Cue Tag to the new tag
- Open the weapon's data asset and set
EquipCueTagto the same tag
The GAS Cue Manager scans Content/SystemLink/AbilitySystem/Cues/ recursively (configured in Config/DefaultGame.ini under AbilitySystemGlobals.GameplayCueNotifyPaths) and registers all GC_ assets automatically by tag — no manual registration needed. Subdirectories are fine.
#Shot Validation
Client-predicted shots are queued in PendingShotHistory and flushed to the server via Server_ProcessShots. The server re-traces each shot and applies damage if the trace confirms a hit. A sphere sweep fallback (HitValidationSweepRadius) provides latency forgiveness. Confirmed hits are broadcast to all clients via Multicast_OnShotsConfirmed → OnShotsConfirmed BP event for impact VFX.
| Property | Purpose |
|---|---|
MaxUnvalidatedShots | How many shots to accumulate before flushing (default 1) |
ShotValidationInterval | Safety-net flush timer in seconds (default 0.15) |
HitValidationSweepRadius | Sphere sweep fallback radius in UU (0 = strict line trace only) |
#Recoil
ApplyRecoilKick is called per shot in ExecuteFire. It adds pitch and random yaw to AppliedRecoil (capped by RecoilMaxAccumulation) and feeds it to the player controller's input via AddPitchInput / AddYawInput. The component ticks while recoil is non-zero and interpolates AppliedRecoil back to zero at RecoilRecoverySpeed. GetRecoilMagnitude() is available to animation blueprints for TP additive poses.
#IK
Left-hand IK targets are calculated by USLWeaponsComponent and read each tick by the animation blueprints:
CalculateLeftHandIKTargets_FP()— first-person arm IK
CalculateLeftHandIKTargets_TP()— third-person arm IK
Both use the weapon mesh sockets defined in FSLWeaponSkeletalMeshData (LeftHandIKEffectorSocketName, LeftHandIKJointTargetSocketName). SetLeftHandIKOverride / ClearLeftHandIKOverride allow abilities (e.g. equip) to temporarily override the IK target.
#Sway & Lag
Called by animation blueprints each frame:
CalculateWeaponSwayTransform(LookX, LookY, DeltaTime, PlayerSpeed)— idle and movement sway driven byUSLWeaponSwaySettings
CalculateWeaponLag(LookYawRate, LookPitchRate, DeltaTime, AdsAlpha)— rotational lag driven byUSLWeaponLagSettings
Both return transforms/rotators that animation blueprints apply as additive offsets to the weapon mesh.
#View Drivers
FSLWeaponViewDriverFP and FSLWeaponViewDriverTP are internal structs owned by USLWeaponsComponent. They handle per-view weapon attachment (EquipWeapon / UnequipWeapon), anim class layer linking, and FP obstruction state (bIsFPWeaponObstructed, FPWeaponObstructionAlpha). Animation blueprints read obstruction state via AnimStateSnapshot.bIsFPWeaponObstructed and AnimStateSnapshot.FPWeaponObstructionAlpha on the character — these are populated each tick by ASLPlayerCharacter::CalculateFirstPersonWeaponBlocking. The legacy accessors IsFPWeaponObstructed() / FPWeaponObstructionAlpha() on the component are deprecated.