Unity inventory system tutorial, modular inventory UI

Building a Modular Inventory System in Unity: From Drag-and-Drop to Equipment Slots

Game UI Systems & Interaction Design

You’ll build a lean, phone-friendly inventory that handles drag-and-drop, stacking, and equipment slots without letting the UI become the source of truth. Mobile constraints matter: touch input, limited CPU, and garbage from per-frame UI rebuilds kill smooth play. Start by keeping the model pure data and the view event-driven.

Here’s a working first brick you can run in a unit test or a Debug.Log harness. The model stores only data, not scene GameObjects:

public int AddItem(ItemDefinition def, int amount) { /* stack, then fill empties, return leftover */ }

Quick test usage:

var leftover = container.AddItem(potionDef, 25); Debug.Log($"Leftover: {leftover}");

This approach maps well to phones: fewer allocations, predictable touch behavior, and no per-frame rebuilds. You’ll follow a clear roadmap in this post: data layer → model API → event-driven binding → drag/drop → equipment → pickups/drop → unique state → performance and tests. Later sections reference Unity’s EventSystem drag handler docs and ScriptableObject docs for data authoring used by PlayMobile.online author George Jones.

Set up the data layer first with ScriptableObjects and runtime item instances</h2>

Start your data model by keeping runtime state in plain C# so your mobile build stays predictable and easy to save.

Create an ItemDefinition asset

Define ItemDefinition as a ScriptableObjects asset that holds immutable metadata: ID, display name, icon sprite, max stack, world pickup prefab, and equipped prefab. This keeps asset lifetime separate from the scene and lets the asset reference prefabs safely without storing scene objects in your model.

Runtime classes: ItemStack and InventorySlot

Make lightweight classes/structs that only store an ItemDefinition (or ID) and a quantity. Do not include GameObject fields. This makes save/load and fast backgrounding reliable on phones.

Type Stored Fields Notes
ItemDefinition (asset) ID, name, icon, maxStack, prefabs Immutable, references prefabs
ItemStack / InventorySlot ItemDefinition or ID, quantity Pure data, no scene objects
Pickup prefab Reference to ItemDefinition, amount Visual only; spawn/despawn separate from model

AddItem logic and the Missing (Item) fix

Implement AddItem to fill existing stacks up to max, then use empties, and return leftover amount if full. Edge cases: partial fill and no empty slots must return the remaining count.

“Missing (Item)” happens when you store a scene instance in a list then destroy it. The reference becomes null in the scene and Unity shows Missing. Fix: store definitions or IDs in your data model, and spawn game objects purely for visuals.

Sanity check: after AddItem, print each slot as “ID x quantity” to verify stacks and leftovers before you build the view.

Decide your requirements before you write UI code (scope that won’t explode)

Before you write a single view script, pin down the exact features your inventory must support and what it must refuse to do. A short, explicit checklist keeps the model small and avoids late-stage rewrites.

Be explicit about constraints. For example, enforce “no split stacks” so your API only needs Merge and Swap, not complex partial-split logic. That single rule reduces edge cases and testing surface dramatically.

Define drag-and-drop behavior

  • Swapping: two slots exchange contents.
  • Merging: same item fills to max stack, leftover returned.
  • Rejecting: disallowed drops give clear feedback (shake, red outline, or toast).

Model vs View (MVC/MVVM-style)

Keep the model as plain C# with no scene objects. The view subscribes to events and sends commands like SwapSlots or MergeStacks. This separation makes the core logic testable and the view replaceable.

Area Requirement Note
Toolbelt 5 slots accepts consumables & tools
Equipment Head / Chest / Weapon typed slots only
Main 30 slots stack max from ItemDefinition

Finally, avoid copying feature lists from big games. Write constraints first, then design—especially for mobile where touch targets, layout, and scroll virtualization are real concerns.

Project and scene setup for a mobile-friendly inventory UI</h2>

A stable scene layout prevents layout recalcs and keeps touch input responsive on real devices. Plan the canvas, safe areas, and prefab map before you wire any code. This reduces stutter and avoids rebuild loops that kill frame time on phones.

Canvas settings for multiple aspect ratios and safe areas

Set the Canvas Scaler to a reference resolution that matches your target phones. Use the match width/height slider to bias tall screens so touch targets remain large.

Place a SafeArea panel at the root to offset close buttons and toolbelt slots away from notches. Readable buttons prevent accidental taps and improve play on diverse screens.

Prefab strategy: slot prefab, item icon prefab, equipment slot prefab

Map prefabs to your model: a Slot prefab holds background and drop target logic. An Item icon prefab contains the Image and count text. Equipment prefabs are typed drop targets bound to an enum, not hard-coded names.

