Godot 4 Inventory System — Build a Flexible Item System
with GDScript
Almost every game needs an inventory. RPGs, survival crafters, puzzle adventures — even platformers with power-ups need a way to store, display, and persist items. Yet most Godot tutorials stop at a simple array and a print statement.
In this Godot 4 inventory system tutorial, you'll build a production-ready item system from scratch. We'll use resource-based items for data-driven design, wrap them in an inventory container class, render a UI grid, add drag-and-drop basics, and save/load everything with JSON. Every example uses complete GDScript code you can paste directly into your project.
If you're new to GDScript patterns, grab our free GDScript Cheat Sheet first. Already comfortable? Let's build.
1 — Resource-Based Items
Godot's Resource class is the backbone of our gdscript item system. Resources are lightweight, serializable data containers that the editor can inspect. Instead of hard-coding item stats in scripts, we define a custom resource that any designer can tweak in the Inspector.
Create a new script and extend Resource. Add exported properties for everything an item needs — name, icon, description, stack size, and any custom stats:
class_name ItemData
extends Resource
@export var id: String = ""
@export var display_name: String = ""
@export var description: String = ""
@export var icon: Texture2D
@export var max_stack: int = 1
@export var item_type: ItemType = ItemType.MISC
enum ItemType { WEAPON, ARMOR, CONSUMABLE, MATERIAL, MISC }
# Optional stats — extend for your game
@export var damage: int = 0
@export var heal_amount: int = 0
@export var sell_value: int = 0With class_name set, the editor auto-registers ItemData as a new resource type. Right-click the FileSystem dock, choose New Resource, and search for ItemData. Save each item as a .tres file — e.g. iron_sword.tres, health_potion.tres.
This data-driven approach means you can add hundreds of items without touching code. It also integrates cleanly with signal bus patterns for broadcasting events like “item picked up” or “item used” across your scene tree.
2 — The Inventory Container Class
Now we need a place to hold items. Our inventory container manages a fixed array of slots, each containing an ItemData reference and a stack count. Signals notify the UI whenever the contents change.
class_name Inventory
extends Node
signal inventory_changed
signal item_added(item: ItemData, slot_index: int)
signal item_removed(item: ItemData, slot_index: int)
@export var max_slots: int = 20
var slots: Array[Dictionary] = []
func _ready() -> void:
# Initialize empty slots
for i in max_slots:
slots.append({ "item": null, "quantity": 0 })
func add_item(item: ItemData, amount: int = 1) -> int:
# First, try stacking into existing slots
for i in slots.size():
var slot = slots[i]
if slot["item"] == item and slot["quantity"] < item.max_stack:
var can_add = mini(amount, item.max_stack - slot["quantity"])
slot["quantity"] += can_add
amount -= can_add
item_added.emit(item, i)
if amount <= 0:
break
# Then, fill empty slots with remaining amount
if amount > 0:
for i in slots.size():
if slots[i]["item"] == null:
var can_add = mini(amount, item.max_stack)
slots[i] = { "item": item, "quantity": can_add }
amount -= can_add
item_added.emit(item, i)
if amount <= 0:
break
inventory_changed.emit()
return amount # Returns leftover that didn't fit
func remove_item(slot_index: int, amount: int = 1) -> void:
if slot_index < 0 or slot_index >= slots.size():
return
var slot = slots[slot_index]
if slot["item"] == null:
return
var removed_item = slot["item"]
slot["quantity"] -= amount
if slot["quantity"] <= 0:
slots[slot_index] = { "item": null, "quantity": 0 }
item_removed.emit(removed_item, slot_index)
inventory_changed.emit()
func get_slot(index: int) -> Dictionary:
if index >= 0 and index < slots.size():
return slots[index]
return { "item": null, "quantity": 0 }
func swap_slots(from: int, to: int) -> void:
if from < 0 or from >= slots.size():
return
if to < 0 or to >= slots.size():
return
var temp = slots[from]
slots[from] = slots[to]
slots[to] = temp
inventory_changed.emit()The key design decisions here: add_item() returns the leftover amount that didn't fit (so you can drop overflow on the ground), and swap_slots() powers rearranging via drag-and-drop. If you want to manage complex game states alongside inventory, consider pairing this with a state machine to handle states like “shop open”, “crafting”, or “looting”.
Want clean architecture in Godot? Our State Machines in GDScript course covers FSMs, hierarchical states, and game flow — all in 15 minutes.
Want more Godot tricks?
Get our free GDScript Cheat Sheet with 20+ copy-paste snippets for signals, exports, state machines & more.
3 — UI Grid Display
Players need to see their items. We'll build a grid UI using Godot's GridContainer filled with slot panels. Each slot shows an icon and quantity label. First, create the individual slot scene:
class_name InventorySlot
extends PanelContainer
@onready var icon: TextureRect = $Icon
@onready var quantity_label: Label = $QuantityLabel
var slot_index: int = -1
func update_slot(data: Dictionary) -> void:
var item: ItemData = data.get("item")
var qty: int = data.get("quantity", 0)
if item == null:
icon.texture = null
icon.modulate = Color.TRANSPARENT
quantity_label.text = ""
else:
icon.texture = item.icon
icon.modulate = Color.WHITE
quantity_label.text = str(qty) if qty > 1 else ""
func clear_slot() -> void:
update_slot({ "item": null, "quantity": 0 })In the scene tree, the InventorySlot is a PanelContainer with a TextureRect child named “Icon” and a Label named “QuantityLabel” anchored to the bottom-right. Now create the grid controller:
extends Control
@export var inventory: Inventory
@export var slot_scene: PackedScene
@onready var grid: GridContainer = $GridContainer
func _ready() -> void:
if inventory:
inventory.inventory_changed.connect(_refresh_grid)
_build_grid()
func _build_grid() -> void:
# Clear existing children
for child in grid.get_children():
child.queue_free()
# Create slot panels
for i in inventory.max_slots:
var slot_node: InventorySlot = slot_scene.instantiate()
slot_node.slot_index = i
grid.add_child(slot_node)
_refresh_grid()
func _refresh_grid() -> void:
for i in grid.get_child_count():
var slot_node: InventorySlot = grid.get_child(i) as InventorySlot
if slot_node:
slot_node.update_slot(inventory.get_slot(i))Set the GridContainer's columns property to 5 (or whatever your grid width is) in the Inspector. Each time the inventory emits inventory_changed, every slot refreshes its icon and quantity label. This reactive pattern mirrors how signal buses decouple your nodes from each other.
4 — Drag-and-Drop Basics
Godot has built-in drag-and-drop support via _get_drag_data(), _can_drop_data(), and _drop_data(). Override these on each slot to let players rearrange items:
# Add these methods to the InventorySlot class
func _get_drag_data(_at_position: Vector2) -> Variant:
if icon.texture == null:
return null
# Create a visual preview
var preview = TextureRect.new()
preview.texture = icon.texture
preview.custom_minimum_size = Vector2(48, 48)
preview.modulate = Color(1, 1, 1, 0.7)
set_drag_preview(preview)
return { "from_slot": slot_index }
func _can_drop_data(_at_position: Vector2, data: Variant) -> bool:
return data is Dictionary and data.has("from_slot")
func _drop_data(_at_position: Vector2, data: Variant) -> void:
if data is Dictionary and data.has("from_slot"):
var from: int = data["from_slot"]
var inventory_node = get_parent().get_parent()
if inventory_node and inventory_node.has_method("swap_from_ui"):
inventory_node.swap_from_ui(from, slot_index)Then add a helper method on the UI controller to bridge the swap back to the data layer:
func swap_from_ui(from_index: int, to_index: int) -> void:
inventory.swap_slots(from_index, to_index)That's it. The call to swap_slots() fires inventory_changed, which triggers _refresh_grid(), and the UI updates automatically. Data flows in one direction: UI action → data mutation → signal → UI refresh. Clean and debuggable.
Love clean architecture? The Godot Pro Pack bundles signals, state machines, shaders, and 3 more courses at 50%+ off.
5 — Saving & Loading with JSON
Resources are great at design time, but for save files JSON is simpler and safer (no arbitrary code execution). We serialize each slot to a dictionary with the item's id and its quantity, then reconstruct on load by looking up resources from a registry.
class_name InventorySaver
extends Node
const SAVE_PATH := "user://inventory.json"
## Save inventory to JSON file
static func save_inventory(inventory: Inventory) -> void:
var save_data: Array[Dictionary] = []
for slot in inventory.slots:
if slot["item"] != null:
save_data.append({
"id": slot["item"].id,
"quantity": slot["quantity"],
})
else:
save_data.append({ "id": "", "quantity": 0 })
var json_string = JSON.stringify(save_data, " ")
var file = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
if file:
file.store_string(json_string)
file.close()
print("Inventory saved to ", SAVE_PATH)
## Load inventory from JSON file
static func load_inventory(inventory: Inventory) -> void:
if not FileAccess.file_exists(SAVE_PATH):
print("No save file found.")
return
var file = FileAccess.open(SAVE_PATH, FileAccess.READ)
if not file:
return
var json_string = file.get_as_text()
file.close()
var json = JSON.new()
if json.parse(json_string) != OK:
push_error("Failed to parse inventory JSON.")
return
var data: Array = json.data
for i in mini(data.size(), inventory.slots.size()):
var entry: Dictionary = data[i]
if entry["id"] == "":
inventory.slots[i] = { "item": null, "quantity": 0 }
else:
var item = _load_item_by_id(entry["id"])
if item:
inventory.slots[i] = {
"item": item,
"quantity": entry["quantity"],
}
inventory.inventory_changed.emit()
## Look up an ItemData resource by its id
static func _load_item_by_id(id: String) -> ItemData:
var path = "res://items/%s.tres" % id
if ResourceLoader.exists(path):
return load(path) as ItemData
push_warning("Item not found: ", id)
return nullThe convention here is that each .tres item resource lives at res://items/<id>.tres. As long as your ItemData.id matches the filename, the loader finds it automatically. Call InventorySaver.save_inventory(inv) on quit and InventorySaver.load_inventory(inv) on startup.
For more complex projects you might save additional game state alongside the inventory. If your game uses randomized content, check out our procedural generation guide to see how seed-based systems keep worlds reproducible across save/load cycles.
6 — Putting It All Together
Here's how the pieces connect in a typical scene tree:
Main (Node)
├── Player (CharacterBody2D)
│ └── Inventory (inventory.gd) ← data layer
├── CanvasLayer
│ └── InventoryUI (inventory_ui.gd) ← display layer
│ └── GridContainer
│ ├── InventorySlot 0
│ ├── InventorySlot 1
│ └── ... (instantiated at runtime)
└── InventorySaver (inventory_saver.gd)Wire the InventoryUI's exported inventory reference to the player's Inventory node in the Inspector. To add items during gameplay, preload your resources and call add_item():
extends Area2D
@export var item: ItemData
@export var quantity: int = 1
func _on_body_entered(body: Node2D) -> void:
if body.has_node("Inventory"):
var inv: Inventory = body.get_node("Inventory")
var leftover = inv.add_item(item, quantity)
if leftover == 0:
queue_free() # Fully picked up
else:
quantity = leftover # Partial pickupThe pickup.gd script is attached to an Area2D node representing an item in the world. Set the item export to your .tres resource in the Inspector and you're done. The leftover handling means the player can't magically absorb more items than their inventory can hold.
7 — Tips & Next Steps
You now have a fully functional Godot 4 inventory system. Here are ideas to extend it:
- Tooltip on hover — Show
ItemData.descriptionand stats in a floating panel. - Item categories & tabs — Filter the grid by
ItemTypewith tab buttons. - Equipment slots — Add a separate “equipped” dictionary alongside the main slots.
- Crafting system — Define recipes as another Resource type and consume items from the inventory.
- Multiplayer sync — Pair with RPCs to synchronize inventories between clients.
- State-driven UI — Use a state machine to transition between inventory, shop, and crafting screens without spaghetti conditionals.
Level up your GDScript with our free cheat sheet — 20+ copy-paste snippets for signals, tweens, exports, and common patterns.
You've built a flexible inventory system in Godot 4 that covers every essential layer: data (resources), logic (container class), presentation (grid UI), interaction (drag-and-drop), and persistence (JSON save/load). Each piece is decoupled, so you can swap, extend, or restyle without breaking the rest.
The godot inventory tutorial approach we used keeps data separate from display — a pattern that scales from a 10-slot backpack to a 500-slot warehouse. Fork the code, adapt it to your game, and ship something great.
Get the Free GDScript Cheat Sheet + Weekly Godot Tips
Join indie devs getting our GDScript Cheat Sheet with 20+ copy-paste snippets for signals, exports, state machines, and more — plus weekly Godot tips and architecture tricks straight to your inbox.
Take your Godot skills further with our 15-minute micro-courses. Each one is focused, practical, and built for indie devs who ship.
Mastering GDScript Signals
Signal buses, typed signals, async patterns — the definitive signals course for Godot 4.
$4.99 · 15 min →State Machines in GDScript
Clean game logic with reusable state machines. Perfect for AI, UI, and game flow.
$4.99 · 15 min →Godot Pro Pack Bundle
All courses at a discount. Signals, shaders, state machines, multiplayer, and more.
$12.99 · Save 50%+ →Godot 4 State Machine Tutorial
Build a clean, reusable state machine in GDScript. Perfect for player controllers, AI, and game flow.
How to Use Signal Bus Pattern in Godot 4
Decouple your nodes with a global event bus. Step-by-step tutorial with complete GDScript code.
Godot 4 Procedural Generation Tutorial
Master noise-based terrain, random room placement, and seed-based generation for infinite, replayable worlds.
Free GDScript Cheat Sheet
20+ copy-paste GDScript snippets for signals, tweens, exports, and common patterns.