March 2025 – by the Cockfite Dev Team
Overview
In this development blog, we’re highlighting the final structure of our player inventory system, with a particular focus on how items are picked up and added to a player’s inventory. This includes robust validation logic, Fusion state synchronization, and flexible support for weapons and ammunition. Below, we’ll walk you through the most important components and share code insights.
StaticPickup: Our World Item Blueprint
We use a StaticPickup
base class to represent world-spawned collectables. It includes cooldowns, validation, and visibility logic.
public bool CanPickup()
{
return Object != null &&
Object.IsValid &&
!m_consumed &&
m_despawnCooldown.ExpiredOrNotRunning(Runner) &&
!m_isDisabled &&
m_dropCooldown.ExpiredOrNotRunning(Runner);
}
public void ConsumePickup(PlayerRef _owner)
{
m_owner = _owner;
m_consumed = true;
if (m_despawnDelay > 0f)
{
m_despawnCooldown = TickTimer.CreateFromSeconds(Runner, m_despawnDelay);
}
}
The actual WeaponPickup
subclass extends this for specific item types (weapons with ammo):
public sealed class WeaponPickup : StaticPickup, IInventoryItem
{
[SerializeField] int AmmoAmount = 1;
public short CurrentAmmo => (short)AmmoAmount;
public override void DropPickup(short _ammo)
{
base.DropPickup(_ammo);
AmmoAmount = _ammo;
}
}
Collision & Item Collection
In the PlayerInventory
, we use a FixedUpdateNetwork()
loop with a OverlapCapsule()
check to detect items:
private void CheckForCollision()
{
int hits = Runner.GetPhysicsScene().OverlapCapsule(
transform.position,
transform.position + Vector3.up * m_capsuleColliderHeight,
m_capsuleColliderRadius,
m_collisionTestColliders,
m_collisionTestMask,
QueryTriggerInteraction.Collide);
for (int i = 0; i < hits; i++)
{
if (!HasStateAuthority)
return;
if (m_collisionTestColliders[i].TryGetComponent(out StaticPickup pickup))
{
if (pickup.CanPickup())
{
if (AddItemToInventory(pickup))
{
pickup.ConsumePickup(Object.InputAuthority);
}
}
}
}
}
Adding Items to the Inventory
The method AddItemToInventory()
handles stacking and slot allocation. It first tries to merge ammo with an existing item:
for (int i = 0; i < m_itemSlots.Length && rest > 0; i++)
{
var iitem = m_itemSlots[i].GetComponent<IInventoryItem>();
if (iitem.ItemId.Equals(_inventoryItem.ItemId))
{
if (iitem.InventoyItemType == InventoyItemType.Weapon)
{
var item = Runner.FindObject(iitem.ItemNetworkId);
if (item != null && item.TryGetComponent(out Weapon weapon))
{
if (weapon.UpdateAmmo(rest, out short newRest))
{
rest = newRest;
if (rest == 0) return true;
}
}
}
}
}
If there’s leftover ammo and an empty slot, a new item is spawned:
var weapon = Runner.Spawn(prefab, null, null, Object.InputAuthority, (_rnr, _no) =>
{
_no.GetComponent<IInventoryItem>().Initalize(
GetComponent<CF_Avatar>().GetSpawnPointTransform(_inventoryItem.SpawnTransform),
(sbyte)i,
Object.InputAuthority,
Context,
ammoToAssign);
});
Inventory Debugging
We use gizmos in-editor to visualize drop and collision zones. This has been essential for debugging:
private void OnDrawGizmosSelected()
{
Gizmos.color = Color.cyan;
Vector3 start = transform.position;
Vector3 end = transform.position + Vector3.up * m_capsuleColliderHeight;
Gizmos.DrawWireSphere(start, m_capsuleColliderRadius);
Gizmos.DrawWireSphere(end, m_capsuleColliderRadius);
}
Summary
- Pickup logic is handled via a dedicated class that validates cooldowns, state, and consumption.
- Inventory merging of ammo is deterministic and synchronized with Fusion’s network state.
- Fallback spawning fills empty slots when merging isn’t possible.
- State Authority only controls sensitive updates like despawning and inventory logic.
This foundation now allows us to confidently build gameplay systems around reliable item interactions. Coming up next: synced projectile logic and weapon cooldown refactors.
Thanks for reading!