Skip to main content
tutorial

Unity ScriptableObjects: Game Data Without the Mess (2025 Beginner Guide)

Angry Shark Studio
10 min read
Unity ScriptableObjects Data Management Beginner Tutorial Best Practices

Unity ScriptableObjects: Game Data Without the Mess (2025 Guide)

Difficulty Level: Beginner

What You’ll Learn

  • How ScriptableObjects solve data management problems
  • Creating your first ScriptableObject in 3 steps
  • Inspector integration and asset management
  • Performance benefits over MonoBehaviour data storage
  • Common patterns: Game settings, item databases, configuration data
  • When NOT to use ScriptableObjects (important limitations)

Quick Answer: Replace hardcoded values and messy arrays with ScriptableObjects. Create clean, reusable data containers that live in your project as assets, not attached to GameObjects. They’re perfect for game settings, item databases, and configuration data that designers need to modify.

Unity projects often start with hardcoded values scattered across MonoBehaviour scripts. As projects grow, this approach creates maintenance nightmares: finding magic numbers, updating duplicate data across multiple scripts, and giving designers access to modify game balance without touching code.

ScriptableObjects fix this by separating data from logic. Instead of cramming arrays and settings into MonoBehaviours, you create data containers that become project assets.

ScriptableObjects work especially well for complex game architectures and when working with teams where designers need to modify game data without opening code files.

The Problem: Data Management Chaos

Most Unity developers start like this - stuffing data right into MonoBehaviours:

// BAD EXAMPLE - Don't do this! Public fields and hardcoded arrays
public class EnemySpawner : MonoBehaviour 
{
    [Header("Wave 1 Enemies")]
    public GameObject[] wave1Enemies = new GameObject[5]; // Public fields expose internals
    public float[] wave1SpawnTimes = {2f, 4f, 6f, 8f, 10f};
    public int[] wave1EnemyCounts = {1, 2, 1, 3, 2};
    
    [Header("Wave 2 Enemies")]
    public GameObject[] wave2Enemies = new GameObject[3];
    public float[] wave2SpawnTimes = {1f, 3f, 5f};
    public int[] wave2EnemyCounts = {2, 4, 3};
    
    // ... more arrays for 10+ waves
    
    private void SpawnWave(int waveNumber) 
    {
        // Complex array indexing logic that breaks easily
        switch(waveNumber) 
        {
            case 1:
                // Use wave1 arrays
                break;
            case 2:
                // Use wave2 arrays  
                break;
            // ... endless switch cases
        }
    }
}

This pattern creates several problems:

  • Hard to maintain: Arrays get out of sync, magic numbers everywhere
  • Error-prone: Easy to mismatch array indices, causing runtime crashes
  • Designer unfriendly: Non-programmers can’t easily modify game balance
  • Memory waste: Data duplicated across multiple MonoBehaviour instances
  • Testing difficulties: Can’t test data logic without GameObjects

It gets worse with each new wave, enemy type, or setting. What starts simple turns into a mess you can’t maintain.

Memory usage comparison showing ScriptableObjects using 50KB vs MonoBehaviour arrays using 250KB for multiple enemies

What Are ScriptableObjects?

ScriptableObjects are Unity’s solution for clean data management. They’re basically data-only classes that become project assets - like textures or audio clips, but for your custom game data.

Here’s what you get:

  • Asset-based: Live in your project, not attached to GameObjects
  • Inspector integration: Full Unity Inspector support with custom editors
  • Memory efficient: Shared references instead of duplicated data
  • Designer accessible: Non-programmers can modify values easily
  • Version control friendly: Clean diffs when data changes

Unlike MonoBehaviours, ScriptableObjects don’t inherit from MonoBehaviour and can’t be attached to GameObjects. They’re pure data containers built for this exact job.

Diagram showing Unity ScriptableObject data flow from asset file through MonoBehaviour script to GameObject in scene

Creating Your First ScriptableObject: Step-by-Step (Unity 2022.3 LTS)

