Lesson 7: Data Model with ScriptableObjects

Hard-coded stats on prefabs work until you need ten enemy variants and your designer asks for a balance pass at midnight. This lesson moves static facts (damage, move speed, display name, icon, point value) into ScriptableObject assets so one file change updates every instance that references it.

Lesson Objective

By the end of this lesson you will have:

  1. A small set of definition assets (for example EnemyDefinition, PickupDefinition) living under Assets/_Project/Data/
  2. Prefab references to those assets instead of duplicated inspector numbers
  3. A clear rule that runtime state (current HP, ammo count) still lives on components or plain C# objects, not mutated blindly on the asset in play mode

Why This Matters

ScriptableObjects are Unity’s lightweight way to store design data. They serialize with the project, diff in version control, and can be shared by many prefabs. Pair them with the event-driven UI you built in Lesson 6 and enemies from Lesson 5, and you get a vertical slice that behaves like a real production repo.

For a deeper standalone reference, read ScriptableObjects and Data-Driven Design in Unity and the Unity guide chapter on ScriptableObjects.

Step-by-Step

Step 1: Create a definitions folder

  1. Under your course root (for example Assets/_Project/), add Data/Definitions/.
  2. Optional subfolders: Enemies/, Pickups/, Waves/ so imports stay navigable.

Step 2: Author an EnemyDefinition

Create EnemyDefinition.cs:

using UnityEngine;

[CreateAssetMenu(fileName = "EnemyDefinition", menuName = "Game/Enemy Definition")]
public class EnemyDefinition : ScriptableObject
{
    public string displayName;
    public int maxHealth = 10;
    public float moveSpeed = 3.5f;
    public int scoreValue = 100;
    public Sprite portrait; // optional for UI later
}
  1. Right-click in Project → Create → Game → Enemy Definition.
  2. Name assets clearly: Enemy_Grunt, Enemy_Rusher.

Pro tip: Keep fields serializable and simple. Complex curves belong in separate assets or animation, not fifty toggles on one SO.

Step 3: Bind definitions to existing enemy prefabs

On your enemy root (Lesson 5):

  1. Add a field public EnemyDefinition definition; on your motor or brain component.
  2. Assign the asset in the Inspector.
  3. In Awake or Start, initialize runtime HP from definition.maxHealth and speed from definition.moveSpeed.

Common mistake: Writing balance numbers both on the SO and on leftover serialized fields on the prefab. Delete the duplicates once the SO drives the values.

Step 4: Pickup or loot definitions (thin slice)

Add PickupDefinition.cs with displayName, healAmount or score, and optional icon.

Your pickup trigger (or inventory stub from your blog reading) calls Apply(PickupDefinition def) so VFX and HUD text can use def.displayName consistently.

Step 5: Read-only discipline in play mode

During Play, changing an SO field in the Inspector can persist when you stop, depending on editor workflow. For learning:

  • Treat SOs as design-time unless you intentionally use a runtime clone pattern.
  • If designers need hot reload, duplicate assets per experiment or use a play-mode-safe copy layer in Lesson 8.

Step 6: Hook HUD labels (optional polish)

Extend your HudBinder from Lesson 6 to show the player’s definition name or equipped item string pulled from a PlayerDefinition SO. Even a single TMP line proves the pipeline works.

Mini Challenge

Create three PickupDefinition assets with different names and point values. Spawn them from the same prefab with only the reference swapped. Confirm your HUD or debug log prints the correct displayName on pickup.

Pro Tips

  • Use GUID stability: renaming assets is fine; moving between folders is fine. Avoid duplicating SOs with copy-paste confusion—duplicate via Create menu when possible.
  • Add a one-line Tooltip attribute on important fields so future-you remembers intent.
  • If you use Addressables later, SOs can still be entries; keep IDs stable now.

Common Mistakes

  • Putting transform or scene references on SOs that must be shared across scenes (use prefab references or runtime lookup instead).
  • Letting UI write back into definitions when the player buys an upgrade (use runtime stats or a separate PlayerProgress object).
  • Giant “god” ScriptableObjects with every system flag—split by domain.

Troubleshooting

Inspector shows missing script after rename

Reattach the script or reimport; Unity sometimes caches old type names after aggressive namespace moves.

Values do not update on existing prefab instances

You may have overridden the field on the prefab instance. Apply or revert overrides so the SO remains authoritative.

Build differs from Editor

Ensure SOs live under Assets/ and are included in the build; they are not Resources unless you explicitly load them that way.

Recap

You moved static gameplay facts into ScriptableObject definitions, referenced them from prefabs, and separated design data from runtime state. Your slice is now ready for save/load and progression in Lesson 8.

Next Lesson Teaser

Lesson 8 wires save, load, and progression so play sessions survive restarts and you can iterate on difficulty without fear of losing tester runs.

FAQ

Should I use JSON or SOs?
For Unity-first teams, SOs win on editor UX. JSON helps for modding or server-driven configs. This course stays SO-first.

What about Odin or custom editors?
Optional. Ship gameplay first; invest in inspector polish when definitions become painful to browse.

Do SOs replace prefab variants?
No. Variants handle hierarchy and components; SOs handle tunable data shared across variants.

Related Links

Treat definitions like a mini content database—small, boring files that save your schedule when scope creeps.