๐ฆ Lesson 2.3: ScriptableObjects: Data as Assets
A MonoBehaviour is code that lives on a GameObject in a scene. But a lot of what your game needs isn't behavior at all โ it's data: how much damage the shotgun does, how fast the goblin walks, what's in the starter kit. A ScriptableObject lets you store that data as a standalone asset in your Project, editable by designers and shared by many objects. It's the third pillar of decoupled design.
๐ฏ Learning Objectives
By the end of this lesson, you will be able to:
- Explain what a ScriptableObject is and how it differs from a
MonoBehaviour - Create one by subclassing
ScriptableObjectwith[CreateAssetMenu] - Author data assets like
WeaponDataandEnemyStatsin the Project window - Reference an SO from a component and read its values at runtime
- Tell shared data from instanced data and avoid editing shared assets by accident
- List the benefits: designer-editable, no scene bloat, and decoupling
Estimated Time: 45 minutes ยท Prerequisite: Lesson 2.2 (Events & Delegates); comfort with [SerializeField] from Fundamentals
In This Lesson
What Is a ScriptableObject?
You know MonoBehaviour: it must sit on a GameObject, in a scene, and gets Update, colliders, transforms. A ScriptableObject is its data-only sibling. It's a class Unity can serialize into a .asset file that lives in your Project, not in any scene and not on any GameObject. It has no Update, no transform โ it just holds fields.
๐ Definition
A ScriptableObject is a serializable Unity object that stores data as a project asset independent of scenes and GameObjects. You subclass it, mark it with [CreateAssetMenu], and create instances as .asset files you fill in via the Inspector.
Think of the difference this way. Twenty goblins in your scene are twenty GameObjects, each with an Enemy component (behavior). But they all share the same stats โ 30 health, 3 damage, 3.5 move speed. Copying those numbers onto twenty components is wasteful and fragile; change the balance and you edit twenty objects. Put the numbers in one EnemyStats asset and all twenty just point at it.
๐ก Not a replacement for MonoBehaviour. ScriptableObjects don't do things each frame โ they describe things. You'll almost always have both: aMonoBehaviourthat acts, holding a reference to aScriptableObjectthat supplies the numbers.
Creating WeaponData
A ScriptableObject definition is just a class that derives from ScriptableObject instead of MonoBehaviour. The magic ingredient is the [CreateAssetMenu] attribute, which adds an entry to Unity's Assets โธ Create menu so you can spawn assets from it:
using UnityEngine;
[CreateAssetMenu(fileName = "NewWeapon", menuName = "Game/Weapon Data")]
public class WeaponData : ScriptableObject
{
public string weaponName = "Pistol";
public int damage = 10;
public float fireRate = 2f; // shots per second
public int magazineSize = 12;
public GameObject projectilePrefab;
public AudioClip fireSound;
public Sprite icon;
}
Notice the fields are plain public โ ScriptableObjects serialize the same way components do, so [SerializeField] private fields work too. Because it's data, there's no Start or Update; it's a tidy bundle of values, and those values can include asset references like prefabs, clips, and sprites.
โ Pro Tip
Use a grouped menuName like "Game/Weapon Data" or "Enemies/Stats". The slash creates a submenu, so a project with dozens of SO types stays organized under Assets โธ Create โธ Game โธ โฆ instead of flooding the top level.
Data as Project Assets
Once the class compiles, right-click in the Project window โธ Create โธ Game โธ Weapon Data. Unity drops a new .asset file that you name and select; its fields appear in the Inspector, ready to fill in. Make one asset per weapon โ Pistol, Shotgun, Rifle โ each a separate file with its own numbers.
Here's the Project window holding three weapon assets, with the selected Shotgun shown in the Inspector โ exactly what you'll see on screen:
WeaponData ScriptableObject in the Project and Inspector (faithfully recreated). Three weapon assets live in a Data folder โ no GameObjects, no scene โ and the selected Shotgun exposes all its fields for a designer to tune.โ ๏ธ SO edits made in Play mode stick
Unlike component values (which reset when you stop Play), changes to a ScriptableObject asset during Play mode are written to the file and persist after you stop. Handy for tuning; dangerous if you change a shared asset's runtime state without meaning to. We'll come back to this under shared vs. instanced data.
Referencing an SO from Code
A component uses the data by holding a WeaponData reference โ exactly like any other serialized field. Drag the Shotgun asset into the slot and the weapon behaves like a shotgun; drag Rifle in and the same script behaves like a rifle. The behavior lives in code; the numbers live in the asset:
using UnityEngine;
public class Weapon : MonoBehaviour
{
[SerializeField] WeaponData data; // drag an asset here
float nextFireTime;
public void TryFire()
{
if (Time.time < nextFireTime) return;
nextFireTime = Time.time + 1f / data.fireRate;
var proj = Instantiate(data.projectilePrefab,
transform.position, transform.rotation);
proj.GetComponent<Projectile>().damage = data.damage;
AudioSource.PlayClipAtPoint(data.fireSound, transform.position);
}
}
Swapping weapons at runtime is now trivial โ assign a different data asset and everything (damage, rate, sound, projectile) follows. That's the decoupling win: the Weapon code doesn't hardcode a single number.
Why This Matters
ScriptableObjects pay off in three big ways:
- Designer-editable. Balancing lives in
.assetfiles a non-programmer can tune in the Inspector โ no code edits, no recompiling. Change the shotgun's damage over lunch without opening Visual Studio. - No scene bloat. Data lives once in the Project, not copied onto every GameObject or baked into each scene. Twenty goblins reference one stats asset instead of carrying twenty copies of the same numbers, which also keeps scene files small and merge-friendly.
- Decoupling. Just like interfaces and events, SOs cut a dependency: code depends on a data contract (the SO type) rather than hardcoded values. A brand-new weapon is a new asset, not a new class โ often no code at all.
๐ก The whole module clicks together here. Interfaces decouple who calls whom. Events decouple who reacts to what. ScriptableObjects decouple code from data. In the next lesson we combine events and SOs into one pattern โ the ScriptableObject event channel โ that decouples across entire scenes.
โ Great first candidates for an SO
Weapon stats, enemy definitions, item/inventory entries, level configs, audio banks, dialogue lines, ability definitions, and difficulty presets. If you find yourself typing the same numbers onto many objects, or wishing a designer could tweak values without you โ that's an SO.
Hands-on Challenge
๐๏ธ Exercise 1: A weapon locker of assets
Objective: Move weapon numbers out of code and into swappable assets.
- Create the
WeaponDataclass above with[CreateAssetMenu(menuName = "Game/Weapon Data")]. - Via Assets โธ Create โธ Game โธ Weapon Data, make three assets: Pistol, Shotgun, Rifle, each with different numbers (match Figure 1's Shotgun).
- Write a
Weaponcomponent with a serializedWeaponData datafield. - Drag each asset into the field in turn and confirm the same script fires at different rates and damage โ no code changes between weapons.
๐ก Hint: "Create" menu doesn't show my type
Make sure the class compiles (check the Console) and that the file name matches the class name exactly. The [CreateAssetMenu] entry only appears after a clean compile. If you renamed the class, Unity may need a moment to refresh.
โ Success check
You can add a fourth weapon (e.g. SMG) by creating one more asset and dragging it in โ with zero edits to Weapon.cs. The numbers live entirely in the assets.
๐๏ธ Exercise 2: Shared stats, private health
Create an EnemyStats SO (maxHealth, moveSpeed, damage). Make one asset, GoblinStats, and put it on five Enemy GameObjects. Have each Enemy seed a private currentHealth from stats.maxHealth in Awake. Damage one goblin and confirm the other four are unaffected โ proving you're reading shared data but storing runtime state per object. Then bump maxHealth in the asset and confirm all five re-balance on the next Play.
๐ฏ Quick Quiz
Question 1: How does a ScriptableObject differ from a MonoBehaviour?
Question 2: What does [CreateAssetMenu] do?
Question 3: Five enemies reference one shared EnemyStats asset. Where should each enemy's current health live?
Summary
๐ Key Takeaways
- A ScriptableObject stores data as a project
.asset, independent of scenes and GameObjects โ noUpdate, no transform. - Subclass
ScriptableObjectand add[CreateAssetMenu]to spawn assets from Assets โธ Create. - Components reference an SO through a serialized field; swap the asset to swap the data (e.g.
WeaponData). - Shared data (the template) is read from the SO; instanced state (current health, ammo) lives on the
MonoBehaviour, seeded from the SO. - SO edits during Play mode persist โ great for tuning, risky for runtime writes to shared assets.
- Benefits: designer-editable, no scene bloat, and decoupling code from data.
๐ What's Next?
You now have all three decoupling tools: interfaces, events, and ScriptableObjects. In Lesson 2.4: ScriptableObject Event Channels we fuse events and SOs into a single elegant pattern โ a GameEvent asset that any system can raise and any GameEventListener can react to, letting entirely separate scenes talk without a single direct reference.
๐ฆ Your data is free of your scenes
Numbers live in assets a designer can tune, shared by everything that needs them. Code describes behavior; ScriptableObjects describe the world it acts on.