
Godot Inventory System
- 217 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Use godot-inventory-system for development tasks
About
godot-inventory-system: A skill for development. This provides functionality for development workflows.
- godot-inventory-system
Godot Inventory System by the numbers
- 217 all-time installs (skills.sh)
- +21 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #1,849 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-inventory-systemAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 217 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-inventory-system for development tasks
Files
Inventory System
Slot management, stacking logic, and resource-based items define robust inventory systems.
Available Scripts
inventory_item_resource.gd
Base Resource for all inventory items, allowing for serialized .tres item databases.
item_slot_data.gd
Reactive data structure for a single inventory slot, broadcasting changes to the UI.
inventory_data_resource.gd
Centralized Resource for managing inventory arrays, stacking logic, and empty slot finding.
inventory_ui_controller.gd
Grid-based UI controller that maps InventoryData to visual slots using the "Reactive UI" pattern.
drag_and_drop_slot.gd
Native Godot drag-and-drop implementation for moving and swapping inventory items.
item_database_loader.gd
Global registry pattern to efficiently load and lookup items by unique ID strings.
inventory_persistence.gd
Expert logic for serializing and deserializing complex inventory structures to disk.
consumable_item_logic.gd
Extension pattern for implementing specific item behaviors (Potions, Food) via inheritance.
loot_table_resource.gd
Data-driven loot distribution definition for random drops and chest contents.
item_pickup_node.gd
World-space bridge for converting physical 2D/3D pickups into inventory data.
NEVER Do in Inventory Systems
- NEVER use Nodes for items —
Item extends Nodeleads to massive SceneTree bloat and memory leaks. Always useItem extends Resourcefor lightweight data [20]. - NEVER attempt to add items without checking stack limits — Adding to an inventory without pre-scanning for existing stacks causes item duplication or loss [21].
- NEVER allow the UI to modify the Inventory Data directly — If UI code clears a slot without notifying the data model, you'll get desyncs and ghost items [22].
- NEVER use `float` for item quantities — Floating point errors (e.g. 0.9999 instead of 1) will break your "equal to zero" checks. Stick to
intfor counts [23]. - NEVER add items before validating weight or volume capacity — Moving validation check after adding the item makes it impossible to prevent over-encumbrance [24].
- NEVER emit signals for every single item inside a batch operation — Adding 50 items = 50 UI updates. Emit a single
inventory_updatedsignal after the loop completes [25]. - NEVER hardcode item references in scripts — Use a String ID and a central
ItemDatabaseto look up resources. This is CRITICAL for save system compatibility. - NEVER ignore `is_instance_valid()` when accessing item icons — If a slot's item is null, trying to access
.iconwill crash the UI. - NEVER use complex Array logic in the UI — The UI should only "reflect" the data. All sorting, stacking, and filtering logic belongs in the
InventoryDataresource. - NEVER create new `Resource` instances inside a `_process()` loop — Pre-instantiate your inventory slots or reuse existing ones to prevent allocation spikes.
---
Core Architecture
# item.gd (Resource)
class_name Item
extends Resource
@export var id: String
@export var display_name: String
@export var icon: Texture2D
@export var max_stack: int = 1
@export var weight: float = 0.0
@export_multiline var description: StringInventory Manager
# inventory.gd
class_name Inventory
extends Resource
signal item_added(item: Item, amount: int)
signal item_removed(item: Item, amount: int)
signal inventory_changed
@export var slots: Array[InventorySlot] = []
@export var max_slots: int = 20
@export var max_weight: float = 100.0
func _init() -> void:
slots.resize(max_slots)
for i in max_slots:
slots[i] = InventorySlot.new()
func add_item(item: Item, amount: int = 1) -> bool:
var remaining := amount
# Try stacking first
if item.max_stack > 1:
for slot in slots:
if slot.item == item and slot.amount < item.max_stack:
var space := item.max_stack - slot.amount
var to_add := mini(space, remaining)
slot.amount += to_add
remaining -= to_add
if remaining <= 0:
item_added.emit(item, amount)
inventory_changed.emit()
return true
# Add to empty slots
while remaining > 0:
var empty_slot := find_empty_slot()
if empty_slot == null:
return false # Inventory full
var to_add := mini(item.max_stack, remaining)
empty_slot.item = item
empty_slot.amount = to_add
remaining -= to_add
item_added.emit(item, amount)
inventory_changed.emit()
return true
func remove_item(item: Item, amount: int = 1) -> bool:
var remaining := amount
for slot in slots:
if slot.item == item:
var to_remove := mini(slot.amount, remaining)
slot.amount -= to_remove
remaining -= to_remove
if slot.amount <= 0:
slot.clear()
if remaining <= 0:
item_removed.emit(item, amount)
inventory_changed.emit()
return true
return false # Not enough items
func has_item(item: Item, amount: int = 1) -> bool:
var count := 0
for slot in slots:
if slot.item == item:
count += slot.amount
return count >= amount
func find_empty_slot() -> InventorySlot:
for slot in slots:
if slot.is_empty():
return slot
return null
func get_total_weight() -> float:
var total := 0.0
for slot in slots:
if slot.item:
total += slot.item.weight * slot.amount
return totalInventory Slot
# inventory_slot.gd
class_name InventorySlot
extends Resource
signal slot_changed
var item: Item = null
var amount: int = 0
func is_empty() -> bool:
return item == null
func clear() -> void:
item = null
amount = 0
slot_changed.emit()Equipment System
# equipment.gd
class_name Equipment
extends Resource
signal equipment_changed(slot: String, item: Item)
@export var weapon: Item = null
@export var armor: Item = null
@export var accessory: Item = null
func equip(slot: String, item: Item) -> Item:
var old_item: Item = null
match slot:
"weapon":
old_item = weapon
weapon = item
"armor":
old_item = armor
armor = item
"accessory":
old_item = accessory
accessory = item
equipment_changed.emit(slot, item)
return old_item
func unequip(slot: String) -> Item:
return equip(slot, null)
func get_total_stats() -> Dictionary:
var stats := {
"attack": 0,
"defense": 0,
"speed": 0
}
for item in [weapon, armor, accessory]:
if item and item.has("stats"):
for key in item.stats:
stats[key] += item.stats[key]
return statsUI Integration
# inventory_ui.gd
extends Control
@onready var grid := $GridContainer
var inventory: Inventory
func _ready() -> void:
inventory.inventory_changed.connect(refresh_ui)
refresh_ui()
func refresh_ui() -> void:
# Clear existing
for child in grid.get_children():
child.queue_free()
# Create slot UI
for slot in inventory.slots:
var slot_ui := InventorySlotUI.new()
slot_ui.setup(slot)
grid.add_child(slot_ui)Crafting Integration
# crafting_recipe.gd
class_name CraftingRecipe
extends Resource
@export var result: Item
@export var result_amount: int = 1
@export var requirements: Array[CraftingRequirement]
func can_craft(inventory: Inventory) -> bool:
for req in requirements:
if not inventory.has_item(req.item, req.amount):
return false
return true
func craft(inventory: Inventory) -> bool:
if not can_craft(inventory):
return false
# Remove ingredients
for req in requirements:
inventory.remove_item(req.item, req.amount)
# Add result
inventory.add_item(result, result_amount)
return trueSave/Load
func save_inventory() -> Dictionary:
return {
"slots": slots.map(func(s): return s.to_dict())
}
func load_inventory(data: Dictionary) -> void:
for i in data.slots.size():
slots[i].from_dict(data.slots[i])
inventory_changed.emit()Best Practices
1. Use Resources - Items as Resources, not class instances 2. Signal-Driven UI - Emit signals, let UI listen 3. Stack Logic - Always check max_stack first 4. Weight Limits - Validate before adding
---
Elite Godot 4.x Patterns
1. Refined Partial Stacking & Overflow Logic
Decouple data from UI using Resource arrays. Use a two-pass approach: fill existing partial stacks first, then seek empty slots for overflow.
# inventory_data.gd
func add_item(item: Item, amount: int) -> int:
var remaining := amount
# Pass 1: Fill partial stacks
for slot in slots:
if slot.item == item and slot.amount < item.max_stack:
var space := item.max_stack - slot.amount
var to_add := mini(remaining, space)
slot.amount += to_add
remaining -= to_add
if remaining == 0: break
# Pass 2: Fill empty slots
if remaining > 0:
for slot in slots:
if slot.is_empty():
var to_add := mini(remaining, item.max_stack)
slot.item = item
slot.amount = to_add
remaining -= to_add
if remaining == 0: break
inventory_changed.emit()
return remaining # Returns overflow2. Spatial Grid Inventory (Tetris-Style)
Use a Dictionary keyed by Vector2i for O(1) coordinate lookups. Items occupy multiple cells defined by a footprint.
# grid_inventory.gd
class_name GridInventory extends Resource
var _grid: Dictionary[Vector2i, Item] = {}
func can_place(item: Item, pos: Vector2i) -> bool:
for offset in item.grid_footprint:
var check_pos := pos + offset
if _grid.has(check_pos) or is_out_of_bounds(check_pos):
return false
return true
func place_item(item: Item, pos: Vector2i) -> void:
if can_place(item, pos):
for offset in item.grid_footprint:
_grid[pos + offset] = item
emit_changed()3. Safe Resource Serialization (JSON Mapping)
Avoid bloating save files with recursive Resource data. Save only the resource_path and use ResourceLoader.load() at runtime to restore references to static item blueprints.
# inventory_serializer.gd
func save_inventory(slots: Array[InventorySlot]) -> void:
var data := []
for slot in slots:
if not slot.is_empty():
data.append({
"path": slot.item.resource_path,
"amount": slot.amount
})
var file := FileAccess.open("user://inv.json", FileAccess.WRITE)
file.store_string(JSON.stringify(data))
func load_inventory() -> void:
# ... read JSON string ...
for entry in data:
var slot := InventorySlot.new()
slot.item = ResourceLoader.load(entry.path) # Efficiently loads cached reference
slot.amount = entry.amountReference
- Master Skill: godot-master
# consumable_item_logic.gd
# Extensible item behavior via inheritance
class_name PotionItem extends InventoryItem
@export var heal_amount: int = 50
func use(actor: Node) -> void:
if actor.has_method("heal"):
actor.heal(heal_amount)
print("Used potion: Healed ", heal_amount)
# drag_and_drop_slot.gd
# Implementing native Godot drag & drop for inventory
extends PanelContainer
# EXPERT NOTE: Using Godot's built-in drag/drop methods
# ensures consistent behavior and cross-platform support.
var slot_data: ItemSlot
func _get_drag_data(_at_position: Vector2) -> Variant:
if slot_data.item == null: return null
var preview = TextureRect.new()
preview.texture = slot_data.item.icon
set_drag_preview(preview)
return slot_data
func _can_drop_data(_at_position: Vector2, data: Variant) -> bool:
return data is ItemSlot
func _drop_data(_at_position: Vector2, data: Variant) -> void:
var dropped_slot = data as ItemSlot
# Swap logic goes here
# (e.g. notify inventory_data to swap indices)
# skills/inventory-system/code/grid_inventory_logic.gd
extends Resource
class_name GridInventory
## Grid Inventory Expert Pattern
## Implements Tetris-style cell occupancy and rotation math.
@export var grid_size: Vector2i = Vector2i(10, 8)
var _grid: Array = [] # 2D Array of item_ids or null
func _init() -> void:
for x in grid_size.x:
_grid.append([])
for y in grid_size.y:
_grid[x].append(null)
func can_place_item(item_size: Vector2i, pos: Vector2i) -> bool:
# 1. Bounds & Occupancy Check
if pos.x < 0 or pos.y < 0 or \
pos.x + item_size.x > grid_size.x or \
pos.y + item_size.y > grid_size.y:
return false
for x in range(pos.x, pos.x + item_size.x):
for y in range(pos.y, pos.y + item_size.y):
if _grid[x][y] != null:
return false
return true
func place_item(item_id: String, item_size: Vector2i, pos: Vector2i) -> void:
if not can_place_item(item_size, pos): return
for x in range(pos.x, pos.x + item_size.x):
for y in range(pos.y, pos.y + item_size.y):
_grid[x][y] = item_id
## EXPERT NOTE:
## For irregular shapes (L-shapes, T-shapes), use a 'BitMap' or an
## 'Array of Vector2i' representing the relative cell offsets instead
## of a simple 'item_size' Vector2i.
## For 'inventory-system', ALWAYS decouple the 'InventoryResource' (Data)
## from the 'InventoryPanel' (UI). The UI should only listen to signals
## (e.g., 'item_placed', 'item_removed') and update its visual grid accordingly.
# inventory_data_resource.gd
# Centralized inventory storage and logic
class_name InventoryData extends Resource
# EXPERT NOTE: Move all logic into the Resource to make it
# decoupled from any specific Node (Player vs Chest).
signal inventory_updated
@export var slots: Array[ItemSlot] = []
func add_item(new_item: InventoryItem, count: int = 1) -> bool:
# 1. Try to stack
if new_item.stackable:
for slot in slots:
if slot.item == new_item and slot.quantity < new_item.max_stack:
slot.quantity += count
inventory_updated.emit()
return true
# 2. Find empty slot
for i in range(slots.size()):
if slots[i].item == null:
var new_slot = ItemSlot.new()
new_slot.item = new_item
new_slot.quantity = count
slots[i] = new_slot
inventory_updated.emit()
return true
return false
func remove_at(index: int):
slots[index].item = null
slots[index].quantity = 0
inventory_updated.emit()
# skills/inventory-system/scripts/inventory_grid.gd
extends Control
## Inventory Grid Expert Pattern
## Grid-based inventory with drag-and-drop support and auto-sorting.
class_name InventoryGrid
signal item_dropped(item: InventoryItem, target_index: int)
signal item_removed(item: InventoryItem)
@export var inventory_data: InventoryData
@export var slot_scene: PackedScene
@export var columns := 5
@onready var grid_container: GridContainer = $GridContainer
func _ready() -> void:
if inventory_data:
inventory_data.inventory_changed.connect(_on_inventory_changed)
_refresh_display()
func _on_inventory_changed() -> void:
_refresh_display()
func _refresh_display() -> void:
# Clear existing sorting
for child in grid_container.get_children():
child.queue_free()
grid_container.columns = columns
for i in inventory_data.slots.size():
var slot = slot_scene.instantiate()
grid_container.add_child(slot)
# Configure slot
if inventory_data.slots[i]:
slot.set_item(inventory_data.slots[i])
# Connect drag/drop signals
slot.gui_input.connect(_on_slot_gui_input.bind(i))
func _on_slot_gui_input(event: InputEvent, index: int) -> void:
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT:
if event.pressed:
# Begin Drag
var item = inventory_data.slots[index]
if item:
var drag_data = {
"item": item,
"source_index": index,
"source_inventory": self
}
var preview = TextureRect.new()
preview.texture = item.icon
preview.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
preview.custom_minimum_size = Vector2(50, 50)
force_drag(drag_data, preview)
func _can_drop_data(_at_position: Vector2, data: Variant) -> bool:
return data is Dictionary and data.has("item")
func _drop_data(_at_position: Vector2, data: Variant) -> void:
var item = data.item
var source_index = data.source_index
var source_inventory = data.source_inventory
# Calculate sorting index based on local mouse position in grid
# This requires raycasting or math logic depending on specific UI setup
# For simplicity, we'll append or swap logic here
if source_inventory == self:
# Swap internal
pass
else:
# Move from sorting
pass
# This script serves as a UI controller foundation
# Implement exact drop logic based on your slot layout
## EXPERT USAGE:
## 1. Create InventoryGrid scene
## 2. Assign InventoryData resource
## 3. Connect signals for gameplay logic
# inventory_item_resource.gd
# Base Resource for all inventory items
class_name InventoryItem extends Resource
# EXPERT NOTE: Defining item properties in a Resource allows
# for creating .tres database files.
@export var id: String = ""
@export var name: String = "New Item"
@export var icon: Texture2D
@export var stackable: bool = false
@export var max_stack: int = 99
@export_multiline var description: String = ""
func use(_actor: Node) -> void:
# Virtual method for item behavior
pass
# inventory_persistence.gd
# Saving and loading complex inventory structures
extends Node
@export var inventory: InventoryData
func save_to_file(path: String):
# Serializing the entire Resource tree automatically
var err = ResourceSaver.save(inventory, path)
if err != OK:
push_error("Inventory save failed: ", err)
func load_from_file(path: String):
if ResourceLoader.exists(path):
inventory = load(path)
# inventory_ui_controller.gd
# Mapping data to visual representations
extends GridContainer
# EXPERT NOTE: The UI should listen to the Data Resource.
# This is the "Reactive UI" pattern.
@export var inventory_data: InventoryData
func _ready():
if inventory_data:
inventory_data.inventory_updated.connect(_on_inventory_updated)
_render_inventory()
func _render_inventory():
for child in get_children():
child.queue_free()
for slot in inventory_data.slots:
var slot_ui = preload("res://ui/inventory_slot.tscn").instantiate()
add_child(slot_ui)
slot_ui.set_slot_data(slot)
func _on_inventory_updated():
_render_inventory()
# item_database_loader.gd
# Global item registry pattern
extends Node
# EXPERT NOTE: Pre-assigning unique IDs to items allows
# the save system to store IDs instead of full resources.
var items: Dictionary = {}
func _ready():
var dir = DirAccess.open("res://items/")
dir.list_dir_begin()
var filename = dir.get_next()
while filename != "":
if filename.ends_with(".tres"):
var item = load("res://items/" + filename) as InventoryItem
items[item.id] = item
filename = dir.get_next()
func get_item(id: String) -> InventoryItem:
return items.get(id)
# item_pickup_node.gd
# World-to-Inventory bridge
extends Area2D
@export var item_data: InventoryItem
@export var count: int = 1
func _on_body_entered(body: Node2D):
if body.has_method("get_inventory"):
var inv = body.get_inventory() as InventoryData
if inv.add_item(item_data, count):
queue_free()
# item_slot_data.gd
# Data structure for a single inventory slot
class_name ItemSlot extends Resource
# EXPERT NOTE: Using a Resource for slots makes the
# inventory system serializable and easy to sync with UI.
signal changed
@export var item: InventoryItem:
set(val):
item = val
changed.emit()
@export var quantity: int = 1:
set(val):
quantity = val
changed.emit()
# LootTableResource.gd
# Randomized item drops definition
class_name LootTable extends Resource
# EXPERT NOTE: Defining loot tables as Resources allow you
# to assign "EliteTable" to bosses and "TrashTable" to minions.
@export var possible_items: Array[InventoryItem] = []
@export var drop_chances: Array[float] = []
func get_random_drop() -> InventoryItem:
var roll = randf()
# Weighted random calculation here...
return possible_items[0] # Placeholder