,

ScriptableObject vs MonoBehaviour: Where Should Data Live?

Software code on a laptop screen representing Unity game data architecture

The short answer: use a ScriptableObject for shared, author-edited definitions; use a MonoBehaviour for behaviour attached to a GameObject; and use a plain C# class or struct for mutable runtime state and save data. The ScriptableObject vs MonoBehaviour decision becomes much easier once you separate what an object is from what is currently happening to it.

That separation matters in every Unity project, but it is especially valuable when the same gameplay data must support mobile and VR. A level definition, enemy type, item description, or puzzle theme can remain identical while touch controls, XR interaction, cameras, and user interfaces stay platform-specific.

The three jobs should not share one container

Unity gives these types different roles:

  • ScriptableObject: a project asset that exists independently of a GameObject and can be referenced by scenes, prefabs, and other assets.
  • MonoBehaviour: a component attached to a GameObject, with scene references and lifecycle messages such as Awake, Start, and Update.
  • Plain C# class or struct: an ordinary runtime object that you construct and control yourself, useful for instance state, calculations, network messages, and save-file models.

Unity’s current ScriptableObject documentation describes its main value as a data store. Multiple objects can reference one shared asset instead of carrying duplicate copies of the same values. The MonoBehaviour manual, by contrast, defines the framework for scripts attached to GameObjects and their event hooks.

Use ScriptableObjects for definitions

A definition is data designed by the developer and normally shared by many runtime instances. Good examples include:

  • enemy base health, speed, defence, reward, and prefab references;
  • weapon damage ranges, cooldowns, icons, and audio clips;
  • puzzle board dimensions and visual themes;
  • element names, symbols, atomic numbers, and educational descriptions;
  • difficulty presets; and
  • platform-independent game rules.

These values benefit from being visible in the Inspector, reusable across scenes, and editable without changing code. The CreateAssetMenu attribute makes each definition easy to create as a named .asset file.

ScriptableObjects also make relationships explicit. A prefab can reference one EnemyDefinition; twenty spawned enemies can read the same definition. That is cleaner than repeating identical values on twenty prefab variants.

Use MonoBehaviours for scene behaviour

A MonoBehaviour is appropriate when code needs a Transform, collider, renderer, Animator, camera, XR rig, Unity event function, coroutine, or another scene object. Movement controllers, hit detection, interaction handlers, UI presenters, and spawn managers naturally belong here.

The component should consume a definition rather than become the only place that definition exists. For example, an enemy component can read maximum health from an EnemyDefinition, then create its own current-health value when it spawns. This keeps the reusable design data separate from the living instance.

Use plain C# for changing runtime state

Runtime state answers questions such as: How much health does this enemy have now? How many moves has the player used? Which rewards were collected? What must be written to the save file?

That state should normally not be stored by changing the shared ScriptableObject asset. Every consumer refers to the same asset, so a mutation can create surprising cross-talk during play. More importantly, Unity notes that a built standalone Player reads the saved data contained in ScriptableObject assets; editing an in-memory value is not a player-save system.

Keep definitions effectively read-only during gameplay. Copy the starting values into a runtime object, then serialize the runtime object with the save approach your project uses.

A compact Unity example

using System;
using UnityEngine;

[CreateAssetMenu(
    fileName = "PuzzleTheme",
    menuName = "Blekol/Puzzle Theme")]
public sealed class PuzzleTheme : ScriptableObject
{
    public string displayName;
    [Min(2)] public int boardSize = 4;
    public Color tileColor = Color.white;
    public AudioClip moveSound;
}

[Serializable]
public sealed class PuzzleRunState
{
    public int moves;
    public int score;

    public PuzzleRunState(int startingScore)
    {
        score = startingScore;
    }
}

public sealed class PuzzleBoard : MonoBehaviour
{
    [SerializeField] private PuzzleTheme theme;
    private PuzzleRunState runState;

    private void Awake()
    {
        runState = new PuzzleRunState(startingScore: 100);
        BuildGrid(theme.boardSize, theme.tileColor);
    }

    public void RegisterMove()
    {
        runState.moves++;
    }

    private void BuildGrid(int size, Color color)
    {
        // Create the board presentation for this platform.
    }
}

Here, PuzzleTheme is reusable authoring data, PuzzleBoard controls a scene object, and PuzzleRunState belongs to one play session. A mobile board and a VR board can reference the same theme asset while implementing different presentation and interaction.

Quick decision table

Question Best starting choice
Must it attach to a GameObject? MonoBehaviour
Is it shared, developer-authored configuration? ScriptableObject
Does each spawned object need its own changing copy? Plain C# runtime state
Must it survive app restarts as player progress? Save-data class plus persistent storage
Must several scenes reference the same definition? ScriptableObject
Does it require Update, coroutines, Transform, or collision events? MonoBehaviour

Common ScriptableObject mistakes

  1. Using one asset as a global bag of mutable state. It becomes difficult to know which system changed it and when.
  2. Treating the asset as a save file. Store authored defaults in the asset and player progress in a separate persistence model.
  3. Putting scene objects into reusable definitions. Project assets should not depend on a particular scene instance.
  4. Creating one giant database. Smaller purpose-driven assets are easier to review, test, and reuse.
  5. Editing shared values without validation. Use attributes such as Min, Range, and clear naming conventions to prevent invalid content.

A practical project workflow

  1. List the values that designers should edit outside code.
  2. Mark which values are shared definitions and which change per instance.
  3. Create small ScriptableObject types for the shared definitions.
  4. Reference those assets from focused MonoBehaviour components.
  5. Create plain runtime-state objects when a level or character starts.
  6. Save only the state required to restore player progress.
  7. Test two simultaneous instances to expose accidental shared mutation.

Follow Unity’s serialization rules when choosing fields for the Inspector or persistence. Not every C# type is serialized automatically, and Unity asset serialization is not the same thing as a deliberate versioned save format.

How this supports Blekol’s mobile and VR games

For Cubus on Android and iOS, theme, level, and scoring definitions can stay separate from the touch interface that presents them. A VR title such as Cubus 2: Colors VR can reuse the same architectural principle while adding spatial interaction. Data-heavy experiences such as Periodic Table VR benefit even more from clear content definitions that are independent of scene behaviour.

The pattern is not complicated: assets describe the design, components connect it to the scene, and runtime objects record what changes. That division keeps a Unity project easier to test, easier to expand, and far safer to share across mobile and VR builds.


Featured photo by Chris Ried on Unsplash.

One response to “ScriptableObject vs MonoBehaviour: Where Should Data Live?”

Leave a Reply

You might also like