Skip to main content

Building a Reliable Inventory System in Unity: Data Models, Save/Load, and Testing

Updated 9 min readMarkdown

An inventory looks straightforward until a player drags an item into a full chest, loads a save after a balance update, or closes a menu halfway through a transfer.

These situations expose the real problem: an inventory is a state management system with a visual interface. Drawing slots is relatively easy. Keeping quantities, ownership, and persistence consistent requires deliberate design.

This article develops an architecture for a slot-based, single-player inventory in Unity. The examples assume stackable items and fixed container capacity. Equipment with unique properties can build on the same principles, but requires additional instance data.

Start with Rules You Can Test

Before building the interface, write down what a valid inventory means.

For this example:

  • Each slot is empty or contains one item type.
  • Occupied slots contain a positive quantity.
  • Quantities never exceed the item’s stack limit.
  • Failed operations leave the inventory unchanged.
  • Transfers preserve the total quantity across both containers.

The last two rules deserve particular attention.

Suppose a player moves eight potions into a chest that has room for three. Should the operation move three or fail completely?

Both behaviors are reasonable. An ambiguous API is the problem.

Use separate operations, such as TryTransferExact and TransferUpTo, so callers can distinguish them. For a partial transfer, return the quantity actually moved and a reason if capacity prevented the full request.

This small decision affects drag-and-drop behavior, crafting, loot collection, and persistence.

Separate Item Definitions from Player State

A potion’s description and the number of potions a player owns have different lifecycles. Keeping them separate prevents shared data from becoming accidental player state.

Item definitions describe a type

An item definition holds information such as:

Field Purpose
Stable ID Identifies the item across sessions
Display name Provides player-facing text
Icon Supports presentation
Maximum stack size Defines a stacking constraint

ScriptableObjects suit authoring shared definitions because they exist independently of scene GameObjects and can hold data referenced by multiple objects. Unity documents this shared-data role explicitly.

Treat these definitions as read-only during gameplay. Changing a shared definition’s quantity would mix configuration with ownership.

Use a stable identifier such as core:potion.small. Avoid using localized names, array positions, or runtime instance identifiers as persistent identity.

Validate the item catalog before shipping: IDs must be unique, references must resolve, and stack limits must be positive.

Inventory state describes ownership

A runtime slot needs an item ID and a quantity. A simple save representation might look like this:

using System;

[Serializable]
public sealed class SlotSave
{
    public int slotIndex;
    public string itemId;
    public int quantity;
}

[Serializable]
public sealed class InventorySave
{
    public int schemaVersion;
    public int capacity;
    public SlotSave[] occupiedSlots;
}

These classes are data transfer objects. They describe stored data; they do not enforce gameplay rules.

A runtime inventory model should own validation and mutation. Expose snapshots or read-only views instead of returning mutable slot objects that any component can edit.

For equipment with durability or randomized attributes, add a separate item-instance identifier. Two swords can share a definition while representing distinct possessions.

Make Mutations Explicit and Atomic

The most damaging inventory bugs often occur between two individually reasonable steps.

Consider this sequence:

  1. Remove items from the player.
  2. Add them to the chest.
  3. Discover the chest cannot accept them.

The transfer has already broken its contract.

Plan first, then commit

For an exact transfer, calculate the complete result before changing either container:

  1. Validate the requested quantity and source slot.
  2. Find compatible destination stacks.
  3. Calculate remaining capacity in those stacks and empty slots.
  4. Build the proposed source and destination states.
  5. Commit both states if the full request fits.
  6. Publish a change notification after the commit.

Here, “atomic” means the model exposes either the old state or the completed result. It does not automatically imply thread safety or crash durability.

A hypothetical example makes the boundary clear.

The source contains eight potions. The destination has one stack containing seven, a stack limit of ten, and no empty slots.

Operation Result
Transfer exactly eight Fails; both containers remain unchanged
Transfer up to eight Moves three; five remain in the source

For small inventories, calculating changes against copied slot data is a reasonable starting point. If profiling later identifies copying as expensive, replace it with a compact mutation plan while preserving the same behavior.

Prevent observers from changing unfinished operations

Events should describe completed changes.

If a notification fires after removing an item but before adding it elsewhere, a subscriber might save an incomplete state or start another mutation.

Keep commits synchronous on the owning gameplay thread. Dispatch notifications afterward, and queue commands raised during notification handling until that dispatch finishes.

