Mastering Scriptable Objects in Unity: A Complete Guide
🛩️

Mastering Scriptable Objects in Unity: A Complete Guide

image

If you're a Unity developer, you've likely heard of MonoBehaviour 📘, but there's another lesser-known feature that can be a game-changer for your workflow - Scriptable Objects.

While these might not have the same fame as their MonoBehaviour cousins, underestimating them could mean missing out on a powerful tool 🔧 that can streamline your development process and vastly improve the efficiency and flexibility of your games 🎮.

Scriptable Objects are a versatile data container (and much more) used in the Unity engine.

They are designed to enable you to create, store, and manipulate custom data independent of script instances, a feature that provides numerous advantages, particularly in a complex game where data needs to be shared across multiple scenes and scripts.

But what does this really mean? 🤔 And how can you leverage Scriptable Objects to elevate your game development process?

This article aims to demystify Scriptable Objects, delving into their benefits, illustrating their use through practical examples, and demonstrating how they can revolutionize your approach to game design.

Whether you're a seasoned Unity developer or a novice just starting your journey, our exploration of Scriptable Objects will provide you with valuable insights and tools to take your projects to the next level 💪.

So, strap in and prepare to unlock the full potential of your game development process! 🚀

Introduction to Scriptable Objects 👍

Welcome to the fascinating world of Unity's Scriptable Objects! If you're a Unity developer, you might already be familiar with the concept of objects. But Scriptable Objects? That might be something new.

Scriptable Objects are a special kind of data container that Unity provides us with. In essence, a Scriptable Object is an instance of a script in your project, separate from MonoBehaviour, that allows you to store large amounts of shared data independent from class instances.

They are, in fact, serializable, custom data structures! 🎓

"But, why should I care?" 🤔

Good question! Scriptable Objects can be a massive boon to your game development process. They offer an excellent way to create and store data that isn't tied to a specific GameObject. Imagine having game data that doesn't disappear or reset every time you hit the play button. Sounds dreamy, right? Well, that's the magic of Scriptable Objects! ✨

The benefits of using Scriptable Objects

Scriptable Objects open up a plethora of benefits for game developers. Here are some significant advantages to integrating Scriptable Objects into your Unity projects:

  1. Reusable Data 🔄: Scriptable Objects are fantastic for creating reusable data. As we showcased with our RPG example, you can use a single Scriptable Object to define the attributes for a specific character class and then apply it to any character of that class in your game. This reduces data redundancy and promotes reusability, making your game development process more efficient.
  2. Data Persistence 💾: Unlike MonoBehaviours, the data stored in Scriptable Objects persists between play sessions in the Unity editor. So, if you change a Scriptable Object's data during play mode, it will remain even after you stop playing. This can be very handy for testing and tweaking game balance!
  3. Designer-Friendly 👩‍💻: Scriptable Objects are incredibly designer-friendly. They allow programmers to set up systems and then designers to populate those systems with data directly in the editor. This not only speeds up iteration time but also facilitates collaboration between coders and designers.
  4. Memory Efficiency 🚀: Scriptable Objects can be loaded and unloaded from memory as needed, and multiple references can point to the same instance of an object. This helps optimize your game's performance, especially important when dealing with large amounts of data or on platforms with limited resources.
  5. Organization and Modularity 🗂️: Scriptable Objects promote cleaner and more modular code. They help segregate the game's data from the logic, making the project easier to manage, understand, and debug.

Remember, while Scriptable Objects offer many benefits, they are not a silver bullet for all situations. In the next sections, we'll take a look at when it's appropriate to use them and some common mistakes to avoid. Stay tuned! 🎵

👨‍🏫 Scriptable Objects vs. MonoBehaviours

If you've been using Unity for a while, you're likely quite comfortable with MonoBehaviours. They're the backbone of Unity's scripting system, allowing you to attach custom behavior to your GameObjects. However, MonoBehaviours have some limitations, which is where Scriptable Objects shine. Let's understand this with a quick comparison.🔍

