Quick answer: Build your inventory using a JSON object to define items and an Array (or JavaScript array) to track slot contents. Create a grid of slot sprites on a dedicated UI layer, use the Drag & Drop behavior for item movement, and serialize the inventory to Local Storage for persistence. The key is separating your data model from the visual representation — update the data first, then refresh the display.
Inventory systems appear in nearly every genre — RPGs need equipment grids, survival games need weight-limited backpacks, even puzzle games track collectible items. Construct 3 does not include a built-in inventory plugin, but its Array object, JSON support, and event system give you everything you need to build one from scratch. This guide walks through a complete, production-ready inventory system step by step.
Defining Your Item Data
Before writing any events, define your items in a structured format. Create a JSON file in your project files or store the data in a global variable. Each item needs a unique ID, a display name, the animation frame for its icon, a maximum stack size, an item type, and any relevant stats.
// Items.json - store in project files
{
"sword_iron": {
"name": "Iron Sword",
"icon": 0,
"type": "weapon",
"stackMax": 1,
"attack": 12,
"description": "A sturdy iron blade."
},
"potion_health": {
"name": "Health Potion",
"icon": 1,
"type": "consumable",
"stackMax": 20,
"healAmount": 50,
"description": "Restores 50 HP."
},
"wood": {
"name": "Wood",
"icon": 2,
"type": "material",
"stackMax": 99,
"description": "Basic crafting material."
}
}
Load this data at the start of the game using the AJAX plugin or the scripting API. Parse it into a global variable so every event sheet can access item definitions.
// Event sheet: On start of layout
System: On start of layout
AJAX: Request "items.json" Tag "items"
AJAX: On completed "items"
System: Set global variable ItemData to AJAX.LastData
Building the Inventory Grid
The inventory grid is a visual layer of slot sprites arranged in rows and columns. Create a sprite called InventorySlot with an instance variable SlotIndex (number). Create another sprite called InventoryItem with instance variables for ItemID (string), Quantity (number), and SlotIndex (number). The item sprite’s animation frames should match the icon indices from your JSON data.
// Create a 5x4 inventory grid (20 slots)
// Event sheet: Function "CreateInventoryGrid"
System: Set local variable SlotSize to 64
System: Set local variable Padding to 8
System: Set local variable Cols to 5
System: Set local variable StartX to 200
System: Set local variable StartY to 100
System: Repeat 20 times
System: Create object InventorySlot on layer "UI"
X: StartX + (loopindex % Cols) * (SlotSize + Padding)
Y: StartY + floor(loopindex / Cols) * (SlotSize + Padding)
InventorySlot: Set SlotIndex to loopindex
For the underlying data, use a JavaScript array where each index corresponds to a slot. Each element is either null (empty) or an object with itemId and quantity.
// Scripting: Initialize inventory data
const INVENTORY_SIZE = 20;
let inventory = new Array(INVENTORY_SIZE).fill(null);
// Add an item to the first available slot (with stacking)
function addItem(itemId, quantity) {
const itemDef = itemDatabase[itemId];
if (!itemDef) return 0;
let remaining = quantity;
// First pass: try to stack with existing items
for (let i = 0; i < INVENTORY_SIZE && remaining > 0; i++) {
if (inventory[i] && inventory[i].itemId === itemId) {
const canAdd = itemDef.stackMax - inventory[i].quantity;
const toAdd = Math.min(remaining, canAdd);
inventory[i].quantity += toAdd;
remaining -= toAdd;
}
}
// Second pass: fill empty slots
for (let i = 0; i < INVENTORY_SIZE && remaining > 0; i++) {
if (inventory[i] === null) {
const toAdd = Math.min(remaining, itemDef.stackMax);
inventory[i] = { itemId: itemId, quantity: toAdd };
remaining -= toAdd;
}
}
refreshInventoryDisplay();
return quantity - remaining; // items actually added
}
Drag-and-Drop Item Movement
Add the Drag & Drop behavior to the InventoryItem sprite. When the player picks up an item, record its starting slot. When they drop it, check which slot it overlaps and perform the appropriate action: move, swap, or stack.
// Event sheet: Drag and drop logic
// Store the original position on drag start
InventoryItem: On drag start
System: Set global variable DragOriginSlot to InventoryItem.SlotIndex
System: Set global variable DragOriginX to InventoryItem.X
System: Set global variable DragOriginY to InventoryItem.Y
InventoryItem: Move to top
// On drop - check for valid slot
InventoryItem: On drop
InventorySlot: Is overlapping InventoryItem
// Valid target slot found
System: Set local variable TargetSlot to InventorySlot.SlotIndex
Functions: Call "MoveItem" (DragOriginSlot, TargetSlot)
Else
// No valid slot - return to original position
InventoryItem: Set position to (DragOriginX, DragOriginY)
The MoveItem function handles three cases: moving to an empty slot, swapping two different items, or stacking identical items.
// Scripting: Handle item move/swap/stack
function moveItem(fromSlot, toSlot) {
if (fromSlot === toSlot) return;
const fromItem = inventory[fromSlot];
const toItem = inventory[toSlot];
if (!fromItem) return;
// Empty target slot: move
if (toItem === null) {
inventory[toSlot] = fromItem;
inventory[fromSlot] = null;
}
// Same item type: try to stack
else if (toItem.itemId === fromItem.itemId) {
const maxStack = itemDatabase[toItem.itemId].stackMax;
const canAdd = maxStack - toItem.quantity;
const toMove = Math.min(fromItem.quantity, canAdd);
toItem.quantity += toMove;
fromItem.quantity -= toMove;
if (fromItem.quantity <= 0) {
inventory[fromSlot] = null;
}
}
// Different items: swap
else {
inventory[fromSlot] = toItem;
inventory[toSlot] = fromItem;
}
refreshInventoryDisplay();
}
Equipment Slots and Item Types
Equipment slots work like inventory slots with restrictions. Create separate sprites for your equipment panel — weapon slot, armor slot, accessory slot — each with an instance variable AcceptType that specifies which item type fits there.
// Event sheet: Equipment slot validation
InventoryItem: On drop
EquipmentSlot: Is overlapping InventoryItem
// Check if item type matches slot type
System: Compare EquipmentSlot.AcceptType = InventoryItem.ItemType
Functions: Call "EquipItem" (InventoryItem.SlotIndex, EquipmentSlot.SlotName)
Functions: Call "RecalculateStats"
Else
// Wrong item type for this slot
InventoryItem: Set position to (DragOriginX, DragOriginY)
// Scripting: Apply equipment stats
function recalculateStats() {
let totalAttack = baseAttack;
let totalDefense = baseDefense;
for (const [slotName, itemId] of Object.entries(equipment)) {
if (itemId) {
const item = itemDatabase[itemId];
totalAttack += item.attack || 0;
totalDefense += item.defense || 0;
}
}
runtime.globalVars.PlayerAttack = totalAttack;
runtime.globalVars.PlayerDefense = totalDefense;
}
When the player equips an item, remove it from the inventory array and place it in a separate equipment object keyed by slot name. If an item was already equipped in that slot, move it back to the inventory.
Saving and Loading the Inventory
Persistence is critical. Use the Local Storage plugin to serialize your inventory state to JSON and restore it when the game loads.
// Scripting: Save inventory to Local Storage
function saveInventory() {
const saveData = {
inventory: inventory,
equipment: equipment,
gold: runtime.globalVars.Gold
};
localStorage.setItem("game_inventory", JSON.stringify(saveData));
}
// Scripting: Load inventory from Local Storage
function loadInventory() {
const raw = localStorage.getItem("game_inventory");
if (!raw) return;
try {
const saveData = JSON.parse(raw);
inventory = saveData.inventory;
equipment = saveData.equipment || {};
runtime.globalVars.Gold = saveData.gold || 0;
refreshInventoryDisplay();
refreshEquipmentDisplay();
} catch (e) {
console.error("Failed to load inventory:", e);
}
}
// Event sheet alternative using Local Storage plugin:
Functions: On "SaveGame"
Local Storage: Set item "inventory"
Value: Browser.ExecJS("JSON.stringify(inventory)")
Functions: On "LoadGame"
Local Storage: Get item "inventory"
Local Storage: On item get "inventory"
Browser: ExecJS "inventory = JSON.parse('" & LocalStorage.ItemValue & "')"
Functions: Call "RefreshInventoryDisplay"
Save automatically when the player closes the inventory, picks up a new item, or exits the game. Load immediately on start of layout before the inventory UI is created so the grid can be built with the correct data.
"Always save after every inventory change, not just when the player manually saves. Browser games can be closed at any moment, and losing inventory progress is one of the most frustrating experiences for players."
Related Issues
If your inventory items disappear after switching layouts, see Fix: Construct 3 Object Not Found After Layout Change. For problems with Local Storage not persisting between sessions, check Fix: Construct 3 Local Storage Data Not Persisting. If your drag-and-drop items flicker on mobile devices, see Fix: Construct 3 Sprite Flickering on Mobile Devices. And if your For Each loops skip items when updating the inventory display, check Fix: Construct 3 For Each Loop Skipping Instances.
Data first, display second. Always.