Step 1: Create the ScriptableObject Class

Unity Editor context menu showing Create → C# Script option for ScriptableObject creation

using UnityEngine;

[CreateAssetMenu(fileName = "WaveData", menuName = "Game/Wave Configuration")]
public class WaveData : ScriptableObject
{
    [Header("Basic Settings")]
    [SerializeField] private string waveName = "Wave 1";
    [SerializeField] private float timeBetweenSpawns = 2f;
    
    [Header("Enemy Configuration")]  
    [SerializeField] private EnemySpawnInfo[] enemies;
    
    public string WaveName => waveName;
    public float TimeBetweenSpawns => timeBetweenSpawns;
    public EnemySpawnInfo[] Enemies => enemies;
}

[System.Serializable]
public struct EnemySpawnInfo
{
    [SerializeField] private GameObject enemyPrefab;
    [SerializeField] private int spawnCount;
    [SerializeField] private float spawnDelay;
    
    public GameObject EnemyPrefab => enemyPrefab;
    public int SpawnCount => spawnCount;
    public float SpawnDelay => spawnDelay;
}

The [CreateAssetMenu] attribute is important - it adds your ScriptableObject to Unity’s Create menu so designers can easily create new configurations.

Unity Editor context menu showing Create → Game → Wave Configuration option for creating ScriptableObject assets

Step 2: Create Assets in Unity

  1. Right-click in Project window
  2. Navigate to “Create → Game → Wave Configuration”
  3. Name your asset (e.g., “Wave_1_Easy”)
  4. Configure values in Inspector

You can create as many wave configs as you want. Each one becomes its own asset file.

Unity Inspector showing ScriptableObject with organized fields using Header attributes and example data

Step 3: Reference in MonoBehaviours

public class EnemySpawner : MonoBehaviour 
{
    [Header("Wave Configuration")]
    [SerializeField] private WaveData currentWave;
    
    [Header("Spawn Settings")]
    [SerializeField] private Transform spawnPoint;
    
    private void Start() 
    {
        if (currentWave != null) 
        {
            StartCoroutine(SpawnWaveCoroutine());
        }
    }
    
    private IEnumerator SpawnWaveCoroutine() 
    {
        foreach (var enemyInfo in currentWave.Enemies) 
        {
            yield return new WaitForSeconds(enemyInfo.SpawnDelay);
            
            for (int i = 0; i < enemyInfo.SpawnCount; i++) 
            {
                Instantiate(enemyInfo.EnemyPrefab, spawnPoint.position, spawnPoint.rotation);
                yield return new WaitForSeconds(currentWave.TimeBetweenSpawns);
            }
        }
    }
}

Now your spawning logic is clean. Designers can create new waves without touching any code.

Unity Inspector showing MonoBehaviour script with ScriptableObject asset reference field assigned

Practical Examples: Real Game Scenarios

Game Settings Configuration (Unity 2025)

[CreateAssetMenu(fileName = "GameSettings", menuName = "Game/Settings")]
public class GameSettings : ScriptableObject
{
    [Header("Audio")]
    [Range(0f, 1f)]
    [SerializeField] private float masterVolume = 1f;
    
    [Range(0f, 1f)]
    [SerializeField] private float musicVolume = 0.7f;
    
    [Range(0f, 1f)]
    [SerializeField] private float sfxVolume = 0.8f;
    
    [Header("Gameplay")]
    [SerializeField] private float playerMoveSpeed = 5f;
    [SerializeField] private int playerStartingHealth = 100;
    [SerializeField] private float difficultyMultiplier = 1f;
    
    [Header("UI")]
    [SerializeField] private Color healthBarColor = Color.green;
    [SerializeField] private Color damageFlashColor = Color.red;
    
    public float MasterVolume => masterVolume;
    public float MusicVolume => musicVolume;
    public float SfxVolume => sfxVolume;
    public float PlayerMoveSpeed => playerMoveSpeed;
    public int PlayerStartingHealth => playerStartingHealth;
    public float DifficultyMultiplier => difficultyMultiplier;
    public Color HealthBarColor => healthBarColor;
    public Color DamageFlashColor => damageFlashColor;
}