Keep one stable canvas and avoid constantly instantiating nested canvases. That reduces draw calls and layout work. Spawn game objects only for visuals and update them on events.

Prefab Contents Binding
Slot prefab Background, drop target component Bind to slot index
Item icon prefab Image, count text, CanvasGroup Bind to item ID + qty
Equipment slot prefab Typed drop target, lock visuals Bind to EquipmentType enum
  • EventSystem present in the scene.
  • GraphicRaycaster enabled on the main Canvas.
  • Test taps and drag on actual device, not only in editor.

Build the inventory model API your UI can call</h2>

Design the API so the view calls intent methods and the model replies with success or a reason to reject. Keep the model pure C# data and expose a small command set the UI or inventory manager can call. That keeps logic testable and avoids storing scene objects in your data.

Expose one primary call like TryMove(int fromIndex, int toIndex, out string reason). The method decides between MergeStacks, SwapSlots, MoveToEmpty, or RejectMove. Always return bool + reason so your view can show clear feedback and avoid silent failures.

Raise events for minimal updates: OnSlotChanged(int index) for a single slot and OnInventoryChanged() for structural changes. The view subscribes and updates icons/text only on those events. This is a good idea in view-model design; the model never touches Images or text.

Command Purpose Outcome
TryMove Move or merge two slots bool + reason
MergeStacks Fill same-item stacks leftover returned
SwapSlots Exchange different items success/fail
SaveSnapshot Serialize list of slot snapshots ID, qty, custom state

Plan saving early: serialize an array of slot snapshots with ItemDefinitionID, quantity, and optional state blob (durability, ammo). Save on app pause so mobile suspend/resume does not lose items. This keeps your code resilient to OS kills and other mobile problems at the end of a session.

Bind the model to UI slots without polling every frame</h2>

Let slot components listen for index-specific updates so the screen redraws sparingly.

Reactive slot binding

Give each InventorySlotView an index and subscribe to OnSlotChanged(index). When that event fires, pull only that slot’s data and update visuals. Update the Image sprite, the count text, and an empty-state toggle. Do not touch other slots.

Why full rebuilds hurt on phones

Rebuilding the whole grid on every click causes layout recalcs, many Instantiate/Destroy calls, and GC spikes. That collection of work creates stutter during drag or rapid taps and wastes CPU and battery.

  • Subscribe in OnEnable, unsubscribe in OnDisable.
  • Guard against duplicate subscriptions when reopening screens.
  • Aim for zero allocations per click/drag and verify with the profiler on device.
Pattern What updates Benefit
Index event Sprite, count, empty flag Minimal redraws
Full rebuild (bad) All slots, re-layout Stutter, GC spikes
Polling/Update() Frequent checks Wastes time and battery

Implement drag-and-drop with EventSystem handlers

Make the drag flow predictable: capture the source, render a ghost icon, then commit one model command on drop. Use the uGUI drag interfaces on the visual icon so your model stays pure data.

Handlers and ghost visuals

Attach IBeginDragHandler, IDragHandler, and IEndDragHandler to the item icon component. Implement IDropHandler on the slot view.

Ghost icon pattern

On begin drag, clone the Image to a top-level canvas and set CanvasGroup.blocksRaycasts = false. Fade the original image so the user sees a pickup. Move the clone with pointer data and avoid creating new Sprite assets.

Pointer-to-slot mapping (step-by-step)

  • On begin: record sourceSlotIndex and pointerId.
  • During drag: move the ghost using eventData.position; handle quick taps gracefully.
  • On drop: read targetSlotIndex from the drop component and call TryMove(source, target).
Handler Where Responsibility
IBeginDragHandler Item icon component Capture source index, create ghost
IDragHandler Ghost on top canvas Follow pointer, minimal allocations
IEndDragHandler / IDropHandler Slot view / icon Resolve target index, call model command

Consult the official Unity Manual and EventSystem API for exact interface signatures and event order to match your code to platform behavior.

Make drag-and-drop actually change the inventory (not just move icons)

Treat every drop as a command to the model. The view should only request a move and then wait for a response. This keeps data and visuals in sync and reduces hard-to-find desync problems.

Drop rules: stack if same item, swap if different, reject if incompatible

  • If target is empty → move the item to that slot.
  • If same definition and stackable → merge up to max and return leftover.
  • If different → swap the two slot contents.
  • If an equipment slot disallows the object → reject and return a reason.

Beginner trap: letting the UI be the source of truth

A common problem is visually swapping icons before the model accepts the change. Then save, load, or game logic reads stale data and breaks. Don’t do that.

