What are Scriptable Objects?

In Unity, while MonoBehaviour objects are stored in memory and exist only while playing the game; Scriptable Objects exist in the project itself and are stored in serialised form. They are used as a container to store values.  

A scriptable object exists even when the game is stopped and persists between play modes. Values are retained between edit and play mode. Scriptable object assets can be created and stored like other assets such as images, materials, etc.

Creating Scriptable Objects

To make a scriptable object, we must create a class that inherits from the ScriptableObject class. It is also helpful to create a CreateAssetMenu header from the editor menu.

In this example, we will create a CharacterStats class to demonstrate the creation and use of a scriptable object. It will be used to store the status of a character. It consists of the health, name  and armor attributes.

Let's create getters for those. Also, we will be adding a Damage method so that health can be decreased. Incoming damage will be decreased by armor amount before decreasing health.

using UnityEngine;

// creating asset menu shows scriptable object in editor menu. We will be able to create this object from there 
[CreateAssetMenu(fileName = "CharacterStats", menuName = "CustomScriptableObject/CharacterStats")]
public class CharacterStats: ScriptableObject {
  // properties of character
  [SerializeField] private float health;
  [SerializeField] private float armor;
  [SerializeField] private string name;

  // Deal damage, damage will be reduced by armor
  public void Damage(float damage) {
    damage = damage - armor;
    if (damage < 0) return;
    health -= damage;
  }

  // get current health
  public float GetHealth() {
    return health;
  }

  // get player's name
  public string GetName() {
    return name;
  }
}
Scriptable Object Storing Player Data

Now, we can create the asset from Asset > Create > CustomScriptableObject.

Creating Scriptable Object Asset

Uses of Scriptable Objects

Since scriptable object assets exist in the project, they can have use cases that are not possible with normal MonoBehaviour objects. Let's discuss some of these use cases.

Global Variables

Scriptable objects assets can be used to make global objects that can be accessed by multiple objects.

For example, let's create a DamageDealer and CharacterUI class that will reference the scriptable asset we created earlier. Since they use the same asset, values will be the same for both. This can also be done from a static class, but static classes can have only one instance, and their value can't be viewed or edited from the editor menu.

public class CharacterDamageDealer: MonoBehaviour {
  public CharacterStats characterStats; // reference to scriptable object asset asset

  // deal damage to player
  public void CharacterHit() {
    characterStats.Damage(10);
  }
}

// UI system to show stats
public class CharacterUI: MonoBehaviour {
  public CharacterStats characterStats; //  reference to scriptable object asset asset

  public TextMeshProUGUI health;// text to display health
  public TextMeshProUGUI characterName;// text to display name

  private void Update() {
    health.text = characterStats.GetHealth().ToString();
    characterName.text = characterStats.name;
  }
}
Multiple Classes Referencing the Same Asset

Game Events

Events can be invoked from scriptable assets and listened to by other classes that reference them. These events can be used to detect when the state of the game is changed.

Let's add an onplayerDamaged event in CharacterStats so that the player can get notified when their character takes damage. CharacterUI will listen to this event so that it can change the UI when the player takes damage. Doing this will eliminate the need to continuously update text in Update call.

public event Action < float > onPlayerDamaged; // event to notify damage

// Deal damage, damage will be reduced by armour
public void Damage(float damage) {
  damage = damage - armour;
  if (damage < 0) return;
  health -= damage;
  onPlayerDamaged?.Invoke(health);
}
Event Invoker
// subscribe to damage event of player at start
private void Start() {
  characterStats.onPlayerDamaged += OnHealthChanged;
}

//change health when damage is done
private void OnHealthChanged(float currentHealth) {
  health.text = currentHealth.ToString();
}
Event Listener

Abstract Scriptable Objects

Making an abstract scriptable object class will allow us to create multiple variations of a single class. These objects can be plugged in from the editor, allowing us to change their behaviour from the editor itself.

For demonstration, let's create a buff system that will modify the damage taken by the player. First, create an abstract  PlayerBuff  class that contains a method to modify damage.

// abstract class to implement buff
public abstract class PlayerBuff: ScriptableObject {
  // modifies damage dealt
  public abstract float ModifyDamage(float damage);
}
Abstract Buff Class

Now create various buffs by extending from the abstract class. The first type of buff is healing. It just converts damage into healing. Let's call it Healing

// this buff heals amount equal to damage
[CreateAssetMenu(fileName = "Healing", menuName = "CustomScriptableObject/Buff/Healing")]
public class Healing: PlayerBuff {
  public override float ModifyDamage(float damage) {
    if (damage > 0) damage = -damage; // make damage negative so that it will heal

    return -damage;
  }
}
Healing class

The second buff we will create is Weakness . This negative buff will increase damage taken proportional to weaknessAmount .

// it increases damage by
[CreateAssetMenu(fileName = "Weakness", menuName = "CustomScriptableObject/Buff/Weakness")]
public class Weakness: PlayerBuff {
  public float weaknessAmount;
  public override float ModifyDamage(float damage) {
    damage *= weaknessAmount; // increasing damage
    return damage;
  }
}
Weakness Class

The third buff is Armor. This buff decreases incoming damage by armor . Also, it removes armor every time damage is taken.

// decreases damage
[CreateAssetMenu(fileName = "Armor", menuName = "CustomScriptableObject/Buff/Armor")]
public class Armor: PlayerBuff {
  public float armor;
  public override float ModifyDamage(float damage) {
    damage -= armor; //decreases damage by armor
    return damage > 0 ? damage : 0; //make sure damage is always positive
  }
}
Armor Class

Now, let's modify the Damage method of the CharacterStats class so that all buffs can be applied. We will store all buffs of the player in the playerBuffs list of the type PlayerBuff.

Notice that we are creating a list of abstract classes so that all types of buffs can be stored.

public List < PlayerBuff > playerBuffs;

// Deal damage, damage will be reduced by armour
public void Damage(float damage) {
  // modify damage according to each buff
  foreach(PlayerBuff buff in playerBuffs) {
    damage = buff.ModifyDamage(damage);
  }

  // damage health
  health -= damage;
  onPlayerDamaged?.Invoke(health);
}
Modifying Incoming Damage

After that, all we need to do is create assets from it and place them in the character's stats list. I have created two Weakness type: Curse and Poisoned , two Armor types: HighArmor and LowArmor and a Healing type. These buffs can be just dragged and dropped in the player's status to apply them. Whenever a player takes damage, it will be modified before decreasing the player's health.

Assign Stats in Stats Object

Preserving Values Across Play and Edit Modes

Values changed in the in-game mode are retained while exiting play mode. This can be useful when we want to try out different values while the game is running. This is not possible using GameObjects as their values get reverted when the game is stopped.

Reducing Memory Usage

Every time a prefab is created in a scene, it creates a clone in memory. This may cause a waste of memory when the same prefab is instantiated multiple times. Suppose you have a prefab of an enemy that has the stats maxHealth and damage . Let's say 20 of them exist in a scene. Those 20 enemies have 20 instances of redundant maxHealth and damage in memory.

This problem can be solved using scriptable objects. You can create a scriptable object with maxHealth and damage property. All instantiated enemies can now refer to the same scriptable object. This creates only one copy of the value in memory, reducing usage. If you want another enemy type with a different value, you can just create another scriptable asset and use it.

Conclusion

Today we discussed the creation and use of scriptable objects. Building a game on the scriptable object architecture will optimise it and make it flexible to change. Besides the above uses, it has many other applications such as pluggable AI, state management, etc.

That's all for today, thank you for reading!