Game Development

What is ScriptableObject and Why You Should Use it?

What is ScriptableObject?

ScriptableObject, just like MonoBehaviour is a class that inherits from the base Object class in unity. However, unlike MonoBehaviours ScriptableObjects cannot be attached to a GameObject nor live in the scene. ScriptableObject, in essence, is a data container that can also have method bodies that exist as assets in your Unit project.

Because they inherit from the base Object class they also have the Awake, OnEnable, OnDisable, OnDestroy, and OnValidate methods. However, they do not have Start and Update methods because they are not actively a part of the unity life cycle. That is, you need to instantiate and manage their loop if you need to.

What are ScriptableObjects Good for?

The question should be what they are not good for in all honesty. ScriptableObjects open up a whole bunch of design options and flexibility when it comes to bringing your game project to life. Be it small or large, all sizes and kinds of games can benefit greatly from them.

First and foremost, they can be used as just data containers, as they are defined in the official Unity docs. You could extract common and less-likely-to-change data from your objects and put them inside of a ScriptableObject and reference it to get the data you need. But, why would you do that? What would be the benefit of it? There are mainly two benefits to this approach.

The first benefit is control over your data and maintainability. Let’s assume that you have a “Unit” object that holds the “MaxHealth”, “BaseSpeed” and “BaseAttackPower” properties among others for all of your units in the game that looks something like this.

using UnityEngine;

namespace Units
{   
    public abstract class Unit : MonoBehaviour
    {
        [SerializeField] private int BaseHealth;
        [SerializeField] private int BaseAttack;     
        [SerializeField] private float BaseSpeed;

    }

}

That you can then create all sorts of units like this

using UnityEngine;

namespace Units
{   
    public  class MeleeUnit : Unit
    {
        [SerializeField] private MeleeSkill Skill;

    }

}
using UnityEngine;

namespace Units
{   
    public  class RangedUnit : Unit
    {
        [SerializeField] private RangeSdkill Skill;

    }

}

You could go into each of your unit prefabs to set or update each of these variables one by one. When you need to update one or more of these properties you need to do all of it all over again. Which would also cause prefab and serialization overrides that is a nightmare to hunt down. This could be manageable in small amounts but as your units and your project grow it’ll become harder and harder to keep track of it.

Instead of doing it this way, extracting these three properties into a ScriptableObject and referencing it will allow us to change all unit values from a single point. That way we don’t have to worry about keeping the shared properties values synchronized. While also saving ourselves from editing tens of, maybe hundreds of different prefabs. A simple implementation would look like this.


using UnityEngine;

namespace UnitDatas
{
    [CreateAssetMenu(menuName = "Data/UnitData", fileName = "Base_UnitData")]
    public class UnitData : ScriptableObject
    {

     public int BaseHealth;
     public int BaseAttack;
     public float BaseSpeed;

    }
}

using UnityEngine;
using UnitDatas;

namespace Units
{
    public abstract class Unit : MonoBehaviour
    {
        [SerializeField] private UnitData UnitBaseData;
    }

}

The second benefit is in terms of performance. Let’s continue with our Unit example. Let’s say that you have 200 Ranged units and 300 Melee units. Because they both inherit from the base Unit class they both have the same there properties. Because of that when we create them we also create the same attributes with them for each object. Meaning we have duplicate values casting our valuable RAM resource. It may not seem that much, however, as the number of units, and the number of common properties increases, more and more resource is wasted. This is especially prominent in low-spec systems such as mobile.

However, when we use ScriptableObject we only need to have a single object that holds the values that are shared with however many objects we want. This significantly reduces memory consumption. Also, ScriptableObjects are not loaded into the memory until you access the values it stores, keeping your memory lean and clean.

Other Usages of Scriptable Objects

Aside from being great for keeping your memory clean and your mind sane, ScriptableObjects are great for creating lowly-coupled and highly-cohesive designs. What do I mean by this?

Low coupling refers to the idea that different parts of a system or software should have minimal dependencies on each other. This means that each part of the system should be independent of the others and only interact through well-defined interfaces. This makes it easier to change or maintain the system without affecting other parts because changes in one part of the system do not have cascading effects on other parts.

High cohesion, on the other hand, refers to the idea that different parts of a system should work together toward a single, well-defined goal. This means that each part of the system must contribute to the overall purpose of the system and be closely related to other parts. A high degree of cohesion makes it easier to understand the system as a whole because each part has a distinct and specific role within the system.

