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.
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.
Creating Your First ScriptableObject: Step-by-Step (Unity 2022.3 LTS)
Step 1: Create the ScriptableObject Class
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.
Step 2: Create Assets in Unity
- Right-click in Project window
- Navigate to âCreate â Game â Wave Configurationâ
- Name your asset (e.g., âWave_1_Easyâ)
- Configure values in Inspector
You can create as many wave configs as you want. Each one becomes its own asset file.
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.
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;
}
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:
- Is this data shared by multiple objects? â Yes: ScriptableObject | No: Continue
- Will designers need to modify it? â Yes: ScriptableObject | No: Continue
- Does it change frequently at runtime? â Yes: Regular field | No: Continue
- 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:
- Store initial values in Awake() and restore in OnDisable()
- Create a âReset to Defaultsâ context menu method
- Use version control to revert changes
- 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:
- ScriptableObject Class Reference - Complete API documentation
- CreateAssetMenu Attribute - Menu creation options
- Unity Serialization Guide - How Unity saves your data
- ScriptableObject Tutorial - Unityâs official tutorial
- Game Architecture with ScriptableObjects - Advanced patterns
The Unity community has also created excellent resources:
- Scriptable Objects Unite Talk - Classic talk on SO architecture
- Unity Forum ScriptableObject Tips - Community discussions and solutions
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.

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