Skip to content
CodeAndBuild LogoCodeAndBuild

Game Design

Store Game Data in Unity ScriptableObjects

Replace scattered prefab numbers with ScriptableObject items, weapons, and encounters a designer can edit.

CodeAndBuild Team8 min read
  • Unity
  • C#
  • ScriptableObject
  • Game Data
On this page
  1. Define an item as an asset
  2. Point gameplay at the asset
  3. Keep a catalog for lookups
  4. Play mode edits stick, and that is dangerous
  5. Resolve ids when the save loads

A sword's damage does not belong on a prefab instance in a scene. The next time someone duplicates the pickup, the damage drifts, and a balance change means opening twenty scenes. A ScriptableObject is an asset that holds that data once. Pickups, shops, and spawners point at the asset. You edit the sword in one place, and every sword in the game reads the new number.

ScriptableObjects are data, not a save file. They live in the project and ship with the build. They are a poor place for the player's current hit points, because those change per session and you do not want a play test to overwrite the designer's default. Put design values in the asset. Put runtime values in a plain class that copies what it needs when the scene starts.

Define an item as an asset

Create a class that extends ScriptableObject. Add a Create Asset menu entry so a designer can make one from the project window. Use a stable id string for saves and analytics. Display names can change. Ids should not, once a save file might contain them.

ItemDefinition.cscsharp
using UnityEngine;

[CreateAssetMenu(menuName = "Game/Item")]
public class ItemDefinition : ScriptableObject
{
    public string id;
    public string displayName;
    public int cost;
    public Sprite icon;

    private void OnValidate()
    {
        if (string.IsNullOrWhiteSpace(id))
        {
            id = name.ToLowerInvariant().Replace(" ", "-");
        }
    }
}

OnValidate is a nudge, not a database. If you rename the asset, decide whether the id follows. For anything already shipped, keep the id and change the display name. A shop that keys off the asset's file name will break when an artist tidies a folder.

Point gameplay at the asset

A pickup in the scene holds a reference to the ItemDefinition and a count. It does not copy cost into a serialized int on the prefab. When the UI draws a price, it reads definition.cost. When the inventory adds the item, it stores the id and the count, and looks the definition up from a catalog.

Pickup.cscsharp
using UnityEngine;

public class Pickup : MonoBehaviour
{
    public ItemDefinition item;
    public int count = 1;

    public void Collect(Inventory inventory)
    {
        if (item == null || count < 1) return;
        inventory.Add(item.id, count);
    }
}

Keep a catalog for lookups

Scenes should not be the only place an item exists. A catalog ScriptableObject lists every item. Save games store ids. When you load, you resolve ids through the catalog. If an id is missing, drop that stack and log it. Do not crash a save because a seasonal item was removed. Tell the designer.

  • One catalog asset, assigned in a bootstrap scene or a Resources folder you meant to use.
  • Ids are unique. OnValidate can warn on duplicates if you scan the list.
  • Icons and prefabs live on the definition so the shop and the world spawn the same art.
  • Runtime modifiers, such as a buff, wrap the definition. They do not write back into the asset during play mode.

Play mode edits stick, and that is dangerous

Unlike a MonoBehaviour on a scene object, changes to a ScriptableObject during play mode are edits to the asset. They survive exiting play mode. A debug button that does item.cost = 1 will ship a shop where everything costs 1 if you save the project afterward. If you need to mutate, copy the values into a runtime object. Treat the asset as read-only at runtime.

Designers can then duplicate an item, change the cost, and drop it on a pickup without opening code. Programmers review the class, not every number. That split is the reason to use the feature. A ScriptableObject that only a programmer can edit, full of hidden logic, is a MonoBehaviour with extra steps.

Resolve ids when the save loads

Build a dictionary from the catalog once at startup, keyed by id. Loading a save should be a loop over stacks that calls the dictionary and skips ids you no longer ship. Do not search the asset list with a linear scan on every inventory draw. The shop, the pickup, and the save must agree on the same dictionary, or you will show a price for an item the inventory cannot find. When a designer duplicates an item, OnValidate should complain about a repeated id before that asset is placed in a scene. A duplicate id is two different swords that overwrite each other in the dictionary, and the bug appears as the wrong icon, not as an exception. Log the id. Do not log the player's whole inventory on every frame.

Prefabs still exist. The pickup prefab is the collision, the mesh, and the reference to the item asset. The numbers a designer balances live on the asset. If a programmer is typing cost into the prefab because it was faster this morning, move the field and delete the copy. Two sources for the same number is the bug this pattern is meant to end. Review the class in code review, and review the assets as content. They change for different reasons and they should not be stuck in the same diff unless the id scheme itself changed.

More guides