MonoBehaviours live on GameObjects and can't exist without a GameObject to hold them. This design is excellent for handling game logic related to GameObjects, but it makes them less ideal for managing game-wide data.

Additionally, the data inside MonoBehaviours gets reset every time you hit play in the editor.

If you're trying to balance your game by tuning parameters in play mode, those changes will be lost when you stop playing. 🔄

Enter Scriptable Objects. These versatile containers aren't tied to GameObjects and can store data persistently even when the game stops running in the editor. Imagine being able to tweak game parameters on the fly without losing your changes—That's the power of Scriptable Objects! 💪 Furthermore, Scriptable Objects can be referenced by multiple scripts or objects, making them ideal for shared data and settings.

Feature
MonoBehaviour
ScriptableObject
Lifespan
Tied to GameObjects. Destroyed when GameObjects are destroyed or when the scene changes.
Independent of scenes and GameObjects. Can persist across scenes.
Use of Coroutines
Supported. Can be used for handling asynchronous tasks or delaying actions.
Not supported.
Event methods
Supports Unity event functions like Start(), Update(), Awake(), OnEnable(), OnDisable(), etc.
Only support some events
Instance creation
Created by attaching scripts to GameObjects in the scene.
Created via ScriptableObject.CreateInstance<T>() or in the editor.
Saving state during play mode
Does not preserve variable state when exiting play mode in Unity Editor.
Preserves variable state when exiting play mode in Unity Editor.
Use of built-in components
Can utilize built-in components (like Transform, Rigidbody, etc.)
Cannot utilize built-in components.
Memory usage
More memory usage as it's tied to GameObjects.
Less memory usage as it's not tied to GameObjects.
Scene interaction
Can interact directly with the scene (e.g., physics, rendering, etc.)
Cannot interact directly with the scene.

User Case RPG Game and Scriptable Objects as Data Container

image

🧙‍♂️🎮 In a typical RPG game, you might have various types of elements like characters, weapons, potions, and a storyline. Let's think of all these as the "actors" or MonoBehaviours of our game. They interact with each other, perform actions like battling monsters, healing, or leveling up, and react to events such as user input or game state changes.

Now, suppose you have a complex system of character stats, weapon stats, and potions that affect these stats. You could store this information within the MonoBehaviour scripts attached to each object, but it quickly becomes hard to manage, especially if different objects need access to the same data. Here's where Scriptable Objects come in handy!🔑

Imagine Scriptable Objects as the "directors" of our game. They don't actively participate in the gameplay but hold critical information that defines how the game operates.

For example, a Scriptable Object could hold the data for a specific character class, including attributes like health, mana, strength, agility, and intelligence. The same goes for weapons and potions, where each type could have a corresponding Scriptable Object holding its specific stats. 📚

This approach provides a centralized location for all of your game's vital data, making it much easier to manage and tweak.

Plus, you can adjust these values in real-time while the game is running in Unity's editor, and they'll persist after you stop playing, making it a breeze to balance your game!

Another advantage of this approach is the easy sharing of data.

For instance, a health potion MonoBehaviour can reference a Scriptable Object to know how much health it should restore. And a character's MonoBehaviour can also reference the same Scriptable Object to modify the character's health. This reusability prevents data duplication and promotes a more robust and flexible system. 💼

To sum up, in our RPG game, Scriptable Objects are like the directors, setting the scene by defining the properties of the characters, weapons, and potions, while the MonoBehaviours (characters, weapons, potions) are the actors that perform actions and interact based on these predefined rules. 🎬

I hope this RPG example makes the MonoBehaviour-Scriptable Object relationship clearer.

📝 How to create a Scriptable Object

In Unity, creating a Scriptable Object is as straightforward as creating a new C# script. Start by creating a new script in your project's Scripts folder and name it something relevant. For our example, let's call it 'CharacterData'. This script will store the various attributes for different character classes in our RPG game.

Step 1 - Create the code

In the CharacterData script, instead of inheriting from MonoBehaviour, you should inherit from Scriptable Object. Below is an example of how your CharacterData script might look:

using UnityEngine;

