M7.1 — The Inventory Model¶
What you'll learn
- How to model an inventory as data — a capacity-bounded list of
ItemData— separate from any UI. - Add/remove operations, a "full" condition, and the signals the UI (M7.3) will listen to.
- Why the inventory is a data object (an autoload or a Resource), not a scene, and how the M6.4
item_picked_upsignal feeds it. - A note on stacking for consumables versus unique equipment.
How it applies
- The inventory is the player's relationship with their loot. Everything M6 drops lands here; this is the structure that holds the run's accumulated power. If the model is clean — data separate from display — the UI, tooltips, equipping, and saving all attach to it without fighting each other.
- Capacity is a design constraint with real consequences. A bounded bag forces decisions (keep or drop?), which is core to the loot-management loop; an unbounded bag removes that tension. Either is valid, but it must be a deliberate choice expressed in the model, and the "full" case must be handled (what happens to a pickup when the bag is full?).
- Data/UI separation is what makes it testable and saveable. An inventory that is pure data can be unit-tested (add N items, assert contents), saved (M8 serializes the list), and rendered by any UI. Bake the inventory into a UI scene and none of that is clean.
- One model, many listeners. The bag emits "contents changed"; the inventory grid, a weight indicator, and an "inventory full" warning all react. The same signal-driven, single-source-of-truth discipline as Health (M3.2) and stats (M5.4).
Concepts¶
Inventory is data, not a scene¶
The inventory is a list of items with a capacity and operations to change it — no visuals. So it is a data object: either an autoload (one global inventory for the player, reachable everywhere) or a Resource (if you want multiple inventories — player, stash, vendor). This book uses an autoload for the single player inventory, consistent with the M1.4 roster, and notes the Resource option for when stashes appear.
The UI (M7.3) is a separate scene that reads this model and renders it, reacting to its signals. The
model never references the UI. This is the same data/presentation split as ItemData (data) vs
ItemPickup (node), now at the container level.
The model¶
Example
An inventory autoload: a bounded list with add/remove and a change signal.
# res://scripts/inventory.gd (registered as autoload "Inventory")
extends Node
signal changed # contents changed; UI redraws
signal full # an add failed because the bag is full
@export var capacity: int = 24
var items: Array[ItemData] = []
func _ready() -> void:
SignalBus.item_picked_up.connect(add_item) # M6.4 feeds the bag
func is_full() -> bool:
return items.size() >= capacity
func add_item(item: ItemData) -> bool:
if is_full():
full.emit()
return false
items.append(item)
changed.emit()
return true
func remove_item(item: ItemData) -> void:
if items.erase(item): # erase returns true if it was present
changed.emit()
func has_room() -> bool:
return not is_full()
add_item is also the handler for SignalBus.item_picked_up (M6.4) — picking up an item adds it to the
bag with no extra glue. add_item returns whether it succeeded and emits full on failure, so the
pickup or UI can react (e.g., refuse to collect, or show "inventory full"). Every successful change
emits changed, the single signal the UI redraws on.
Handling "full"¶
A bounded bag must decide what happens when an item can't fit. Options, each a design choice:
- Refuse the pickup.
add_itemreturnsfalse; the pickup (M6.4) stays on the ground and shows a "bag full" message. Simple and honest. - Auto-drop the lowest-value item. More complex; needs a value metric. Usually not worth it early.
- Overflow to a stash. Requires a second inventory (the Resource option).
The model supports the first cleanly by returning success/failure. The key is that "full" is a handled, visible state, not a silently dropped item — losing loot without telling the player is a trust-breaking bug.
Stacking: consumables vs equipment¶
Unique equipment never stacks — each rolled item is distinct (different affixes), so it occupies its own
slot. Consumables (potions, currency) do stack: twenty potions should be one slot showing "×20," not
twenty slots. Stacking needs a quantity per entry and a "is this stackable, and does an existing stack
have room?" check on add. This book's equipment-focused inventory treats items as non-stacking by default;
the note here is so you know where stacking would slot in (a stackable flag on ItemData and a
quantity-aware add_item). Adding it later is a contained change to add_item, not a model rewrite.
Why an autoload here¶
The player has exactly one inventory, it is accessed from many places (pickup, UI, equip, save), and it
exists independently of any scene — the three autoload conditions from M1.4. So Inventory is an
autoload. If the game later needs stashes or vendor inventories (many instances), the model moves to a
Resource you instantiate per container; the operations stay the same. Recognizing which case you are in
is the judgment.
Walkthrough¶
- Create
res://scripts/inventory.gd(the autoload above) and register it:Project → Project Settings → Globals → Autoload, pathres://scripts/inventory.gd, nameInventory. Order it afterSignalBus(it connects to a bus signal in_ready). - Confirm the M6.4 chain now reaches the bag:
item_picked_up→Inventory.add_item. (The connection is inInventory._ready.) - (Temporary, until M7.3) Connect
Inventory.changedto a printer:func(): print("bag: ", Inventory.items.size(), "/", Inventory.capacity)andInventory.fulltofunc(): print("BAG FULL"). - Press
F5, kill skeletons, and collect drops. Watch the bag count rise on each pickup. Setcapacitylow (e.g., 3) and confirm the fourth pickup triggersfulland (depending on M6.4 wiring) the pickup refuses to be collected. - Remove the temporary printers (the real grid is M7.3).
Optional sanity check
Set capacity to 1, pick up one item, then walk over a second drop: add_item should return false and
emit full, and — if M6.4 checks the return value — the second pickup should remain on the ground
rather than vanishing. If the second item disappears with the bag still at 1, the pickup is collecting
without honoring add_item's result, silently destroying loot — the trust-breaking bug. Fix by having
_collect only queue_free when add_item returns true.
Self-check quiz¶
Q1 — Why is the inventory a data object (autoload/Resource) rather than part of the inventory UI scene?
A. Autoloads render faster.
B. Separating data from display lets the model be unit-tested, saved (M8), and rendered by any UI, with
the model emitting changed for listeners; baking it into the UI couples storage to one view and
blocks testing/saving.
C. UI scenes can't hold arrays.
D. Resources can't be edited at runtime.
Reveal answer
B. The data/UI split is what makes the inventory testable (add items, assert contents),
serializable (M8 saves the list), and view-agnostic; the UI subscribes to changed. A is
irrelevant. C is false. D is false (Resources are very much runtime-mutable).
Q2 — Why does add_item return a boolean and emit full on failure instead of just appending?
A. To make it faster. B. Because a bounded bag can reject an item, and the caller (pickup/UI) must know so it can keep the drop on the ground or warn the player; silently dropping the item loses loot without telling the player. C. Because GDScript functions must return a value. D. To stack items automatically.
Reveal answer
B. A full bag is a real state; returning success/failure lets the pickup refuse to vanish and the UI warn, so loot is never silently destroyed — a trust-breaking bug otherwise. A is irrelevant. C is false (functions can be void). D is unrelated (stacking is a separate feature).
Q3 — When would the inventory move from an autoload to a Resource?
A. Never. B. When the game needs multiple inventories (player bag, stash, vendor) — many instances — since an autoload is one global instance; the operations stay the same, only the container multiplicity changes. C. When the bag exceeds 24 slots. D. When items start stacking.
Reveal answer
B. An autoload is the right call for the single player inventory (global, one instance, scene-independent); the moment you need several inventories, a Resource you instantiate per container fits, with the same add/remove logic. A is false. C and D are unrelated to the single-vs-many distinction.
Integration question¶
Q4 — open
The inventory model receives items from M6.4's item_picked_up, will be rendered by M7.3's grid, feeds
M7.2's equip, and is serialized by M8. Explain how the data/UI separation and the single changed
signal make all four of those attach cleanly, and contrast what would go wrong if the inventory lived
inside the UI scene as, say, an array of slot nodes.
Reveal expected answer
The inventory as a pure data object with one changed signal is a hub every other system attaches
to without coupling: M6.4's pickup emits item_picked_up, which the model consumes via add_item
(no UI involved); M7.3's grid reads Inventory.items and redraws whenever changed fires,
without owning the data; M7.2's equip calls remove_item/add_item on the same model and the grid
updates through the same signal; and M8 serializes Inventory.items directly because it is plain
data, not entangled with nodes. Each system depends only on the model's data and its changed
signal, so they compose. If the inventory instead lived in the UI scene as an array of slot nodes,
the "contents" would be tangled with visual state: the pickup would have to find and manipulate UI
nodes to add an item, equipping would mutate node children, the save system would have to walk the
UI tree to extract data (and reconstruct nodes on load), and the model couldn't be unit-tested
without instantiating the whole UI. Worse, "what's in the bag" would have two possible answers (the
nodes vs any backing list), which drift — the same multiple-sources-of-truth failure the book
avoids everywhere. Keeping the inventory as data with one change signal is what lets pickup, UI,
equip, and save each attach by reading the model and listening for changed, exactly as Health and
stats are single sources with single change signals.