Crafting follows the same principle: validate ingredients and output capacity together before consuming anything.

Keep the Interface Outside the Rules

A slot view should display state and report player intent. It should not decide whether a transfer is legal.

During a drag operation, retain a command description containing the source container, source slot, requested quantity, and destination.

The drag preview is temporary presentation. The authoritative inventory stays unchanged until the drop command succeeds.

This avoids needing to restore items when a player releases the pointer outside the inventory window.

It also handles stale interfaces. A crafting action might consume an item while its icon is being dragged. Validate the source again when processing the drop; do not assume the state still matches the drag’s starting point.

After a successful command, update affected views from the committed result. A failed command can display a specific explanation such as “Destination full” without inventing a second version of inventory logic inside the UI.

Treat Save/Load as a Validation Boundary

Saving should capture a consistent model snapshot. Loading should construct a candidate state and validate it before replacing the active inventory.

Serialize plain data

Unity’s JsonUtility supports structured JSON through serializable classes and structs. It follows Unity serialization rules, including field serialization, and does not directly support dictionaries. The array-based save representation above fits that approach.

Store stable item IDs and quantities. Resolve icons and definitions through the catalog after loading.

Successful JSON parsing does not establish that the inventory is valid. Check:

  • Supported schema version.
  • Capacity within application-defined limits.
  • Unique, valid slot indices.
  • Known item IDs.
  • Positive quantities within permitted limits.

Bound file size and collection lengths as well, especially when importing external saves.

Plan for changing definitions

Imagine reducing a potion’s stack limit from twenty to ten. An older save containing sixteen potions now needs a migration policy.

One option is to split the stack across available slots. If capacity is insufficient, place the remainder in a defined recovery container or preserve the original save and report the issue.

Silently clamping sixteen to ten destroys player progress.

Unknown IDs need an equally explicit policy. Preserve unresolved records for recovery, migrate renamed IDs through a mapping, or reject the candidate save with a useful diagnostic.

Protect the previous save

Avoid overwriting the only valid save directly.

A common approach writes a temporary file, closes it successfully, then uses a platform-supported replacement strategy while retaining a backup. Replacement and durability guarantees vary by platform and filesystem, so verify behavior on target devices.

If saving asynchronously, capture the snapshot first. Serialize that snapshot rather than reading a collection while gameplay continues to mutate it.

Test Conservation and Failure Behavior

A plain C# inventory model makes the important rules testable without constructing a scene.

Start with concrete cases:

Scenario Expected behavior
Add to a compatible stack Quantity increases within its limit
Exact transfer exceeds capacity Neither container changes
Transfer succeeds Combined quantity remains constant
Split has no destination space Original stack remains unchanged
Save contains duplicate slot indices Candidate state is rejected
Load encounters an unknown item Explicit recovery policy runs

Then test sequences: add, split, transfer, remove, save, and load.

After every operation, assert the invariants. For transfers, compare total quantities across both containers. For failed commands, compare the complete before-and-after states.

These checks detect bugs that a successful UI animation can hide.

Keep a smaller set of Play Mode tests for integration behavior: cancelled drags, menu reopening, scene transitions, and event subscriptions.

Use Game Research to Generate Technical Questions

Studying other games can reveal useful interaction questions: Do pickups partially fill an inventory? Does sorting preserve hotbar assignments? How does crafting communicate missing output space?

Game listings and community coverage on Play Mod can provide starting points for identifying games to investigate, while implementation conclusions should come from direct observation or developer documentation. modplaygame.com

When investigating mods—community-created additions or alterations—check compatibility, download provenance, and the game’s terms. Availability does not establish that a file is safe, authorized, or compatible.

Translate observations into requirements you can test, rather than assumptions about another game’s internal code.

Conclusion

A reliable inventory begins with explicit rules: who owns state, what operations may change it, and what failure means.

Separate item definitions from ownership. Validate complete transfers before committing them. Keep UI interactions dependent on the model, and treat loaded saves as candidate data that must pass validation.

The result is an inventory whose behavior can be explained and tested—even when the player’s next action is one you did not anticipate.

Tags: Unity, CSharp, GameDevelopment, SoftwareArchitecture, Testing

About the author

  • Play Mod là kho tải game và ứng dụng MOD APK miễn phí dành cho Android, liên tục cập nhật phiên bản mới, tốc độ tải nhanh, dễ cài đặt và an toàn cho người dùng.

No comments yet. Be the first to comment.