How do we achieve this so-called low coupling, high cohesion with ScriptableObjects then? The answer is abstraction and how ScriptableObjects live. First, let’s imagine that in your game you need to know when a unit dies so that you can update the UI that displays the number of units that the player has. Normally you’d do something like this.

namespace Units
{
    public abstract class Unit : MonoBehaviour
    {
        [SerializeField] private int BaseHealth;
        [SerializeField] private int BaseAttack;
        [SerializeField] private float BaseSpeed;

        public event Action OnUnitDied;
    }

}
using System.Collections.Generic;
using UnityEngine;
using Units;

namespace UIControllers
{
    public class UnitDisplayManager : MonoBehaviour
    {
        [SerializeField] private Text UnitCountText;
        [SerializeField] private List<Unit> Units = new List<Unit>();
        private int _unitCount = 0;
        private void Start()
        {
            _unitCount = Units.Count;

            foreach (var unit in Units)
            {
                unit.OnUnitDead += UpdateUnitCount();
            }
        }

        private void UpdateUnitCount()
        {
            _unitCount --;
            UnitCountText.text = $"{_unitCount}";
        }
    }

}

You first create an event for other objects to subscribe to so that when you fire that event they will be notified. In your “UIController” for example, you’d have the get the reference for each unit so that you can subscribe to their OnUnitDied event. Each time any unit that your UIController listens to fires its event you capture it and update your UI accordingly.

However, this is a terrible design. First of all, why is your UIController has a bunch of reference to your Unit objects? UIController’s job should be to display whatever data it needs to and not keep track of which unit has died or not. Secondly, what would happen when it’s decided that instead of the Unit object a UnitController object is going to fire the OnUnitDied event from now on? Now, you need to update every single class that references the Unit’s OnUnitDied event. Solution? Using ScriptableObjects as an intermediary to pass events between objects.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace EventDelegation
{
    [CreateAssetMenu(menuName = "EventDelegates/Void Event Delegate", fileName = "Void_Delegate")]
    public class VoidEventDelegateSO : ScriptableObject
    {

        private event Action _voidEvent;


        public void Subscribe(Action subscriber)
        {
            _voidEvent += subscriber;
        }
        public void UnSubscribe(Action subscriber)
        {
            _voidEvent -= subscriber;
        }
        public void FireEvent()
        {
            _voidEvent?.Invoke();
        }

    }
}

Here is a simple example of an event delegation with ScriptableObject. Now, all we need to do instead of directly referencing the OnUnitDie event on the Unit object, or any other object for that matter is to reference a VoidEventDelegateSO. Like this,

using UnityEngine;
using EventDelegation;

namespace Units
{
    public abstract class Unit : MonoBehaviour
    {
        [SerializeField] private int BaseHealth;
        [SerializeField] private int BaseAttack;
        [SerializeField] private float BaseSpeed;
        [SerializeField] private VoidEventDelegateSO OnUnitDiedEvent;

        void Die(){
            OnUnitDiedEvent.FireEvent();
        }


    }

}

using UnityEngine;
using EventDelegation;

namespace UIControllers
{
    public class UnitDisplayManager : MonoBehaviour
    {
        [SerializeField] private Text UnitCountText;
        [SerializeField] private VoidEventDelegateSO OnUnitDiedEvent;
        private int _unitCount = 0;
        private void Start()
        {
            _unitCount = Units.Count;


            OnUnitDiedEvent.Subscribe(UpdateUnitCount());

        }

        private void UpdateUnitCount()
        {
            _unitCount--;
            UnitCountText.text = $"{_unitCount}";
        }
    }

}

Now, we decoupled our UIController and Unit and increased our overall cohesion because now we can put any VoidEventDelegateSO the as a reference for our UIController and it wouldn’t care who fires the event. All it cares about is that the event is fired. Another, important advantage we gained is that this design is independent of the scene. Meaning, because ScriptableObjects live as assets, they can be referenced from anywhere without fearing losing the referance.

However, an important note is that even though they live as assets once we subscribe to the event to the example ScriptableObjects event, we should make sure to remove the subscribe object before destroying it because leaving dead references can cause problems.

With this, we pretty much covered some of the most important use cases for ScriptableObjects. There are many more ways that we can use them to improve our code base. However, I’m not gonna go into them in this post but, I plan to write a more in-depth post about them in the feature. In the meantime, you can look at how to implement object pooling with scriptable objects.

Leave a Reply

Your email address will not be published. Required fields are marked *