Building a Modular Item System in Unity
A modular item system in Unity uses ScriptableObjects for data, composition for behaviour, and a flat lookup at runtime. Here is how to structure it so items scale without rewrites.

A modular item system separates item data from item behaviour, stores definitions as ScriptableObjects, and composes stats and effects at runtime rather than inheriting them from a class tree. This approach scales from ten items to a thousand without structural changes, and it keeps designers out of code files when they need to add or tune content.
Why modularity matters for items
The common first approach is an enum or a class hierarchy: Sword extends Weapon extends Item. It works for a prototype and collapses around fifty items.
The collapse happens because every new item type requires a code change, every stat variation needs a new subclass, and the hierarchy becomes a decision tree nobody wants to touch.
A modular system avoids this by treating items as data containers with pluggable behaviour. Adding a new item is a configuration task, not a programming task.
The data layer: ScriptableObjects
Each item definition is a ScriptableObject asset. The base fields cover identity and presentation.
- id (string): a unique key for lookup and save data.
- displayName (string): what the player sees.
- icon (Sprite): the inventory image.
- description (string): tooltip text.
- category (enum): weapon, armor, consumable, material, and so on.
- stackable (bool): whether multiples occupy one slot.
- maxStack (int): if stackable, the ceiling.
These fields never change at runtime. They are reference data, authored in the editor.
ScriptableObjects are not save data. They define what an item is. What the player owns is a separate runtime list referencing these definitions by id.
Stat composition instead of inheritance
Stats attach to items through a list of stat modifier entries rather than through typed fields on a subclass.
Each modifier entry has three fields.
- statType (enum): health, damage, speed, armor, luck, and whatever else your game tracks.
- value (float): the magnitude.
- operation (enum): flat add, percentage add, or multiply.
An item carries a list of these. A sword with damage and speed is two entries. A ring with health and luck is two entries. Neither needs its own class.
At equip time, the system iterates the list and applies each modifier to the player's stat collection. At unequip, it reverses them. The order of operations matters: apply flat adds first, then percentage adds, then multiplies. This is consistent and predictable.
| Operation | When applied | Example |
|---|---|---|
| Flat add | First | +10 damage |
| Percentage add | Second | +15% damage |
| Multiply | Third | 1.2x damage |
This three-step pipeline handles nearly every stat interaction a mobile game needs.
Behaviour through components
Some items do things beyond modifying stats: heal on use, apply a buff over time, unlock an ability.
Handle these with effect components rather than subclass methods.
Define an abstract base, something like ItemEffect with an Apply method. Then create concrete implementations: HealEffect, BuffEffect, UnlockEffect. Each is a ScriptableObject that can be assigned to any item definition through a list field.
An item's effects list can hold zero entries (a material with no active use), one entry (a health potion), or several (a scroll that heals and buffs). The item definition does not need to know what the effects do internally.
This is the same composition principle as the stat modifiers, applied to behaviour. Adding a new effect type means one new ScriptableObject class and no changes to the item system itself.
Runtime lookup and the item registry
At runtime, something needs to resolve an item id into its ScriptableObject definition. A flat registry handles this.
The registry loads all item ScriptableObjects on startup (or uses Addressables for lazy loading on larger projects). It exposes a single method: give it an id, get back the definition.
Two rules keep this clean.
- Save data stores ids, never references. The player's inventory is a list of ids and quantities. The ScriptableObject is looked up when it is needed for display or logic.
- The registry is read-only at runtime. Item definitions do not change during play. If you need items with variable properties, those properties live on the inventory entry, not on the definition.
For projects with hundreds of items, lazy loading through Addressables prevents the registry from holding everything in memory at once. The pattern from Unity Addressables vs Asset Bundles applies directly.
Inventory as a runtime layer
The inventory is separate from the item definitions. It is a list of entries, each holding an item id, a quantity, and any instance-specific data like durability or enchantment level.
Keep the inventory logic in its own class with clear boundaries.
- Add checks capacity and stacking rules.
- Remove validates quantity and fires an event.
- Query returns whether the player has a specific item and how many.
- Equip and unequip apply and reverse stat modifiers.
Fire events on every change. The UI subscribes to those events and updates. The inventory never talks to the UI directly, and the UI never modifies the inventory without going through the public methods.
This separation is what makes the save system straightforward. The inventory serialises as a list of id-quantity pairs plus instance data. Loading reconstructs the list and fires events so the UI rebuilds. The serialisation approach from Unity Save System Guide fits this cleanly.
Common mistakes
Five patterns that create problems as the system grows.
- Storing references instead of ids in save data. ScriptableObject references break across builds. Always save the string id.
- Putting display logic in the item definition. The definition says what the item is. How it looks in the inventory, the shop, or the tooltip is a UI concern.
- Using enums for item types with subclass behaviour. Enums are fine for categories. When they start controlling logic through switch statements, you have a class hierarchy disguised as data.
- Skipping events on inventory changes. Without events, every system that cares about inventory state has to poll. Polling scales badly and produces stale UI.
- Making stat operations order-dependent on the item. The stat pipeline order should be global and consistent, not per-item.
Scaling past a hundred items
Below a hundred items, brute-force search works and you rarely notice. Past that, two things help.
Index by category. The registry holds a dictionary keyed by category, so filtering for "all weapons" or "all consumables" does not iterate the full set.
Use Addressables for loading. Group items by category or by area, and load groups on demand. A menu that shows armor does not need to load all weapons.
Both are optional refinements, not prerequisites. Build the flat system first and optimise when the profiler says so.
What we would do
Start with the ScriptableObject base, the stat modifier list, and the flat registry. Skip effects until a specific item needs one, because the architecture supports adding them later without changing what exists.
Build the inventory as a separate runtime list with events from day one. Retroactively adding events to an inventory that was not designed for them is painful and error-prone.
Test the full loop early: create an item definition, add it to the registry, give it to the player, equip it, verify the stat changes, save, reload, and verify again. That loop exercised once catches the structural mistakes that are expensive to fix later.
The short version
- Separate item data (ScriptableObjects) from runtime state (inventory entries).
- Compose stats with modifier lists: flat add, percentage add, then multiply.
- Attach behaviour through effect components, not through subclasses.
- A flat registry resolves ids to definitions at runtime.
- Save ids and quantities, never ScriptableObject references.
- Fire events on every inventory change so the UI stays in sync.
Start with the data layer and the registry, then add effects when you need them. If you want a modular item system built for your game, reach out.
Related reading: Unity Save System Guide, Unity Addressables vs Asset Bundles, and Shader Basics for Devs.
Got a game idea? We build it.
You bring the concept. We design, build, test and launch it, and you own 100% of the finished game.
Share Your Game Idea →