15 KiB
Item Example — Pistol (Weapon)
Item Type: Weapon
Complexity: High
Systems Used: DA_ItemData, BP_ItemPickup, BP_WeaponBase, BPC_FirearmSystem, BPC_AmmoComponent, BPC_DamageReceptionSystem, BPC_RecoilSystem, BPC_ReloadSystem, BPC_EquipmentSlotSystem, BPC_InventorySystem, I_UsableItem, I_Damageable
What You Learn: Full weapon pipeline — Data Asset with EquipmentData, pickup-to-equip flow, frame-driven fire with hitscan, ammo management, damage application, recoil
1. Create the DA_ItemData
1.1 — Create the Data Asset
Content Browser → Game/Items/ (or Game/Weapons/)
Right-click → Miscellaneous → Data Asset
Class: DA_ItemData
Name: DA_Item_Pistol
1.2 — Fill Core Properties
| Property | Value | Notes |
|---|---|---|
Item Tag |
Framework.Item.Weapon.Pistol |
Registered in DA_GameTagRegistry |
Display Name |
"9mm Pistol" | |
Description |
"A standard-issue 9mm semi-automatic pistol." | |
Icon |
T_Pistol_Icon |
|
World Mesh |
SM_Pistol |
Soft reference |
Weight |
3.0 |
Heavier than consumables |
Stack Limit |
1 |
Cannot stack weapons |
Item Type |
Weapon |
Triggers Equipment Data panel |
1.3 — Fill Equipment Data
When ItemType == Weapon, the Equipment Data panel appears:
| Field | Value | Notes |
|---|---|---|
Slot |
Framework.Equipment.Slot.PrimaryWeapon |
Occupies player's primary weapon slot |
Damage |
15.0 |
Base damage per shot |
Fire Rate |
0.15 |
Seconds between shots (400 RPM) |
Range |
5000.0 |
Hitscan range in Unreal units |
Magazine Size |
12 |
Rounds per magazine |
Reload Time |
2.0 |
Seconds to reload |
1.4 — Save
2. Create the BP_Pickup_Pistol (World Pickup)
Same as other pickups, with weapon mesh:
2.1 — Blueprint + Components
Content Browser → Game/Pickups/
Right-click → Blueprint Class → Actor
Name: BP_Pickup_Pistol
Components:
├── StaticMeshComponent "Mesh"
└── SphereComponent "PickupTrigger" (radius 200, OverlapOnlyPawn)
2.2 — Variable & Construction Script
Same pattern as flashlight — ItemData → Get World Mesh → Load Synchronous → Set Static Mesh.
2.3 — I_Interactable Wiring
Same as other pickups, but after adding item to inventory, also auto-equip it:
Event Interact
│
├─ Interactor → Get BPC_InventorySystem
├─ CanAddItem(ItemData, 1) → True → AddItem → store result
│ │
│ ├─ Interactor → Get BPC_EquipmentSlotSystem
│ │ └─ Equip To Slot(ItemData, PrimaryWeapon slot tag)
│ │ └─ This spawns the BP_Pistol actor at the player's hand socket
│ │
│ ├─ Destroy Actor
│ └─ Return True
│
└─ False → Print "Inventory Full" → Return False
3. Create the BP_Pistol (Held Weapon Actor)
This is the actor that appears in the player's hand when equipped.
3.1 — Create Blueprint
Content Browser → Game/Actors/ (or Game/Weapons/)
Right-click → Blueprint Class → Actor (or BP_WeaponBase from framework)
Name: BP_Pistol
3.2 — Add Components
| # | Component | Name | Purpose |
|---|---|---|---|
| 1 | SkeletalMeshComponent |
WeaponMesh |
Animated weapon (slide, magazine) |
| 2 | SceneComponent |
MuzzleSocket |
Where the bullet trace starts (attach to muzzle bone) |
| 3 | AudioComponent |
FireSound |
Plays fire sound |
| 4 | AudioComponent |
ReloadSound |
Plays reload sound |
| 5 | ParticleSystemComponent |
MuzzleFlash |
Fire particle effect |
| 6 | (Optional) PointLightComponent |
MuzzleLight |
Brief flash on fire |
Parent MuzzleSocket to the muzzle bone on WeaponMesh, or set its relative location to where the barrel ends.
3.3 — Variables
| Variable | Type | Default | Purpose |
|---|---|---|---|
CurrentAmmoInMag |
Integer | 12 |
Rounds currently loaded |
MaxMagazineSize |
Integer | 12 |
From EquipmentData |
bIsFiring |
Boolean | false |
Prevents re-trigger during fire rate cooldown |
FireRateTimer |
Float | 0.15 |
Seconds between shots |
WeaponData |
DA_ItemData* | — | Reference to the pistol Data Asset |
FireRange |
Float | 5000.0 |
Hitscan distance |
3.4 — Interfaces
Class Settings → Interfaces → Add:
UUsableItem— so active item system can route fire input
3.5 — Fire Logic (Hitscan)
Event Use Item (Interface -> UUsableItem)
│ User: AActor*, Target: AActor*
│ Return Boolean
│
├─ bIsFiring? → Branch
│ ├─ True → Return False (fire rate lockout)
│ └─ False:
│ │
│ ├─ CurrentAmmoInMag > 0? → Branch
│ │ ├─ True:
│ │ │ ├─ Set bIsFiring = true
│ │ │ ├─ CurrentAmmoInMag = CurrentAmmoInMag - 1
│ │ │ │
│ │ │ ├─ [Play Effects]
│ │ │ │ ├─ MuzzleFlash → Activate
│ │ │ │ ├─ FireSound → Play
│ │ │ │ └─ MuzzleLight → Set Visibility (true)
│ │ │ │ └─ Delay 0.05 → Set Visibility (false)
│ │ │ │
│ │ │ ├─ [Line Trace — Hitscan]
│ │ │ │ │
│ │ │ │ ├─ Get Player Controller → Get Camera Location/Direction
│ │ │ │ ├─ Start: MuzzleSocket → Get World Location
│ │ │ │ ├─ End: Start + (CameraForward × FireRange)
│ │ │ │ │
│ │ │ │ └─ Line Trace By Channel (Visibility)
│ │ │ │ ├─ Trace Complex: false
│ │ │ │ └─ Ignore Self, Ignore Owning Pawn
│ │ │ │ │
│ │ │ │ ├─ Hit? → Branch
│ │ │ │ │ │
│ │ │ │ │ ├─ True:
│ │ │ │ │ │ ├─ Hit Actor → Does Implement Interface (UDamageable)?
│ │ │ │ │ │ │ ├─ True:
│ │ │ │ │ │ │ │ ├─ Call I_Damageable.Take Damage
│ │ │ │ │ │ │ │ │ ├─ Damage Amount: WeaponData → Get Equipment Data → Damage
│ │ │ │ │ │ │ │ │ ├─ Damage Causer: Self (or OwningPawn)
│ │ │ │ │ │ │ │ │ ├─ Damage Type: Framework.Combat.Damage.Physical
│ │ │ │ │ │ │ │ │ ├─ Hit Location: HitResult.ImpactPoint
│ │ │ │ │ │ │ │ │ └─ Hit Direction: (ImpactPoint - Start) normalized
│ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ └─ Print String: "Hit {HitActor} for {Damage} damage"
│ │ │ │ │ │ │ │
│ │ │ │ │ │ │ └─ False: Print "Hit non-damageable object"
│ │ │ │ │ │ │
│ │ │ │ │ │ └─ Spawn Decal/Impact Effect at Hit Location
│ │ │ │ │ │
│ │ │ │ │ └─ False (miss): nothing
│ │ │ │ │
│ │ │ │ └─ Draw Debug Line (red for 1 second, editor only)
│ │ │ │
│ │ │ ├─ [Recoil]
│ │ │ │ └─ Get BPC_RecoilSystem (on pawn) → Apply Recoil (WeaponData)
│ │ │ │
│ │ │ └─ Delay (FireRateTimer) → Set bIsFiring = false → Return True
│ │ │
│ │ └─ False: (out of ammo)
│ │ ├─ Play click/empty sound
│ │ ├─ (Optional) Auto-reload → Call Reload
│ │ └─ Return False
3.6 — Reload Logic
Function: Reload
│
├─ CurrentAmmoInMag >= MaxMagazineSize? → Return (already full)
├─ Set bIsReloading = true
│
├─ Get Owning Pawn → Get BPC_AmmoComponent
│ │
│ └─ Get Ammo Count (AmmoTypeTag = Framework.Item.Ammo.Pistol9mm)
│ │
│ ├─ Branch (> 0?)
│ │ ├─ True:
│ │ │ ├─ Rounds To Load = Min(AmmoAvailable, MaxMagazineSize - CurrentAmmoInMag)
│ │ │ ├─ CurrentAmmoInMag += Rounds To Load
│ │ │ ├─ AmmoComponent → Consume Ammo (RoundsToLoad)
│ │ │ └─ Play ReloadSound
│ │ │
│ │ └─ False: Print "No ammo" → Return
│ │
│ ├─ Delay (ReloadTime) → Set bIsReloading = false
│ └─ Broadcast OnReloadComplete
4. Wire BPC_AmmoComponent (On Player Pawn)
Create BP child of BPC_AmmoComponent (spec #70):
| Variable | Type | Default | Purpose |
|---|---|---|---|
AmmoPool |
Map (GameplayTag → Integer) | Empty | AmmoType → Count |
Function: Get Ammo Count (AmmoTypeTag) → Integer
│
└─ AmmoPool.Find(AmmoTypeTag) → Return count (0 if not found)
Function: Consume Ammo (AmmoTypeTag, Amount)
│
├─ Current = Get Ammo Count (AmmoTypeTag)
├─ Current -= Amount
├─ Clamp to 0 minimum
└─ AmmoPool[AmmoTypeTag] = Current
Function: Add Ammo (AmmoTypeTag, Amount)
│
├─ AmmoPool.Find or Add (AmmoTypeTag)
└─ Increment by Amount
5. Wire BPC_RecoilSystem (On Player Pawn)
Create BP child of BPC_RecoilSystem (spec #77):
Function: Apply Recoil (WeaponData: DA_ItemData*)
│
├─ WeaponData → Get Equipment Data → Get Damage (or FireRate)
├─ Calculate kick: PitchUp = random(1.0, 2.5), YawSide = random(-0.5, 0.5)
│
├─ Get Player Controller → Add Controller Pitch Input (PitchUp × -1)
├─ Get Player Controller → Add Controller Yaw Input (YawSide)
│
└─ (For smooth recovery, use a Timeline or interp to return to original aim)
6. Create DA_Item_Ammo_Pistol9mm (Ammo Data Asset)
Content Browser → Game/Items/
Right-click → Miscellaneous → Data Asset → DA_ItemData
Name: DA_Item_Ammo_Pistol9mm
Properties:
ItemTag: Framework.Item.Ammo.Pistol9mm
DisplayName: "9mm Rounds"
ItemType: Ammo
StackLimit: 999
Weight: 0.05
(AmmoData sub-panel auto-appears — set PerPickupCount = 15)
Create BP_Pickup_Ammo_Pistol9mm following the same pattern as other pickups. On interact, call BPC_AmmoComponent.Add Ammo(AmmoTypeTag, PerPickupCount) instead of adding to inventory.
7. Verification Checklist
Step 1 — Pickup:
- Pistol pickup in level → mesh visible → interact → goes to inventory
- Auto-equips to primary weapon slot (weapon appears in player's hand)
Step 2 — Fire:
- Equip pistol, press fire → muzzle flash + sound + hitscan line
- Hit an enemy actor implementing
I_Damageable→ health decreases - Ammo counter decreases (12 → 11 → 10...)
- Camera kicks slightly on each shot (recoil)
- Fire rate enforced: cannot spam fire faster than 0.15s
Step 3 — Empty Magazine:
- Fire all 12 rounds → click sound plays, no trace, no damage
- Auto-reload? Or manual reload prompt appears
Step 4 — Reload:
- Press Reload key → reload animation/sound plays
- Ammo transfers from pool to magazine
- Magazine shows 12 rounds again
- If no ammo in pool → reload fails
Step 5 — Ammo Pickup:
- Pick up
BP_Pickup_Ammo_Pistol9mm→ ammo pool increases by 15 - Reload → magazine fills, pool decreases by rounds loaded
Step 6 — Damage Pipeline:
- Enemy pawn has
BPC_DamageReceptionSystem+BPC_HealthSystem - Fire at enemy →
ApplyDamage(15.0, ...)called - Damage pipeline: resistance → armor → shield → health
- Enemy health decreases proportionally
8. Weapon Data Flow (Complete)
Player Input (IA_Fire)
│
└─ BPC_ActiveItemSystem routes to equipped weapon
│
└─ BP_Pistol (implements I_UsableItem)
│
├─ [Ammo Check] → BPC_AmmoComponent (in-mag count)
├─ [Hitscan] → Line Trace from MuzzleSocket
│ │
│ └─ Hit Actor (implements I_Damageable)
│ │
│ └─ BPC_DamageReceptionSystem.ApplyDamage(RawDamage, Causer, Type, Location, Direction)
│ │
│ ├─ Calculate Resistance
│ ├─ Apply Shield (BPC_ShieldDefenseSystem)
│ ├─ Apply Health (BPC_HealthSystem)
│ ├─ Evaluate Hit Reaction (BPC_HitReactionSystem)
│ │ ├─ Stagger? (damage > threshold)
│ │ └─ Knockdown? (damage > higher threshold)
│ │
│ └─ Broadcast OnDamageReceived → UI, audio, metrics
│
├─ [Recoil] → BPC_RecoilSystem.ApplyRecoil()
├─ [Effects] → MuzzleFlash, FireSound, ImpactDecal
└─ [Fire Rate] → Timer lockout until next shot
When magazine empty:
└─ Player presses IA_Reload
└─ BP_Pistol.Reload()
└─ BPC_AmmoComponent.ConsumeAmmo() (from pool)
└─ BP_Pistol.CurrentAmmoInMag += rounds loaded
9. Key Differences — Weapon vs Tool vs Consumable
| Aspect | Pistol (Weapon) | Flashlight (Tool) | MedKit (Consumable) |
|---|---|---|---|
| ItemType | Weapon |
Tool |
Consumable |
| Slot | PrimaryWeapon |
Tool |
None (inventory bag) |
| Use action | Frame-driven (continuous) | Toggle (single click) | One-shot (consumed) |
| Ammo | Yes (magazine + pool) | No (battery as optional resource) | No |
| Damage output | Yes | No | No (heals) |
| Fire rate | Throttled (timer) | N/A | Use duration (2s anim) |
| Recoil | Yes | No | No |
| Reload | Yes | No | No |
| Stackable | No (StackLimit=1) | No (StackLimit=1) | Yes (StackLimit=5) |
| Interfaces | I_UsableItem |
I_UsableItem + I_Toggleable |
I_UsableItem |
| Pickup → Equip | Auto-equips to slot | Auto-equips to slot | Goes to inventory only |