Every RPG, survival game, and adventure game needs an inventory, and it always sounds like a weekend job. A list of items, right? Then you're three days in, wrestling with drag-and-drop, stacking edge cases, and a UI that refuses to update, and you realize the foundation was wrong from the start.
This tutorial walks through the architecture that actually holds up in Godot 4: item data as Resources, a slot-based inventory, signal-driven UI, and the engine's built-in drag-and-drop. Get the data layer right and everything else clicks into place.
The Architecture: Resources, Not Nodes
The biggest mistake beginners make with inventory systems is storing item data in nodes. You end up with item scenes dragging sprites, collision shapes, and scripts around, then you try to stuff those into an array and wonder why everything breaks.
Use Resources instead.
A Resource in Godot is a lightweight data container. It doesn't live in the scene tree. No position, no sprite. Just data, which is exactly what an inventory item is.
# item.gdclass_name Itemextends Resource@export var id: String@export var name: String@export var description: String@export var icon: Texture2D@export var max_stack: int = 1@export var item_type: ItemTypeenum ItemType { WEAPON, ARMOR, CONSUMABLE, MATERIAL, KEY_ITEM }That's your item definition. Create items in the editor by right-clicking in the FileSystem dock, choosing New Resource, and picking Item. Fill in the fields, save it as sword.tres or health_potion.tres, and you've got a reusable item you can tweak without touching code.
The key insight: the Resource is the blueprint, not the instance. Your inventory doesn't hold a "sword node". It holds a reference to the sword Resource plus a quantity.
The Inventory Data Structure
The inventory itself is just an array of slots, each holding an optional item reference and a count:
# inventory.gdclass_name Inventoryextends Resourcesignal inventory_changed@export var slots: Array[InventorySlot] = []@export var max_slots: int = 20func _init() -> void: for i in range(max_slots): slots.append(InventorySlot.new())And each slot:
# inventory_slot.gdclass_name InventorySlotextends Resourcevar item: Item = nullvar quantity: int = 0func is_empty() -> bool: return item == nullfunc can_stack(new_item: Item) -> bool: return item == new_item and quantity < item.max_stackNotice that Inventory extends Resource too, not Node. So you can save it, load it, duplicate it, and pass it around without it being chained to the scene tree.
Adding and Removing Items
The core operations look simple until stacking gets involved:
# inventory.gd (continued)func add_item(new_item: Item, amount: int = 1) -> int: var remaining := amount # First pass: try to stack with existing slots for slot in slots: if remaining <= 0: break if slot.can_stack(new_item): var space := new_item.max_stack - slot.quantity var to_add := mini(remaining, space) slot.quantity += to_add remaining -= to_add # Second pass: fill empty slots for slot in slots: if remaining <= 0: break if slot.is_empty(): slot.item = new_item var to_add := mini(remaining, new_item.max_stack) slot.quantity = to_add remaining -= to_add inventory_changed.emit() return remaining # Returns leftover that didn't fitfunc remove_item(target_item: Item, amount: int = 1) -> bool: var remaining := amount for slot in slots: if remaining <= 0: break if slot.item == target_item: var to_remove := mini(remaining, slot.quantity) slot.quantity -= to_remove remaining -= to_remove if slot.quantity <= 0: slot.item = null slot.quantity = 0 inventory_changed.emit() return remaining <= 0 # True if we removed everything requestedThe two-pass approach in add_item matters. Stack into existing slots first, then fill empty ones. Skip that and your inventory fragments into five separate stacks of 1 potion instead of one stack of 5, and players will absolutely notice.
Building the Inventory UI
The UI mirrors the data: a grid of slot panels, each showing an icon and a quantity.
# inventory_ui.gdextends Control@export var inventory: Inventory@export var slot_scene: PackedScene@onready var grid: GridContainer = $GridContainerfunc _ready() -> void: inventory.inventory_changed.connect(_refresh) _refresh()func _refresh() -> void: # Clear existing UI slots for child in grid.get_children(): child.queue_free() # Rebuild from data for slot in inventory.slots: var ui_slot: InventoryUISlot = slot_scene.instantiate() ui_slot.set_slot_data(slot) grid.add_child(ui_slot)Each UI slot is a small scene:
# inventory_ui_slot.gdextends PanelContainer@onready var icon: TextureRect = $Icon@onready var quantity_label: Label = $QuantityLabelfunc set_slot_data(slot: InventorySlot) -> void: if slot.is_empty(): icon.texture = null quantity_label.text = "" else: icon.texture = slot.item.icon quantity_label.text = str(slot.quantity) if slot.quantity > 1 else ""The signal connection is the glue. Inventory data changes, UI rebuilds. The two sides stay fully decoupled, so you can rework how items are stored without touching the UI, or redesign the UI without touching inventory logic. If signals still feel fuzzy, my Godot 4 signals guide covers the whole system, including this exact pattern.
Drag and Drop
Godot ships a drag-and-drop system on Control nodes. You override three methods, and that's the whole feature:
# inventory_ui_slot.gd (extended)func _get_drag_data(_pos: Vector2) -> Variant: if slot_data.is_empty(): return null # Create a preview that follows the mouse var preview := TextureRect.new() preview.texture = slot_data.item.icon preview.size = Vector2(64, 64) set_drag_preview(preview) return {"slot": slot_data, "source": self}func _can_drop_data(_pos: Vector2, data: Variant) -> bool: return data is Dictionary and data.has("slot")func _drop_data(_pos: Vector2, data: Variant) -> void: var source_slot: InventorySlot = data.slot # Swap the two slots var temp_item := slot_data.item var temp_qty := slot_data.quantity slot_data.item = source_slot.item slot_data.quantity = source_slot.quantity source_slot.item = temp_item source_slot.quantity = temp_qty inventory.inventory_changed.emit()Three methods, zero plugins. Godot handles the mouse tracking, the preview rendering, and the drop detection. You just define what data travels and what happens when it lands.
Common Inventory Mistakes
Storing items as nodes in an array is the big one, and I already made my case above. Nodes belong in the scene tree. Inventory items are data. Resources.
Rebuilding the entire UI on every change sounds wasteful, and for huge inventories it is. But for anything under 30 slots it's completely fine, so don't optimize before you've shipped the simple version. Update individual slots later if profiling actually tells you to.
If your UI isn't updating, check that inventory_changed.emit() runs after every mutation. A forgotten emit is the single most common inventory bug, and it always looks like a UI problem when it's really a data problem.
One subtle one: comparing items by value instead of reference. Two different Item resources with the same id are not the same object. If you need to compare by ID, use slot.item.id == target.id instead of slot.item == target.
Going Further
This is the data layer and basic UI, which is maybe half of a finished inventory. A complete system adds right-click context menus (Use, Equip, Drop), hover tooltips with stats and rarity, equipment slots with stat bonuses, category tabs for filtering, and serialization so it survives a save and load.
We built all of it in the free Inventory System quest: 10 hands-on lessons that go from Resources to a fully working drag-and-drop inventory with stacking, UI, and integration testing. It's free, and you don't need an account to start.
Get the Resource architecture right first, though. Everything on that list bolts onto it. Get it wrong and you'll be rewriting the whole system the week you add saving.
FAQ
Should inventory items be Resources or Nodes in Godot?
Resources. An item sitting in an inventory is pure data (name, icon, stack size), and Resources are Godot's data containers: no scene tree, no position, cheap to load and duplicate. A node only makes sense for the item's physical pickup in the world, and that node should just hold a reference to the item Resource.
How do I save an inventory in Godot 4?
Serialize each slot to plain data (item id and quantity), write it to JSON in user://, and on load look the ids back up to find the matching Resources. Don't try to dump the Resource objects themselves into your save file. I walk through the full pattern in my Godot 4 save system tutorial.
Do I need a plugin for drag and drop?
No. Godot's Control nodes have drag and drop built in through _get_drag_data, _can_drop_data, and _drop_data. The engine handles mouse tracking and drop detection, so an inventory only needs the three overrides shown above.


