Coding Quests
The Scroll Library
Tutorials

How to Build an Inventory System in Godot 4

February 19, 2026Updated June 11, 20266 min read

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.

GDScript
# item.gd
class_name Item
extends 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: ItemType
enum 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:

GDScript
# inventory.gd
class_name Inventory
extends Resource
signal inventory_changed
@export var slots: Array[InventorySlot] = []
@export var max_slots: int = 20
func _init() -> void:
for i in range(max_slots):
slots.append(InventorySlot.new())

And each slot:

GDScript
# inventory_slot.gd
class_name InventorySlot
extends Resource
var item: Item = null
var quantity: int = 0
func is_empty() -> bool:
return item == null
func can_stack(new_item: Item) -> bool:
return item == new_item and quantity < item.max_stack

Notice 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:

GDScript
# 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 fit
func 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 requested

The 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.

GDScript
# inventory_ui.gd
extends Control
@export var inventory: Inventory
@export var slot_scene: PackedScene
@onready var grid: GridContainer = $GridContainer
func _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:

GDScript
# inventory_ui_slot.gd
extends PanelContainer
@onready var icon: TextureRect = $Icon
@onready var quantity_label: Label = $QuantityLabel
func 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:

GDScript
# 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.

godottutorialinventory

Stop reading. Start building.

Beat the demo boss by writing real Godot code, then build this for real in the Inventory System Quest.

Free, and no card needed. Built by a real person, with new quests every month.

Get the next Godot build in your inbox

New quests, project breakdowns, and game-dev tips. Free, no spam, unsubscribe anytime.

Written by Coding Quests

We teach Godot 4 by making you build complete systems: inventories, save systems, action roguelike controllers, enemy AI. The scrolls are free. The quests are where it sticks.