Unity Item Database for RPG Systems

[CreateAssetMenu(fileName = "Item", menuName = "Game/Item")]
public class ItemData : ScriptableObject
{
    [Header("Basic Info")]
    [SerializeField] private string itemName;
    [SerializeField] private Sprite itemIcon;
    
    [TextArea(3, 5)]
    [SerializeField] private string description;
    
    [Header("Stats")]
    [SerializeField] private ItemType type;
    [SerializeField] private int value;
    [SerializeField] private int stackSize = 1;
    [SerializeField] private bool consumable = false;
    
    [Header("Effects")]
    [SerializeField] private StatModifier[] statModifiers;
    
    public string ItemName => itemName;
    public Sprite ItemIcon => itemIcon;
    public string Description => description;
    public ItemType Type => type;
    public int Value => value;
    public int StackSize => stackSize;
    public bool Consumable => consumable;
    public StatModifier[] StatModifiers => statModifiers;
}

public enum ItemType { Weapon, Armor, Consumable, Quest, Miscellaneous }

[System.Serializable]
public struct StatModifier
{
    [SerializeField] private StatType statType;
    [SerializeField] private float modifierValue;
    [SerializeField] private ModifierType modifierType;
    
    public StatType StatType => statType;
    public float ModifierValue => modifierValue;
    public ModifierType ModifierType => modifierType;
}

Unity Project window showing organized ScriptableObject assets in Data folders with descriptive names

Audio Library Management

[CreateAssetMenu(fileName = "AudioLibrary", menuName = "Game/Audio Library")]
public class AudioLibrary : ScriptableObject
{
    [Header("UI Sounds")]
    [SerializeField] private AudioClip buttonClick;
    [SerializeField] private AudioClip buttonHover;
    [SerializeField] private AudioClip menuOpen;
    [SerializeField] private AudioClip menuClose;
    
    [Header("Combat Sounds")]  
    [SerializeField] private AudioClip[] weaponSwings;
    [SerializeField] private AudioClip[] impactSounds;
    [SerializeField] private AudioClip[] footsteps;
    
    [Header("Ambient")]
    [SerializeField] private AudioClip[] backgroundMusic;
    [SerializeField] private AudioClip[] environmentAmbience;
    
    public AudioClip ButtonClick => buttonClick;
    public AudioClip ButtonHover => buttonHover;
    public AudioClip MenuOpen => menuOpen;
    public AudioClip MenuClose => menuClose;
    public AudioClip[] WeaponSwings => weaponSwings;
    public AudioClip[] ImpactSounds => impactSounds;
    public AudioClip[] Footsteps => footsteps;
    public AudioClip[] BackgroundMusic => backgroundMusic;
    public AudioClip[] EnvironmentAmbience => environmentAmbience;
}

Best Practices and Common Mistakes

Organize with Headers and Tooltips

[CreateAssetMenu(fileName = "PlayerConfig", menuName = "Game/Player Configuration")]
public class PlayerConfig : ScriptableObject
{
    [Header("Movement Settings")]
    [Tooltip("Base movement speed in units per second")]
    [SerializeField] private float moveSpeed = 5f;
    
    [Tooltip("Multiplier applied when sprinting")]
    [Range(1f, 3f)]
    [SerializeField] private float sprintMultiplier = 2f;
    
    [Header("Combat Settings")]
    [Tooltip("Base damage dealt by player attacks")]
    [SerializeField] private int baseDamage = 25;
    
    [Space(10)]
    [Header("Advanced Settings")]  
    [Tooltip("Only modify if you understand the physics implications")]
    [SerializeField] private float gravityScale = 1f;
    
    public float MoveSpeed => moveSpeed;
    public float SprintMultiplier => sprintMultiplier;
    public int BaseDamage => baseDamage;
    public float GravityScale => gravityScale;
}

Don’t Modify at Runtime (Unless Intentional)

