Migrating from v1.x to v2.0.0¶
Welcome to Modular Inventory v2.0.0! This major release introduces an Equipment Manager to automate item logic, a UI State Manager for handling menus, and dynamic slot stacking limits.
Breaking Changes
Upgrading will require updates to your Player Controller, UI toggling logic, and custom Slot Rules.
1. The New EquipmentManager (Automated Item Logic)¶
In v1.x, you had to manually detect input in your player controller and call on_primary_use() on your item logic. This is no longer required.
v2.0.0 introduces the EquipmentManager node. It automatically listens for input, handles charging/releasing, instantiates 3D weapon models, and updates the active item's logic every frame.
How to migrate:¶
- Add an
EquipmentManagernode to your Player scene. - In the Inspector, assign your
Playernode,InventoryComponent, andModularHotbar. - Delete all custom item-use code (like
if Input.is_action_just_pressed("attack"): logic.on_primary_use()) from your player controller script. TheEquipmentManagerhandles it automatically!
2. UIStateManager Replaces Manual UI Toggling¶
In v1.x, opening a chest required manually toggling visible, calling UICoordinator, and calling InputMode.ui().
v2.0.0 introduces the UIStateManager Autoload. It manages a stack of UI panels, automatically adds a dark background overlay, and can optionally pause the game.
Old Code (v1.x)¶
func open_chest():
player_ui.visible = true
chest_ui.visible = true
UICoordinator.position_inventory(player_ui, "player")
UICoordinator.position_inventory(chest_ui, "container", player_ui)
InputMode.ui()
New Code (v2.0.0)¶
Instead of placing the chest UI in your scene and toggling it, save your chest UI as a .tscn (PackedScene) and spawn it via the manager:
@export var chest_ui_scene: PackedScene
@export var chest_inventory: InventoryComponent
func open_chest():
# Ensure player inventory is open first
UIStateManager.open_panel(player_ui_scene, player_inventory.inventory, "Inventory", "player")
# Open the chest. The manager handles positioning, background, and mouse mode!
UIStateManager.open_panel(chest_ui_scene, chest_inventory.inventory, "Chest", "container")
UIStateManager.close_all().
3. ItemLogic Signature Changes¶
Because the EquipmentManager now handles context, the setup() method for ItemLogic has been expanded to provide more data to your items.
New Code (v2.0.0)
Additionally, a new cleanup() method was added. Override it to free any custom nodes or timers you created during setup().
4. Slot Rules & Dynamic Stacking¶
Slot Rules now support limiting how many items can be added to a slot, rather than just a simple "yes/no" acceptance.
If you wrote custom SlotRule scripts, you must update the signature of can_accept_item to include the amount parameter:
Old Code (v1.x)
New Code (v2.0.0)
New Feature: You can now override get_max_allowed_amount(item, current_count, inventory) -> int in your rules. For example, the built-in MaxOneRule now uses this to enforce a strict limit of 1 item, even if the item's max_stack_size is 64.
5. UI Component Refactoring (InventoryBinder & SlotGrid)¶
If you are building custom UI panels, the way slots are generated has been decoupled.
- InventoryBinder: A new Control node that automatically finds all SlotUI children and binds them to an inventory.
- SlotGrid: A new GridContainer that dynamically generates SlotUI nodes based on a slot_count export.
You no longer need to manually write loops to instantiate SlotUI nodes in your custom panels; just use SlotGrid!
Project Setup¶
This guide explains how to integrate the Modular Inventory System into your Godot 4.6 project.
Enabling the Plugin¶
- Copy the
addons/modular_inventory_system/folder into your project'sres://addons/directory. - Go to Project > Project Settings > Plugins.
- Check the Enable box next to Modular Inventory.
Autoloads¶
When you enable the plugin, InventoryPlugin.gd automatically registers global nodes that are always available (Autoloads). You do not need to add them manually:
| Autoload | Description |
|---|---|
DragDropSystem |
Handles global mouse input for dragging items, calculating drop targets, and dropping items into the 3D world. |
InputMode |
Manages mouse capture states (MOUSE_MODE_CAPTURED vs VISIBLE) so your player controller and UI don't fight for mouse control. |
UICoordinator |
Handles the positioning of multiple inventory panels (used automatically by UIStateManager). |
UIStateManager |
(New in v2.0) Manages a stack of UI panels, handles background overlays, game pausing, and quick-move targeting. |
Core Nodes¶
To give a Node (like your Player or a Chest) an inventory, you must attach the InventoryComponent node to it.
To handle item usage (attacks, consumables) automatically, add an EquipmentManager node to your Player.
Accessing the Inventory
You can access the underlying Inventory resource from any script via code:
Quick Start¶
Get up and running with the Modular Inventory System in just a few steps.
1. Add InventoryComponent & EquipmentManager¶
Give your Player an inventory and the ability to use items.
- Add an
InventoryComponentto your Player. Set Capacity to20. - Add an
EquipmentManagerto your Player. - In the
EquipmentManagerInspector: - Assign your Player node.
- Assign the Main Inventory (
InventoryComponent). - Assign your Hotbar (
ModularHotbar).
No Manual Input Code Needed!
The EquipmentManager automatically listens for mouse clicks and triggers on_primary_use() on your items. You do not need to write input handling code in your player controller!
2. Create an Item Definition¶
Items are data-driven using the ItemDefinition resource.
- In the FileSystem dock, right-click and select Create New > Resource.
- Search for and select
ItemDefinition. - Name it
apple.tres. - In the Inspector, fill out the properties:
- ID:
apple - Display Name:
Apple - Max Stack Size:
64 - Icon: Drag and drop your 2D Apple icon here.
3. Add Items to Inventory¶
extends CharacterBody3D
@export var starting_apple: ItemDefinition
@onready var inv_comp: InventoryComponent = $InventoryComponent
func _ready():
inv_comp.inventory_ready.connect(_on_inventory_ready)
func _on_inventory_ready(inv: Inventory):
if starting_apple:
inv.add_item(starting_apple, 5)
4. Setup the UI¶
Inventory Panel¶
- Add a
CanvasLayerto your player scene. - Add a
ModularInventoryPanelnode. - In the Inspector, assign the Grid Container (create a
GridContainerchild) and the Source Component (yourInventoryComponent). - Save this entire UI setup as a PackedScene (e.g.,
player_inventory.tscn).
Hotbar¶
- Add a
ModularHotbarnode to yourCanvasLayer. - Assign the Slots Container (
HBoxContainer) and Source Component.
5. Toggle the UI with UIStateManager¶
In v2.0, we use the global UIStateManager to open and close inventories. It handles mouse capture and background dimming automatically!
extends CharacterBody3D
@export var player_ui_scene: PackedScene
@export var toggle_action: StringName = &"toggle_inventory"
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed(toggle_action):
if UIStateManager.has_open_ui():
UIStateManager.close_all()
else:
# Open the player inventory
var inv = $InventoryComponent.get_inventory()
UIStateManager.open_panel(player_ui_scene, inv, "Inventory", "player")
get_viewport().set_input_as_handled()
Next Steps¶
- Learn how to restrict items to specific slots using Slot Rules.
- Add custom behaviors to items with Item Logic.
- Build interactive chests and containers in the Advanced Integration guide.
Item Logic¶
ItemLogic allows you to attach custom, scriptable behaviors to items. Instead of writing massive match statements in your player controller, you assign a Logic script to an ItemDefinition.
Automatic Execution via EquipmentManager
You do not need to write code in your player controller to trigger item logic!
Simply add an EquipmentManager node to your player, assign your ModularHotbar, and it will automatically detect input and call on_primary_use(), on_release(), and update() on the active item.
Base Class: ItemLogic (RefCounted)¶
All custom logic scripts must extend ItemLogic.
Setup & Teardown¶
# Called when the logic is initialized. v2.0 provides full context!
func setup(item: ItemDefinition, player: Node, slot_index: int = -1, weapon_model: Node3D = null, inventory: Inventory = null) -> void
# Called when the item is unequipped or the slot changes. Free custom nodes here!
func cleanup() -> void
Execution Methods¶
# Return false to prevent the item from being used (e.g., out of stamina).
func can_use() -> bool
# Called when the player presses the "Primary Use" button (e.g., Left Click).
func on_primary_use(slot_index: int = -1) -> void
# Called when the player presses the "Secondary Use" button (e.g., Right Click).
func on_secondary_use(slot_index: int = -1) -> void
# Called when the player releases the "Use" button (great for charged attacks).
func on_release() -> void
# Called every frame while the item is being held/used.
func update(delta: float) -> void
# Direct access to raw input events (handled by EquipmentManager).
func on_input(event: InputEvent) -> void
Signals¶
use_started: Emitted when use begins.use_ended: Emitted when use finishes.use_finished(item: ItemDefinition, success: bool): Emitted with the final result.
Built-in Helper¶
# Automatically reduces durability using the provided inventory context.
func _consume_item_durability(slot_index: int, amount: int = 1) -> bool
Slot Rules¶
Slot Rules allow you to restrict what items can be placed into specific inventory slots.
Base Class: SlotRule (Resource)¶
All rules extend SlotRule. In v2.0, rules now support dynamic stacking limits.
# Returns true if the item is allowed in this slot.
# 'amount' is the number of items being dropped/added.
func can_accept_item(item: ItemDefinition, slot_index: int, inventory: Inventory, amount: int = 1) -> bool
# Returns the maximum number of items allowed in this slot (overrides item.max_stack_size).
# Return -1 to use the item's default max_stack_size.
func get_max_allowed_amount(item: ItemDefinition, current_count: int, inventory: Inventory) -> int
# Returns a human-readable string explaining why the item was rejected.
func get_rejection_reason(item: ItemDefinition, slot_index: int) -> String
Built-in Rules¶
EquipmentTypeRule¶
Locks a slot to a specific integer equipment_type.
ItemTagRule¶
Requires items to have specific string tags.
MaxOneRule¶
Prevents items from stacking in a specific slot.
Dynamic Limits
MaxOneRule now implements get_max_allowed_amount() to return 1. This means even if you drag a stack of 64 swords onto an equipment slot, the system will only allow 1 to be placed, preventing illegal stacking.
Containers & Chests¶
Creating an interactive container (like a chest) in v2.0 is handled entirely by the UIStateManager. You no longer need to manually toggle visibility or manage mouse states.
Step-by-Step Setup¶
1. Physical Container & Inventory¶
- Create a
StaticBody3DorRigidBody3Dfor your chest. - Add an
InventoryComponentas a child. Set the Capacity.
2. Create the Chest UI Scene¶
- Create a new Scene (e.g.,
chest_ui.tscn). - Add a
ModularInventoryPanel(or useSlotGrid+InventoryBinder). - Do not assign the
source_componentin the Inspector. TheUIStateManagerwill bind the inventory dynamically at runtime. - Save this scene.
3. Interaction Logic¶
When the player presses "E", use the UIStateManager to open both the player and chest inventories.
@export var player_ui_scene: PackedScene
@export var chest_ui_scene: PackedScene
@export var player_inv: InventoryComponent
@export var chest_inv: InventoryComponent
func open_chest():
# 1. Open Player Inventory (Role: "player" -> anchors to the right)
UIStateManager.open_panel(
player_ui_scene,
player_inv.inventory,
"Inventory",
"player"
)
# 2. Open Chest Inventory (Role: "container" -> anchors to the left)
UIStateManager.open_panel(
chest_ui_scene,
chest_inv.inventory,
"Chest",
"container"
)
# The UIStateManager automatically:
# - Calls InputMode.ui() to show the mouse cursor.
# - Adds a dark background overlay.
# - Tells UICoordinator to position them side-by-side.
4. Closing the Container¶
To close the container, simply call close_all().
func close_chest():
UIStateManager.close_all()
# The UIStateManager automatically:
# - Frees the UI panels.
# - Calls InputMode.game() to capture the mouse.
# - Removes the background overlay.
Quick Moving Items
Because both panels are registered with the UIStateManager, players can now Shift+Left-Click items to instantly transfer entire stacks between the chest and their inventory!
(Note: In v2.0, Right-clicking no longer quick-moves; instead, it starts a drag operation picking up exactly 1 item).
UI Coordinator¶
Managed by UIStateManager
In v2.0, you rarely need to call UICoordinator directly. The UIStateManager automatically calls it behind the scenes whenever you open or close panels to ensure they are arranged perfectly side-by-side.
How it Works¶
UICoordinator looks at the ui_role metadata assigned to panels by the UIStateManager and applies anchor presets.
| Role | Behavior |
|---|---|
"player" |
Anchors the UI to the right side of the screen. |
"container" |
Anchors to the left side if a player UI is open. Otherwise, centers itself. |
"default" |
Places the UI dead center in the middle of the screen. |
Manual API (For Custom UI Systems)¶
If you are building your own UI manager and need to position panels manually, you can use:
# Arrange an array of open panels automatically
UICoordinator.arrange_panels(panels_array)
# Position a specific panel manually
UICoordinator.position_panel(panel_control, "primary") # "primary", "secondary", "centered"
UI Components Overview¶
The UI system is highly decoupled. Visuals update based on signals from the Inventory resource.
Core Components¶
ModularInventoryPanel: A window that shows all inventory slots in a grid.ModularHotbar: A horizontal row of slots for quick access.SlotUI: A single slot showing the item's picture, stack count, and durability bar.ItemTooltip: A popup window showing formatted BBCode text.
Advanced UI Building (v2.0)¶
If you want to build completely custom UI layouts (e.g., a character paperdoll screen), v2.0 introduces two helper nodes:
InventoryBinder¶
A Control node that automatically searches its children for SlotUI nodes and binds them to an Inventory. It also handles tooltip routing and quick-move logic.
SlotGrid¶
A GridContainer that dynamically instantiates SlotUI nodes based on a slot_count export.
Usage:
1. Add InventoryBinder to your custom UI.
2. Add SlotGrid as a child.
3. Call binder.bind_inventory(inventory_resource) via code, or let the UIStateManager handle it automatically when opening the panel.
API Reference¶
New Autoloads & Managers¶
UIStateManager¶
Extends: Node (Autoload singleton)
Manages a stack of UI panels, background overlays, and game pausing.
Methods:
| Method | Description |
|---|---|
open_panel(scene: PackedScene, inv: Inventory, title: String, role: String, type: UIType) -> Control |
Instantiates a UI panel, binds the inventory, and arranges it. |
close_all() -> void |
Closes all open panels, restores mouse capture, and removes overlays. |
close_top_ui() -> void |
Closes only the most recently opened panel. |
get_other_inventory(current_inv: Inventory) -> Inventory |
Finds the second open inventory (used for Quick Move). |
has_open_ui() -> bool |
Returns true if any panels are open. |
EquipmentManager¶
Extends: Node
Automates item logic execution and 3D model equipping.
Properties:
| Property | Type | Description |
|---|---|---|
player |
Node3D |
The player node. |
main_inventory |
InventoryComponent |
The player's inventory. |
hotbar |
ModularHotbar |
The hotbar to track active items from. |
item_socket |
Marker3D |
Optional 3D node to attach weapon models to. |
Signals:
| Signal | Description |
|---|---|
active_item_changed(item: ItemDefinition, slot_index: int) |
Emitted when the player scrolls to a new hotbar slot. |
Updated Core Classes¶
Inventory¶
Updated Method:
| Method | Description |
|---|---|
can_accept_at_slot(item: ItemDefinition, slot_index: int, amount: int = 1) -> bool |
Checks if slot accepts item, factoring in dynamic rule limits. |
ItemLogic (base class)¶
Updated Setup Signature:
| Method | Description |
|---|---|
setup(item: ItemDefinition, player: Node, slot_index: int = -1, weapon_model: Node3D = null, inventory: Inventory = null) -> void |
Called with full context by the EquipmentManager. |
cleanup() -> void |
Called when the item is unequipped. Override to free custom nodes. |
on_input(event: InputEvent) -> void |
Raw input event passed from EquipmentManager. |
SlotRule (base class)¶
Updated Methods:
| Method | Description |
|---|---|
can_accept_item(item: ItemDefinition, slot_index: int, inventory: Inventory, amount: int = 1) -> bool |
Now includes the amount being dropped. |
get_max_allowed_amount(item: ItemDefinition, current_count: int, inventory: Inventory) -> int |
(New) Returns the max stack size allowed by this specific rule. Return -1 to defer to the item's default. |
New UI Components¶
InventoryBinder¶
Extends: Control
Automatically binds child SlotUI nodes to an inventory and handles tooltips/quick-moves.
Method: bind_inventory(inv: Inventory) -> void
SlotGrid¶
Extends: GridContainer
Dynamically generates SlotUI nodes.
Properties: slot_scene, start_index, slot_count.
Method: bind_inventory(inv: Inventory) -> void