[CreateAssetMenu(fileName = "New Character", menuName = "Character Data")]
public class CharacterData : ScriptableObject
{
    public string characterName;
    public int maxHealth;
    public int maxMana;
    public int strength;
    public int agility;
    public int intelligence;
}

The [CreateAssetMenu] attribute above the class declaration allows you to create new instances of this Scriptable Object via the Unity editor's 'Assets' menu. 📁

Step 2 - Creating Instances of Scriptable Objects

image

To create a new instance of your CharacterData scriptable object, right-click in your project window, navigate to 'Create', then 'Character Data'. This will create a new CharacterData object in your project. You can create as many instances as you like, each with different data, representing different character classes (like a warrior, mage, or thief) in your game. 🧝‍♂️🧙‍♀️🦹‍♂️

Step 3 - Using Scriptable Objects

image

Now that you have your CharacterData instances, you can use them in your game! Let's say you have a character MonoBehaviour script attached to your player character. You can create a public field in this script of the type CharacterData. Then, in the Unity editor, you can drag the Scriptable Object instance you want to use onto this field.

Your MonoBehaviour script will now have access to all the data stored in the Scriptable Object, allowing you to use this data in your gameplay scripts. It's that simple! 🎉

Understanding the Lifecycle and Callbacks of Scriptable Objects in Unity 🔄

In Unity, Scriptable Objects have their own lifecycle and set of callbacks that are triggered at different points of their existence. To better understand how and when these events happen, let's go through each callback:

image
  1. Awake: Just like MonoBehaviour’s Awake callback, this is called when the ScriptableObject script starts. This happens when the game launches or if a scene loads that references the ScriptableObject asset. Imagine it as the ScriptableObject opening its eyes for the first time. 🌅
  2. OnEnable: This callback is triggered when the ScriptableObject is loaded or instantiated. It comes immediately after the Awake callback. OnEnable executes during the ScriptableObject.CreateInstance process or after a successful script recompilation. Think of this as the ScriptableObject getting ready for the day. 🏋️
  3. OnDisable: This callback is called when the ScriptableObject goes out of scope. This occurs if you load a Scene without any references to the ScriptableObject asset or just before the ScriptableObject's OnDestroy method is called. Unity also executes OnDisable before script recompilations. When entering Play mode, OnDisable runs right before OnEnable. Consider this as the ScriptableObject going to sleep for the day. 🌙
  4. OnDestroy: This is called when something destroys the ScriptableObject, either from deleting it in the Editor or from code. If you’ve created the ScriptableObject at runtime, OnDestroy also invokes when the application quits or if the Editor exits Play mode. Note that this only destroys the native C++ part of the object. This is the end of the lifecycle for our ScriptableObject - a final farewell. 👋

Remember, understanding these lifecycle events is key to harnessing the full potential of Scriptable Objects in your game development. So, spend some time exploring these concepts and experimenting with them in your projects.

Editor-only functions/callbacks

  1. OnValidate: OnValidate executes when the script is loaded or a value changes in the Inspector. This can be used to ensure that your data stays within a certain range.
  2. Reset: Reset invokes when you hit the Reset button in the Inspector context menu.

Scriptable objects on Device vs Editor

A scriptable object on the device is like a pointer, every time you load a scriptable object with Resources.Load<>(Constants.GAME_DATA_PATH); the SAME scriptable object will be loaded, not a new one or a copy. (It’s like a pointer to the same resources)

When you are in editor mode, every change made during the session will be saved, but on the device, once the session is over, all the changes made to the scriptable object will be LOSE.

Understanding the Persistence of ScriptableObjects Across Scenes 🌐

As game developers, we often want to use ScriptableObjects as mutable data containers that are synchronized across all scenes during runtime, including in the build. The challenge comes in understanding how to maintain this data consistently across scenes, especially when dealing with the concept of "resets."

When a scene is loading, serialized references to assets in the scene will cause the instantiation of these assets as new runtime objects. ScriptableObject assets are no exception to this.

In essence, the data "resets" due to the creation of new instances of the asset during scene transitions. This could lead to the perception that ScriptableObjects are losing data between scenes.

