Inventory Systems in Godot 4.3+
All examples target Godot 4.3+ with no deprecated APIs. This skill carries the decisions; the implementations live in references/ (GDScript first, then C#) — load the one you need.
Related skills: resource-pattern for custom Resource data containers, save-load for inventory serialization, event-bus for inventory change notifications, hud-system for inventory UI display, popochiu for adventure-game inventory.
1. Architecture Overview
┌─────────────────────────────────────────────────────────┐ │ UI Layer │ │ InventoryUI (Control) │ │ └─ GridContainer │ │ └─ SlotUI × N (Button) │ │ └─ TextureRect (icon) + Label (qty) │ │ │ │ Connects to: inventory_changed signal │ │ Drag-and-drop via _get_drag_data / _drop_data │ └───────────────────────┬─────────────────────────────────┘ │ reads / mutates ┌───────────────────────▼─────────────────────────────────┐ │ Inventory (Node) │ │ slots: Array[InventorySlot] │ │ add_item(item, qty) → leftover: int │ │ remove_item(item, qty) │ │ has_item(item, qty) → bool │ │ get_item_count(item) → int │ │ │ │ signals: inventory_changed │ │ item_added(item, quantity) │ │ item_removed(item, quantity) │ └───────────────────────┬─────────────────────────────────┘ │ references ┌───────────────────────▼─────────────────────────────────┐ │ Data Layer (Resources) │ │ ItemData (Resource) │ │ id, name, description, icon, max_stack_size, │ │ item_type enum │ │ │ │ InventorySlot (inner class / Resource) │ │ item: ItemData, quantity: int │ └─────────────────────────────────────────────────────────┘
2. The Decisions That Matter
Three choices shape every inventory. Make them explicitly and state which you picked — the rest is mechanical.
| Decision | Take | Why |
|---|---|---|
Item identity: Resource vs. Dictionary / JSON | Resource in .tres | @export slots accept typed items, the Inspector edits them, and shared instances compare by reference |
Slot container: Resource vs. RefCounted | RefCounted | Slots are runtime state, not authored assets. A Resource invites saving slots to disk and reloading stale item copies |
add_item return: bool vs. leftover count | leftover int | Partial adds are the common case (stack caps, full bag). A bool forces the caller to re-query what actually fit |
If your game needs something else — a weight-based bag with no slots, a grid inventory with shaped items, an infinite stack list — say so before building. The fixed slot array this skill describes is the wrong skeleton for all three.
Non-obvious specifics
- Fill existing stacks before empty slots — two passes, in that order. A single naive pass scatters one item type across several slots and reports the bag full while space remains.
- Emit
inventory_changedonce per operation, after both passes complete — not once per slot touched. Per-slot emission re-renders the whole UI N times for oneadd_item. - Items compare by reference. Godot caches loaded resources, so
load()on the same.tresreturns the same instance andslot.item == itemis correct and cheap. If youduplicate()items at runtime, that breaks — key offItemData.idinstead. - Clear
itemtonullwhen quantity reaches 0, oris_empty()lies and the slot is never reusable. - Prevent stacking with data, not branches — set
max_stack_size = 1onEQUIPMENTandKEY_ITEMitems rather than special-casing them inadd_item. - C#: mark
ItemData[GlobalClass]or the Inspector will not offer it as a type when creating.tresfiles.
Inventory API contract
| Member | Returns | Contract |
|---|---|---|
add_item(item, quantity) | int | Leftover that did not fit; 0 means everything was added |
remove_item(item, quantity) | — | Removes up to quantity; silent when fewer are held |
has_item(item, quantity) | bool | Convenience over get_item_count |
get_item_count(item) | int | Summed across every slot |
inventory_changed | signal | Fired once per mutating operation — drives all UI |
item_added(item, quantity) | signal | Carries the amount actually added |
item_removed(item, quantity) | signal | Carries the amount actually removed |
Full
ItemData,Inventory, andInventorySlotimplementations in GDScript and C#: references/core-classes.md
3. Equipment Extension
Add equipment slots (HEAD, CHEST, WEAPON, etc.) by extending the Inventory class with a typed slot map. Stat aggregation runs by summing ItemData.stats across equipped items; signal equipment_changed when slots change.
See references/equipment.md for the full GDScript and C#
Equipmentclass withEquipmentSlotTypeenum, equip / unequip API, and stat aggregation.
4. UI Binding
Slot-grid UI: a GridContainer of Panel slot widgets, each rendering one InventorySlot. Drag-and-drop uses _get_drag_data / _drop_data / _can_drop_data on the slot widget. The Inventory emits inventory_changed; the UI re-renders affected slots.
See references/ui-binding.md for the full GDScript and C# slot widget (drag/drop, hover preview), inventory grid layout, and tooltip wiring.
5. Serialization
Persist Inventory + Equipment as a Dictionary keyed by item resource path (since ItemData lives at res://items/<name>.tres). Reload by load(path) and reconstructing the slot list. Version field gates migration on load.
See references/serialization.md for the GDScript and C# save/load implementation with
versionfield and ConfigFile / JSON variants.
Implementation Checklist
-
ItemDataextendsResourcewith a stableidstring set in the Inspector -
ItemDatafiles live underres://items/and are committed to version control -
Inventory.add_item()returns leftover count; callers handle a full inventory -
inventory_changedsignal drives all UI updates — UI never polls per-frame -
InventorySlot.remove_from_stack()clearsitemtonullwhen quantity reaches 0 - Equipment slots keyed by
SlotTypeenum, not by string, to catch typos at compile time -
Equipment.get_total_stat()is called when stats are needed, not cached unless profiling demands it - Serialization stores
id + quantityonly — never fullItemDataobjects or resource paths -
ItemRegistryloads items at startup; all deserialization goes through it - Drag-and-drop swaps slot contents directly then emits
inventory_changedonce -
max_stack_size = 1onEQUIPMENTandKEY_ITEMtypes to prevent stacking - All
push_error()messages include the class name and method for easy tracing


