Containers & Inventories
Learn how to build inventory systems using Stack Inventory’s flexible container architecture.
Overview
Section titled “Overview”Stack Inventory provides the core item system (IItem, Item, ItemDefinition) but leaves container implementation up to you. This gives you complete flexibility to build inventories that match your game’s needs.
Basic Container Concept
Section titled “Basic Container Concept”A container stores items and their amounts:
public partial class BasicInventory : Node{ private List<(IItem item, int amount)> _slots = new();
public void AddItem(IItem item, int amount) { // Your logic here }
public bool RemoveItem(IItem item, int amount) { // Your logic here return true; }}Simple List-Based Inventory
Section titled “Simple List-Based Inventory”The simplest inventory is just a list:
using Godot;using StackInventory.Core.Items;using System.Collections.Generic;using System.Linq;
public partial class ListInventory : Node{ private readonly List<(IItem item, int amount)> _items = new();
[Export] public int MaxSlots { get; set; } = 20;
public bool AddItem(IItem item, int amount) { // Check if we already have this item var existing = _items.FirstOrDefault(i => i.item.Id == item.Id);
if (existing.item != null) { // Stack with existing int newAmount = existing.amount + amount; if (newAmount > item.StackMaximum) { // Can't stack that much return false; }
int index = _items.IndexOf(existing); _items[index] = (existing.item, newAmount); return true; } else { // Add new slot if (_items.Count >= MaxSlots) { return false; // Inventory full }
_items.Add((item, amount)); return true; } }
public bool RemoveItem(IItem item, int amount) { var existing = _items.FirstOrDefault(i => i.item.Id == item.Id);
if (existing.item == null || existing.amount < amount) { return false; // Don't have enough }
int newAmount = existing.amount - amount; int index = _items.IndexOf(existing);
if (newAmount <= 0) { _items.RemoveAt(index); } else { _items[index] = (existing.item, newAmount); }
return true; }
public int GetAmount(IItem item) { var existing = _items.FirstOrDefault(i => i.item.Id == item.Id); return existing.item != null ? existing.amount : 0; }
public bool HasItem(IItem item, int amount = 1) { return GetAmount(item) >= amount; }
public IEnumerable<(IItem item, int amount)> GetAllItems() { return _items.ToList(); }}Slot-Based Inventory
Section titled “Slot-Based Inventory”For fixed-size inventories with specific slots:
public partial class SlotInventory : Node{ public class InventorySlot { public IItem Item { get; set; } public int Amount { get; set; } public bool IsEmpty => Item == null || Amount <= 0; }
private InventorySlot[] _slots;
[Export] public int SlotCount { get; set; } = 20;
public override void _Ready() { _slots = new InventorySlot[SlotCount]; for (int i = 0; i < SlotCount; i++) { _slots[i] = new InventorySlot(); } }
public bool AddItemToSlot(int slotIndex, IItem item, int amount) { if (slotIndex < 0 || slotIndex >= _slots.Length) return false;
var slot = _slots[slotIndex];
if (slot.IsEmpty) { // Empty slot - add item slot.Item = item; slot.Amount = amount; return true; } else if (slot.Item.Id == item.Id) { // Same item - try to stack int newAmount = slot.Amount + amount; if (newAmount <= item.StackMaximum) { slot.Amount = newAmount; return true; } }
return false; // Can't add }
public bool RemoveFromSlot(int slotIndex, int amount) { if (slotIndex < 0 || slotIndex >= _slots.Length) return false;
var slot = _slots[slotIndex];
if (slot.IsEmpty || slot.Amount < amount) return false;
slot.Amount -= amount; if (slot.Amount <= 0) { slot.Item = null; slot.Amount = 0; }
return true; }
public InventorySlot GetSlot(int index) { return (index >= 0 && index < _slots.Length) ? _slots[index] : null; }}Specialized Containers
Section titled “Specialized Containers”Equipment Container
Section titled “Equipment Container”public partial class Equipment : Node{ public enum EquipSlot { Head, Chest, Legs, Feet, MainHand, OffHand, Ring1, Ring2 }
private Dictionary<EquipSlot, IItem> _equippedItems = new();
public bool Equip(EquipSlot slot, IItem item) { // Check if item can be equipped in this slot if (!CanEquipInSlot(item, slot)) return false;
// Unequip current item if any if (_equippedItems.ContainsKey(slot)) { Unequip(slot); }
_equippedItems[slot] = item; EmitSignal(SignalName.ItemEquipped, (int)slot, item); return true; }
public IItem Unequip(EquipSlot slot) { if (!_equippedItems.TryGetValue(slot, out var item)) return null;
_equippedItems.Remove(slot); EmitSignal(SignalName.ItemUnequipped, (int)slot, item); return item; }
private bool CanEquipInSlot(IItem item, EquipSlot slot) { // Check tags based on slot return slot switch { EquipSlot.Head => item.HasTag("Armor.Head"), EquipSlot.Chest => item.HasTag("Armor.Chest"), EquipSlot.MainHand => item.HasTag("Weapon"), _ => false }; }
[Signal] public delegate void ItemEquippedEventHandler(int slot, IItem item);
[Signal] public delegate void ItemUnequippedEventHandler(int slot, IItem item);}Hotbar
Section titled “Hotbar”public partial class Hotbar : Node{ private IItem[] _hotbarSlots;
[Export] public int HotbarSize { get; set; } = 10; [Export] public int SelectedSlot { get; set; } = 0;
public override void _Ready() { _hotbarSlots = new IItem[HotbarSize]; }
public void AssignToSlot(int slot, IItem item) { if (slot >= 0 && slot < HotbarSize) { _hotbarSlots[slot] = item; EmitSignal(SignalName.HotbarChanged, slot); } }
public IItem GetSelectedItem() { return _hotbarSlots[SelectedSlot]; }
public void SelectSlot(int slot) { if (slot >= 0 && slot < HotbarSize) { SelectedSlot = slot; EmitSignal(SignalName.SlotSelected, slot); } }
[Signal] public delegate void HotbarChangedEventHandler(int slot);
[Signal] public delegate void SlotSelectedEventHandler(int slot);}Container Patterns
Section titled “Container Patterns”Filtered Container
Section titled “Filtered Container”// Container that only accepts certain itemspublic partial class FilteredContainer : Node{ private List<(IItem item, int amount)> _items = new();
[Export] public string RequiredTag { get; set; } = "";
public bool AddItem(IItem item, int amount) { if (!string.IsNullOrEmpty(RequiredTag) && !item.HasTag(RequiredTag)) { return false; // Item doesn't match filter }
// Add to container... return true; }}Weight-Limited Container
Section titled “Weight-Limited Container”public partial class WeightLimitedContainer : Node{ private List<(IItem item, int amount)> _items = new();
[Export] public float MaxWeight { get; set; } = 100f;
public float CurrentWeight { get => _items.Sum(i => i.item.Value * i.amount); }
public bool AddItem(IItem item, int amount) { float itemWeight = item.Value * amount;
if (CurrentWeight + itemWeight > MaxWeight) { return false; // Too heavy }
// Add to container... return true; }}Connecting to Pickups
Section titled “Connecting to Pickups”Integrate containers with the pickup system:
public partial class Player : CharacterBody2D{ private ListInventory _inventory;
public override void _Ready() { _inventory = GetNode<ListInventory>("Inventory"); }
private void OnPickupAreaEntered(Area2D area) { if (area is Pickup2D pickup) { IItem item = pickup.Item; int amount = pickup.Amount;
if (_inventory.AddItem(item, amount)) { GD.Print($"Picked up {amount}x {item.Name}"); pickup.QueueFree(); } else { GD.Print("Inventory full!"); } } }}Save/Load Containers
Section titled “Save/Load Containers”Serialize containers using Item POCS:
using System.Text.Json;
public class InventorySaveData{ public List<ItemData> Items { get; set; } = new();
public class ItemData { public Item Item { get; set; } public int Amount { get; set; } }}
public partial class ListInventory : Node{ public string SaveToJson() { var saveData = new InventorySaveData();
foreach (var (item, amount) in _items) { // Convert IItem to Item POCS if needed var itemPocs = item is Item i ? i : Item.FromIItem(item);
saveData.Items.Add(new InventorySaveData.ItemData { Item = itemPocs, Amount = amount }); }
return JsonSerializer.Serialize(saveData); }
public void LoadFromJson(string json) { var saveData = JsonSerializer.Deserialize<InventorySaveData>(json);
_items.Clear(); foreach (var itemData in saveData.Items) { _items.Add((itemData.Item, itemData.Amount)); } }}Best Practices
Section titled “Best Practices”- Use IItem everywhere - Keep containers generic
- Emit signals - Notify UI when container changes
- Validate additions - Check weight, slots, filters
- Handle overflow - What happens when container is full?
- Test edge cases - Empty containers, max stacks, etc.
Next Steps
Section titled “Next Steps”- UI Guide - Build inventory UI
- Tag System - Filter items by tags
- Architecture - Understand the design
- API Reference - Explore IItem interface