Make TryMove atomic: apply the change or do nothing, then fire slot-changed events. Return a rejection reason so the user gets clear feedback and your code stays robust on mobile.

Rule Action Outcome
Move to empty Model.Move Slot updated
Merge same item Model.Merge Combined, leftover returned
Incompatible Model.Reject Reason sent to view

Equipment slots: data rules + visuals</h2>

Treat equipment as a separate data container and keep visuals out of the model. That keeps your scripts simple and your saves reliable on phones.

Define slot types and make rules data-driven

Expose an EquipmentSlotType enum (Head, Chest, Weapon, Toolbelt) and add allowed types or tags to each ItemDefinition. This moves compatibility logic into assets and keeps code tidy.

Equipping and visual spawn flow

When a player equips, call a model command that checks compatibility. On success, instantiate the equipped prefab referenced by the item definition at the correct bone or anchor. Keep that visual separate from any world pickup prefab.

Unequipping and failure handling

Unequip commands move the data back to the first free slot. If no free slot exists, reject the action with a clear reason such as “No empty slot”. Don’t silently drop or delete items on mobile screens.

  • Keep equipment slots in the same plain-C# model as other containers.
  • Spawn equipped prefabs without adding colliders unless needed.
  • Give immediate feedback on rejects: toast, color flash, or brief message.
Slot Type Allowed Item Tags Visual Prefab Behavior Reject Reason
Head helmet Instantiate head prefab, attach to head bone Wrong item type
Chest armor Attach chest prefab, disable physics Wrong item type
Weapon sword, bow Spawn at hand anchor, play equip anim Incompatible weapon
Toolbelt tool, consumable Icon-only prefab, no colliders No empty slot

World pickups and dropping items without hacky inventory GameObjects</h2>

Separate the physics and visuals of a pickup from the data that describes the item in the bag. Keep your runtime model as plain data and let the scene host transient pickup actors. This avoids stored scene references and missing reference bugs when objects are destroyed.

Pickup prefab holds only definition + amount

Create a Pickup MonoBehaviour that references the ItemDefinition and an amount. Optionally serialize per-drop state (durability, tags) so unique drops keep state without adding heavy behaviors.

Pickup and drop flow

On pickup, call AddItem(def, amount) on your model. If leftover > 0, update the pickup’s amount. If leftover == 0, destroy the pickup game object. Do not “store the pickup” in the model.

On drop, remove from the model and spawn the worldPrefab referenced by the definition at the chosen point. Set the spawned Pickup fields to the dropped stack amount so the scene actor represents the dropped items only.

Why this is less hacky

Storing game objects in your data forces you to toggle colliders, disable physics, and wrestle missing references. Instead, scriptable objects can reference prefabs for colliders and animations while your model stays pure data. This prevents save/load problems and reduces runtime checks.

  • Pickup MonoBehaviour: ItemDefinition + amount (+ optional state).
  • Pickup action: model.AddItem → update or destroy pickup actor.
  • Drop action: model remove → spawn world prefab → set Pickup amount.
Approach Scene Actors Model Content Benefits
Storing GameObjects in model Embedded objects disabled/enabled Scene references Leads to missing refs, save issues
Pickup-as-actor (recommended) Transient pickup MonoBehaviour Definition + amount Clean save/load, separate physics
ScriptableObject-driven Prefabs referenced by assets Pure data & state IDs Author-time compatibility, simpler code

Handling per-item unique state on mobile (durability, ammo) without re-inventing MonoBehaviours

Give each stack a tiny, serializable instance object so per-item state like durability or ammo stays testable and compact. Keep immutable metadata in assets and put only mutable fields into the runtime record.

Wrapper instance pattern

Implement an ItemInstance plain C# class that references an ItemDefinition and stores fields such as durability, ammo, or rolled stats. Slots can hold instances or stacks of instances depending on your rules.

Behavior: MonoBehaviours vs plain C#

Keep collision, animation triggers, and VFX on scene components. Keep stacking math, durability math, and saveable numbers in pure C# so you can unit test and serialize without scene refs.

Avoid overengineering

Do not build a second component loop or deep inheritance trees. Heavy generics and class hierarchies make mixed collections painful and slow your ship date.

Role Holds Serialized? Best for
Definition (asset) Immutable metadata, icons No Author-time design
ItemInstance (plain class) Durability, ammo, modifiers Yes Saves, tests, runtime state
MonoBehaviour view Colliders, VFX, animations No (transient) Visuals and player feedback

Mobile performance considerations for an inventory and modular inventory UI</h2>

Mobile devices demand that your inventory code minimize allocations and avoid costly layout passes. Focus on three bottlenecks: memory, CPU/battery, and draw work. Each has practical fixes you can apply in the editor and at runtime.