Example:

Let's say in our RPG game, we have a ScriptableObject named "PlayerData" that holds vital statistics about our player character, such as health and experience points. These values are dynamic and can change during gameplay.

Now, let's say our brave hero 🗡️ has just finished a ferocious battle in Scene 1. They've gained experience points and lost some health, both of which are stored in our "PlayerData" ScriptableObject. Then, our hero moves onto Scene 2.

Upon loading Scene 2, Unity will instantiate a new "PlayerData" ScriptableObject based on the initial, serialized blueprint of "PlayerData". This is akin to a 'reset'. Therefore, rather than seeing the updated health and experience points from the battle in Scene 1, we might see the 'fresh' character stats as they were when the game first started.

The problem with "resets" is the following: when a scene is loading, serialized references to assets in this scene will cause instantiating of these assets as new runtime objects, and scriptable object assets are not an exception, therefore, if you have mutated data in the instantiated Asset Ain scene 1, in scene 2 you often will have another instance of Asset A with all states "resetted" because its object was created from serialized "blueprint" of Asset A

This example illustrates the importance of understanding how Unity handles ScriptableObject instances across scenes, and why we need to implement measures to maintain consistent, mutable data. 🎮🔄

Prevent the unloading of ScriptableObjects between scenes 🧞

To prevent the unloading of ScriptableObjects between scenes, even if they're not referenced in one scene, you can add a specific code snippet to your ScriptableObject (see the solution here: http://answers.unity.com/comments/1569389/view.html).

 private void OnEnable() => hideFlags = HideFlags.DontUnloadUnusedAsset;

Basically, you can set the ScriptableObject's hideFlag to prevent it from unloading through scenes where it's not being referenced, like so:

This understanding is crucial for leveraging the power of ScriptableObjects in managing data across different scenes, providing a seamless and consistent gaming experience. 🎮🔁

Create a scriptable object in the code at run time

public static void CreateMyAsset()
    {
        MyScriptableObjectClass asset = ScriptableObject.CreateInstance<MyScriptableObjectClass>();        
    }

Create and save a scriptable object in code in Editor Mode:

Note:remember to use the full scriptable object asset path es:“Assets/Gladio Games/Resources/GameData.asset”

AssetDatabase.CreateAsset(asset, "Assets/NewScripableObject.asset");
        AssetDatabase.SaveAssets();

        EditorUtility.FocusProjectWindow();

        Selection.activeObject = asset;

Destroying Scriptable Objects

image

Show scriptable objects data directly into the monobehaviour

ScriptableObjects give us a convenient way to decouple data from behavior. But when you have data distributed across ScriptableObjects and MonoBehaviours, you might find yourself clicking back and forth between them in the Unity editor. A neat solution to streamline this process is using Custom Inspectors. 🚀👀

Custom Inspectors in Unity allow you to customize how your scripts look in the Unity Inspector. When dealing with ScriptableObjects, this feature becomes a powerful tool that can help you visualize and edit your data right from the MonoBehaviour inspector. This section will guide you through creating a custom editor for your ScriptableObject.

Let's begin by understanding the essentials of creating a Custom Inspector:

  • First, you'll create a new class derived from Editor. This class needs to be saved in a folder named "Editor".
  • Apply the CustomEditor attribute to this new class, and specify the type of your ScriptableObject. This tells Unity that this editor class will customize the inspector for your specific ScriptableObject.
  • Inside the OnInspectorGUI method, you can define how your custom inspector should look and behave.

Below is an example of a Custom Inspector for a ScriptableObject called NPCHealth. This code shows the MonoBehaviour's variables and also draws the ScriptableObject's inspector:

using UnityEditor;
[CustomEditor(typeof(NPCHealth))]
public class NPCHealthEditor : Editor
{
    private Editor editorInstance;

    private void OnEnable()
    {
        // Reset the editor instance
        editorInstance = null;
    }