// DON'T DO THIS - modifies the asset file
public void TakeDamage(int damage) 
{
    playerData.currentHealth -= damage; // Modifies ScriptableObject permanently!
}

// DO THIS - use runtime variables
public class PlayerHealth : MonoBehaviour
{
    [SerializeField] private PlayerData playerData; // ScriptableObject reference
    private int currentHealth; // Runtime variable
    
    private void Start() 
    {
        currentHealth = playerData.MaxHealth; // Copy from ScriptableObject
    }
    
    public void TakeDamage(int damage) 
    {
        currentHealth -= damage; // Modify runtime copy, not asset
    }
}

Separate Concerns Appropriately

Make small, focused ScriptableObjects instead of huge “god objects”:

// GOOD - Focused responsibility
[CreateAssetMenu(fileName = "WeaponData", menuName = "Game/Weapon")]
public class WeaponData : ScriptableObject 
{
    [SerializeField] private int damage;
    [SerializeField] private float attackSpeed;
    [SerializeField] private float range;
    
    public int Damage => damage;
    public float AttackSpeed => attackSpeed;
    public float Range => range;
}

[CreateAssetMenu(fileName = "PlayerMovement", menuName = "Game/Player Movement")]  
public class MovementData : ScriptableObject 
{
    [SerializeField] private float walkSpeed;
    [SerializeField] private float runSpeed;
    [SerializeField] private float jumpForce;
    
    public float WalkSpeed => walkSpeed;
    public float RunSpeed => runSpeed;
    public float JumpForce => jumpForce;
}

// AVOID - Too many responsibilities
[CreateAssetMenu(fileName = "Everything", menuName = "Game/Everything")]
public class EverythingData : ScriptableObject 
{
    // Weapon stats
    public int damage;
    // Movement stats  
    public float walkSpeed;
    // UI settings
    public Color healthBarColor;
    // Audio settings
    public float masterVolume;
    // ... becomes unmaintainable
}

Performance Benefits Over MonoBehaviour Data

ScriptableObjects give you better performance:

Memory Efficiency: Multiple GameObjects can share the same ScriptableObject. If 100 enemies use the same EnemyData, you’re only storing it once, not 100 times.

// Memory efficient - one EnemyData asset referenced by many enemies
public class Enemy : MonoBehaviour 
{
    [SerializeField] private EnemyData enemyData; // Reference to shared asset
    
    private int currentHealth; // Instance-specific runtime data
    
    private void Start() 
    {
        currentHealth = enemyData.MaxHealth; // Copy from shared data
    }
}

Asset Loading: Unity loads ScriptableObjects efficiently. They load with the scene or when first used, not copied for every GameObject.

Build Size: Shared data means smaller build sizes compared to duplicating arrays across multiple MonoBehaviours.

When NOT to Use ScriptableObjects

ScriptableObjects aren’t always the right choice. Here’s a quick guide to help you decide:

Use ScriptableObjects When:

  • Data is shared across multiple GameObjects (enemy stats, item properties)
  • Designers need access without touching code
  • Data should persist between scenes
  • You want clean architecture with separated concerns
  • Testing different configurations (swappable data sets)

Avoid ScriptableObjects When:

  • Runtime-Only Data: For data that exists only during gameplay (current player position, temporary UI states), use regular classes or MonoBehaviour fields
  • Frequently Changing Data: If data changes every frame, ScriptableObjects add unnecessary overhead. Use direct variable access instead
  • Simple Single Values: For basic settings used by one script, a public field might be simpler than creating a ScriptableObject
  • Performance-Critical Code: In hot paths where every allocation matters, direct field access is faster than ScriptableObject references

Quick Decision Guide:

Ask yourself these questions:

  1. Is this data shared by multiple objects? → Yes: ScriptableObject | No: Continue
  2. Will designers need to modify it? → Yes: ScriptableObject | No: Continue
  3. Does it change frequently at runtime? → Yes: Regular field | No: Continue
  4. Is it complex configuration data? → Yes: ScriptableObject | No: Regular field