Memory: icons and asset loading

Keep icons as Sprite references instead of creating Texture2D copies at runtime. Use SpriteAtlas to pack assets and, when appropriate, Addressables to load icons on demand.

CPU and battery

Remove Update polling from view components. The only per-frame activity during drag should be moving the ghost icon. Drive slot updates from events so taps and moves do not trigger scans or allocations.

Draw calls and layout

Minimize nested canvases and avoid per-slot canvases. Batch elements and prevent layout rebuild loops by changing visuals only when a slot event fires.

Screen size and virtualization

Compute grid cell size from safe-area bounds and scale touch targets across aspect ratios. For large collections, virtualize rows so you only instantiate visible cells.

Problem Likely cause Change to make Expected result
Stutter while dragging Layout rebuilds each frame Use event-driven updates; avoid Update polling Smooth drag, stable 60fps
Memory spikes Runtime Texture2D/Sprite.Create loops Use SpriteAtlas / Addressables Lower GC, less RAM pressure
High draw calls Nesting canvases and many independent canvases Batch UI, reduce canvases Fewer draw calls, better battery life
Poor touch scaling Fixed grid for one aspect ratio Compute cell size from safe area; virtualize Consistent targets across screens

Testing and debugging the hard parts (drag edge cases and inventory integrity)

Building safety checks around every move saves you debugging time later. Test flows that cancel or never commit so your model never becomes corrupt. Keep tests focused on touch quirks and interrupted gestures.

Edge cases to exercise

Drop outside the UI, close the panel mid-drag, or get interrupted by an OS event. The model must stay unchanged unless a valid drop completes.

Dragging from an empty slot should do nothing. Prevent ghost creation and avoid any model calls in that path.

Rapid taps and repeated drags are common on phones. Guard the view with a simple dragging flag so begin/end pairs cannot double-fire moves.

Invariant checks and practical tests

Add dev-only asserts: quantities never go negative, stacks never exceed max, and total item counts match expected totals after operations.

Create a small harness that runs random moves (for example, 1,000 iterations) and asserts invariants after each step. This finds merge and swap bugs fast.

Test What to watch Recovery
Drop outside No change to model Cancel drag, restore visuals
Empty source No ghost created Ignore input
Rapid taps Duplicate moves Use dragging state lock
Random stress Merge/swap invariants Automated assertions

Common beginner mistakes pulled from real threads (and how you avoid them)

Catching a few recurring errors early saves hours of debugging and keeps your codebase sane. Below are blunt fixes tied to real symptoms you’ll see in the wild.

Storing scene objects in runtime data

Mistake: you keep game objects in your slot list. Symptom: “Missing (Item)” after a destroy or scene unload. Fix: store an ItemDefinition ID, amount, and small instance state. Spawn visuals separately when needed.

Rebuilding one prefab per slot constantly

Mistake: you reinstantiate slot prefabs on every change. Symptom: stutter and battery drain on phones. Fix: instantiate once, then update only changed slots via events.

Mixing pickup physics with equipped visuals

Mistake: a single game object is both pickup and equipped mesh. Symptom: toggling colliders and weird physics. Fix: keep the pickup actor transient and use a separate equipped prefab on equip.

Copying big-game features without scoping

Mistake: you clone complex designs before writing requirements. Symptom: scope creep and broken edge cases. Fix: write constraints (for example, no split stacks) and ship the smallest working manager first.

Mistake Symptom Quick Fix
Scene objects in data Missing references Store IDs + spawn visuals
Full grid rebuilds Stutter / GC spikes Event-driven slot updates
Pickup = equipped Collider toggles Separate prefabs
No requirements Feature bloat Define constraints, iterate

Reality check: your inventory manager should coordinate model, save, and view events. Keep physics and visuals out of scripts that own data. That one rule prevents most post-release questions and problems.

Conclusion</h2>

This post wraps up a pragmatic, testable approach to handling item flows and touch input on phones.

You built a clear architecture: ScriptableObject definitions, a pure C# model as the single source of truth, event-driven view binding, uGUI drag/drop handlers, typed equipment slots, and pickup/drop that never stores scene objects. Keep your code as the authority; let visuals only render state and send commands.

The mobile wins are real: far fewer layout rebuilds, lower allocations, predictable touch handling, and better battery life because you avoid polling. Next steps that won’t force a rewrite include stack splitting, item filtering tabs, scroll virtualization, and richer feedback like tooltips and haptics.

Check the EventSystem drag interfaces and ScriptableObjects docs on Unity for API details as versions change.

Leave a Reply

Your email address will not be published. Required fields are marked *