    public override void OnInspectorGUI()
    {
        // The inspected target component
        NPCHealth npcHealth = (NPCHealth)target;

        if (editorInstance == null)
            editorInstance = Editor.CreateEditor(npcHealth.config);

        // Show the variables from the MonoBehaviour
        base.OnInspectorGUI();

        // Draw the ScriptableObject's inspector
        editorInstance.DrawDefaultInspector();
    }
}

When you've set this up, you should be able to see the NPCHealth ScriptableObject's data directly in the MonoBehaviour that references it, making your data much more accessible! 🙌

Remember, the implementation of a custom editor might vary depending on the complexity of your ScriptableObject and the specific needs of your project. For more detailed guidance on custom editors, check out Unity's documentation on Custom Editors.

General script to display the fields of a ScriptableObject in the inspector

image

This following script from Tom Kail is an extension of how ScriptableObject references are displayed in the inspector. By default, Unity only shows you a single line with a reference to the ScriptableObject, but with this script, you'll see all properties of the ScriptableObject right there in the inspector when the object reference is not null.

Make Changes to Scriptable objects persistent

In order to persistently save changes made to ScriptableObjects during runtime, you'll need to Serialize then and then save the data into a file.

While ScriptableObjects themselves are not intended to be saved during runtime, they can serve as a reference for the data that needs to be saved and loaded.

Here's how you might approach it:

Let's say you have a ScriptableObject that tracks the state of a game character - their health, level, position, etc. While you play the game, you’ll change the scriptableObject data, but at some point during the game, you can decide to serialize and save the scriptable object data into a file.

Now to persist these changes, you would serialize this scriptable into a format suitable for storage. JSON and binary are common formats used for serialization. The JsonUtility.ToJson() method in Unity can be used for JSON serialization.

Once you have your serialized data, you can write it to a file on your user's disk using System.IO.File.WriteAllText() for JSON.

Then, when you want to load your data back into the game, you'd read the data from the file, deserialize it back into a class instance, and use that data to set the values of your ScriptableObject.

This approach lets you take advantage of the convenience of ScriptableObjects during development while still being able to persistently save and load data during runtime. It's a best-of-both-worlds solution! 💾🎮🔄

Design pattern with Scriptable Objects

The following pattern and example have been taken from the Create modular game architecture in Unity with ScriptableObjects Ebook.

Pattern Delegate Objects with Scriptable Objects

You can make this pattern more useful by defining the EnemyAI ScriptableObject as an abstract class. This allows it to act as a template for a variety of ScriptableObjects that are compatible with the EnemyUnit MonoBehaviour, so the abstract ScriptableObject can stand in for more than one algorithm.

image

Thus, you could have concrete ScriptableObject classes for behaviors like Patrol, Idle, or Flee that derive from the base EnemyAI. Even though they all implement the same MoveUnit method, each can produce very different results.

In the Editor, each asset is interchangeable. You can just drag and drop the ScriptableObject of choice into the EnemyAI field. Any compatible ScriptableObject is “pluggable” in this fashion.

The EnemyUnit or another component can behave as the “brain” that monitors when to switch ScriptableObjects and also swap behavior at runtime. This is one way the EnemyUnit can react to gameplay events. Simply switch EnemyAI ScriptableObjects on each state change.

In production, a second developer or designer can implement the actual movement or AI logic within the ScriptableObject. As additional movements or behaviors get added to the game (e.g., DuckAndCover, Chase, etc.), the original EnemyUnit script remains unchanged. This pattern can help keep your codebase more extensible, in keeping with the open-closed principle from SOLID programming. Because everything is already split into smaller objects, the resulting project is more scalable as you add team members.

Observable Pattern

Many developers opt to use singletons – one global instance of a class that survives scene loading. Singletons, however, introduce global states and make unit testing difficult.

If you’re working with a Prefab that references a singleton, you’ll end up importing all of its dependencies just to test an isolated function. This reduces modularity and debuggability.

Consider an alternate solution to help your objects communicate: ScriptableObject-based events.

In the observer design pattern, a subject broadcasts a message to one or more loosely decoupled observers. Each observing object can react independently of the subject but is unaware of the other observers. The subject can also be referred to as the “publisher” or “broadcaster.” The observers are also known as “subscribers” or “listeners.” An event-based architecture only executes when needed, rather than running each frame. For this reason, it’s often more optimized than adding logic to a MonoBehaviour’s update methods.

