menu_bookDocumentation
Sandbox: PlayerInventory — weapon slots, pickup, drop, and loadout persistence
Sandbox PlayerInventory — Weapon Slots, Pickup, Drop, and Loadout Persistence
PlayerInventory manages up to 6 weapon slots, handles pickup/drop logic, and delegates loadout save/restore to PlayerLoadout.Slot system
CSHARP
public sealed class PlayerInventory : Component, Local.IPlayerEvents
{
[Property] public int MaxSlots { get; set; } = 6;
// All weapons ordered by slot number
public IEnumerable<BaseCarryable> Weapons =>
GetComponentsInChildren<BaseCarryable>( true ).OrderBy( x => x.InventorySlot );
// Synced from host — change callback enables/disables weapon GameObjects
[Sync( SyncFlags.FromHost ), Change]
public BaseCarryable ActiveWeapon { get; private set; }
public void OnActiveWeaponChanged( BaseCarryable oldWeapon, BaseCarryable newWeapon )
{
if ( oldWeapon.IsValid() ) oldWeapon.GameObject.Enabled = false;
if ( newWeapon.IsValid() )
{
newWeapon.GameObject.Enabled = true;
newWeapon.SetDropped( false );
}
}
}Pickup
CSHARP
public bool Pickup( string prefabName, bool notice = true )
{
if ( !Networking.IsHost ) return false;
var prefab = GameObject.GetPrefab( prefabName );
if ( !prefab.IsValid() ) return false;
var slot = FindFreeSlot();
if ( slot < 0 ) return false; // Inventory full
var baseCarry = prefab.Components.Get<BaseCarryable>( true );
if ( !baseCarry.IsValid() ) return false;
// If we already have this weapon type, add ammo instead
var existing = Weapons.Where( x => x.GameObject.Name == prefab.Name ).FirstOrDefault();
if ( existing.IsValid() )
{
if ( existing is BaseWeapon existingWeapon && existingWeapon.UsesAmmo )
{
if ( existingWeapon.ReserveAmmo < existingWeapon.MaxReserveAmmo )
existingWeapon.AddReserveAmmo( pickupWeapon.ClipContents );
}
return false;
}
// Clone the prefab as a child of the player
var go = prefab.Clone( new CloneConfig { Parent = GameObject, StartEnabled = false } );
go.Components.Get<BaseCarryable>( true ).InventorySlot = slot;
go.NetworkSpawn( Network.Owner );
if ( notice ) OnClientPickup( weapon );
return true;
}Drop
CSHARP
public bool Drop( BaseCarryable weapon )
{
if ( !Networking.IsHost ) return false;
var dropPosition = Player.EyeTransform.Position + Player.EyeTransform.Forward * 48f;
var dropVelocity = Player.EyeTransform.Forward * 200f + Vector3.Up * 100f;
if ( ActiveWeapon == weapon )
SwitchWeapon( null, true );
// Weapons with DroppedWeapon component: spawn a fresh prefab clone
var droppedWeapon = weapon.GetComponent<DroppedWeapon>( true );
if ( droppedWeapon.IsValid() )
{
var pickup = prefab.Clone( new CloneConfig { Transform = new Transform( dropPosition ), StartEnabled = true } );
Ownable.Set( pickup, Player.Network.Owner );
pickup.Tags.Add( "removable" );
pickup.NetworkSpawn();
if ( pickup.GetComponent<Rigidbody>() is { } rb )
{
rb.Velocity = Player.Controller.Velocity + dropVelocity;
rb.AngularVelocity = Vector3.Random * 8.0f;
}
}
weapon.DestroyGameObject();
return true;
}PlayerLoadout — persistence
PlayerLoadout serializes the inventory to JSON and saves it to the scene's save data. On respawn, it restores the loadout.CSHARP
public struct LoadoutEntry
{
public string PrefabPath { get; set; }
public int Slot { get; set; }
public string SpawnerDataPayload { get; set; } // For SpawnerWeapon
}
// Saved as JSON in the scene save file, keyed by SteamId
// Restored in Global.IPlayerEvents.OnSpawnedPresets are stored in LocalData (client-side) as List<SavedPreset> — they persist across sessions.
Key points
- ActiveWeapon is [Sync( SyncFlags.FromHost ), Change] — the Change attribute auto-calls OnActiveWeaponChanged
- Weapons are children of the Player GameObject — GetComponentsInChildren finds them all
- ShouldAutoswitchTo checks GamePreferences.AutoSwitch and whether the active weapon IsInUse()
- Default weapons: physgun, toolgun, camera (slot 8)
- SetToolMode(name) gives the toolgun if missing, equips it, then switches the tool mode
Was this helpful?