Integration with Existing Unity Systems

ScriptableObjects work well with Unity’s other systems:

Addressables: ScriptableObjects can be loaded asynchronously using Unity’s Addressables system for better memory management.

Custom Editors: Create custom inspectors for ScriptableObjects to provide designer-friendly interfaces.

Events: Combine ScriptableObjects with Unity Events or C# events for data-driven event systems.

ScriptableObjects scale way better than hardcoded arrays since they keep game logic and data separate. Designers don’t have to dig through code to change enemy health - they just modify the asset.

Frequently Asked Questions

How do ScriptableObjects compare to Singletons?

ScriptableObjects are often better than Unity singletons for shared data. They provide the same global access without singleton pattern problems like initialization order issues or testing difficulties.

Can I create ScriptableObjects at runtime?

Yes, but they won’t persist between sessions unless explicitly saved. Most use cases involve creating ScriptableObjects as assets during development, not runtime.

Should I use ScriptableObjects for save data?

Not directly. ScriptableObjects are for configuration data. For save games, serialize runtime data to JSON or binary files, potentially using ScriptableObjects as templates for default values.

How do I reset ScriptableObject values changed during play mode?

Changes made to ScriptableObjects during play mode persist after stopping. To reset values:

  1. Store initial values in Awake() and restore in OnDisable()
  2. Create a “Reset to Defaults” context menu method
  3. Use version control to revert changes
  4. Duplicate the asset before testing

Most devs just create runtime copies to avoid this problem.

Can ScriptableObjects have methods and game logic?

Yes! ScriptableObjects can have methods, not just data. This makes them powerful for strategy patterns:

public abstract class PowerUp : ScriptableObject
{
    public abstract void Apply(PlayerStats player);
}

public class HealthPowerUp : PowerUp
{
    [SerializeField] private int healAmount = 25;
    
    public override void Apply(PlayerStats player)
    {
        player.Heal(healAmount);
    }
}

How do ScriptableObjects work with prefab variants?

ScriptableObject references in prefabs work perfectly with prefab variants. Each variant can reference different ScriptableObject assets while maintaining the prefab connection. This is ideal for enemy variants with different stats or weapons with different configurations.

What’s the performance difference vs Resources.Load?

ScriptableObjects referenced in scenes load with the scene (better performance). Resources.Load happens at runtime (causes hitches). ScriptableObjects also support preloading and async loading through Addressables, making them more flexible for performance optimization.

How do I handle ScriptableObject references in version control?

ScriptableObject assets are text files (YAML) by default, making them merge-friendly. Best practices:

  • One ScriptableObject per file (automatic with CreateAssetMenu)
  • Descriptive filenames (“EnemyData_Goblin” not “Data1”)
  • Keep ScriptableObjects small and focused
  • Use force text serialization in Project Settings

Unity ScriptableObject Documentation & Resources (2025)

For deeper technical details, check Unity’s official documentation:

The Unity community has also created excellent resources:

Conclusion

ScriptableObjects fix messy data problems. They keep data separate from logic, let designers tweak balance without code, and boost performance by sharing data between objects.

You wouldn’t put texture data in your scripts, right? So why put game config there? Treat your data like any other Unity asset.

Start small - pick one place where you’re using arrays or public fields for config data and convert it to a ScriptableObject. You’ll see the difference right away.

For bigger projects with tons of enemies, weapons, and levels, you need ScriptableObjects. Sure, setting up the data structure takes time at first, but you’ll save hours later when making changes.

Angry Shark Studio Logo

About Angry Shark Studio

Angry Shark Studio is a professional Unity AR/VR development studio specializing in mobile multiplatform applications and AI solutions. Our team includes Unity Certified Expert Programmers with extensive experience in AR/VR development.

Related Articles

More Articles

Explore more insights on Unity AR/VR development, mobile apps, and emerging technologies.

View All Articles

Need Help?

Have questions about this article or need assistance with your project?

Get in Touch