You can implement the observer pattern with MonoBehaviours or C# objects. While this is already common practice in Unity development, a script-only approach means your designers will rely on the programming team for every event needed during gameplay

An alternative is to create ScriptableObject-based events.

This is a designerfriendly way to set up the observer pattern. Here, the ScriptableObject works as an intermediary between subject and observer, providing a graphical interface in the Editor.

image

Pattern Run Time Sets

At runtime, you’ll often need to track a list of GameObjects or components in your scene. For example, you may need to maintain a list of enemies or NPCs.

Because a ScriptableObject instance appears at the project level, it can store data that’s available to any object from any scene.

Again, this replicates much of the easy global access of a singleton without that pattern’s known drawbacks. Reading data directly from a ScriptableObject is also more optimal than searching the Scene Hierarchy with a find operation such as.

Object.FindObjectOfType or GameObject.FindWithTag. Depending on your use case and the size of your hierarchy, these are relatively expensive methods that can be inefficient for per-frame updates.

Basic Runtime Set

Instead, consider storing data on a ScriptableObject as a “Runtime Set.” This is a specialized data container that maintains a public collection of elements but also provides basic methods to add and remove to the collection.

image

Here’s a basic Runtime Set that tracks a list of GameObjects:

using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(menuName = "GameObject Runtime Set", fileName
= "GORuntimeSet")]
public class GameObjectRuntimeSetSO : ScriptableObject
{
private List<GameObject> items = new List<GameObject>();
public List<GameObject> Items => items;
public void Add(GameObject thingToAdd)
{
if (!items.Contains(thingToAdd))
items.Add(thingToAdd);
}
public void Remove(GameObject thingToRemove)
{
if (items.Contains(thingToRemove))
items.Remove(thingToRemove);
}
}

At runtime, any MonoBehaviour can reference the public Items property to obtain the full list. Another script or component must be responsible for managing how the GameObjects are added or removed from this list.

image

Reference the Runtime Set in a MonoBehaviour. Then, in the OnEnable and OnDisable event functions, add or remove the object from the Runtime Set’s Items list. Alternatively, use an event channel to send a GameObject as its payload (e.g., GameObjectEventChannel)

Generic Version

image
image

Scriptable Objects Unity: Conclusion

"In conclusion, ScriptableObjects in Unity offer a powerful 💪 and efficient way to handle data storage and manage state. They allow for modular design 🧩, ease of testing 🧪, and a clear separation of concerns, thereby improving the overall organization and structure of your game development project.

While they cannot entirely replace MonoBehaviours for direct scene interaction or use of Unity's lifecycle events, their persistent nature and lower memory usage make them an indispensable tool 🛠️ for managing game data. Remember, understanding when to use ScriptableObjects versus MonoBehaviours can significantly impact your project's performance and scalability. So keep exploring, keep learning, and happy coding! 👨‍💻🎮.

Resources 📕

  1. ScriptableObject | Unity Engine API → Unity's official documentation on ScriptableObjects
  2. Create modular game architecture in Unity with ScriptableObjects → Unity Ebook
  3. Three ways to architect your game with scriptableobjects → Unity Blog
  4. Unity Learn - Scriptable Objects → Unity's Learn Tutorials
  5. Unity Tutorial - Scriptable Objects - Brackeys → video
  6. Unite 2016 - Overthrowing the MonoBehaviour Tyranny in a Glorious ScriptableObject Revolution → Video
  7. Unite Austin 2017 - Game Architecture with Scriptable Objects - Video
  8. The power of Scriptable Objects → Unity Blog
  9. Understanding Scriptable Objects in Unity → Article

Author

image

Marco Mignano 👋 ➡️ Passionate Unity Game Developer Marco - coding aficionado, video game enthusiast, and self-proclaimed piazza addict. Constantly conquering new challenges, one line of code at a time. Got an exciting project? Let's make your game next game together!

You may also like 💙