Sponsored By

Unity Editor Scripting (A kick-starter guide) - Part 3

An insight and overview of ScriptableObjects, how they can improve data loading/unloading data and can be beneficial in retaining play-mode runtime data.

Asad Sohail, Blogger

May 30, 2017

9 Min Read
Game Developer logo in a gray background | Game Developer

This article is originally published on devcrew.io.

In our previous posts on editor scripting, we discussed an overview and importance of editor scripting. We explored Gizmos, Custom Inspectors and Editor Windows in the part 1 and part 2. Now in this part, we will give you an insight and overview of scriptable objects.

What are Scriptable Objects?

According to Unity:

“A class, derived from Unity’s Object class, whose references and fields can be serialized.”

How scriptable objects look like

Scriptable object is a special type of object that doesn't need to be present in the scene or doesn't need to be attached on a game object to exist, because it can be saved as an asset in the project. The major use of scriptable object is to save data in a persistent way. It is just a data-only version of MonoBehavior and can only hold data variables.

What they can do?

Scriptable objects can be built/generated into Unity and can be saved as assets inside the project. They can be saved during runtime which means if you have to change something on runtime or play-mode, you can retain those changes using scriptable objects. You don't have to worry about parsing/serializing your data during runtime, they will do that for you. As it can store only data and is almost same as MonoBehavior, you don't have to worry about it's inter-op communication with your classes.

What are the limitations?

There are some limitations of scriptable objects which should be kept in mind while using them. They require editor scripting because they can only be created inside the editor. Also, you can't edit them outside Unity using a third-party tool. Because of this, they are meant to be used for saving and managing development life-cycle data only; i.e. they cannot be modified or saved once deployed. In short, scriptable objects are best for storing game development/game design data and optimized data loading.

Possible Usage Scenarios

  • You want to tweak values during play mode and want to retain them.

  • You want to change all game objects of some type.

  • You don't want to change game designer or artist to mess with the irrelevant values of your game object.

In all above scenarios, scriptable objects are useful and can always be saved into their own unique asset file. You can easily edit ScriptableObject instances during play mode and let game designers/artist iterate without worrying about the actual game data. Moreover, your scenes and prefabs will save and load faster. Also, it has a robust Separation of Concern (SoC) pattern implemented.

Example

Let's get hands on playing with scriptable objects. We have a simple class 'Player.cs' right now:


public class Player : MonoBehaviour
{
    //Player data
    public string _name;
    public int _health;
    public Color _color;

    private MeshRenderer _renderer;

    void Awake()
    {
        _renderer = GetComponent<MeshRenderer>();
        _name = "John";
        _health = 100;
        _renderer.material.color = Color.green;
    }

Our player is a placeholder cube (to keep it simple to understand) with a name, health and a color. In the Awake() callback, we are assigning our data to the player. Let's make a scriptable object. It's just a simple C# class 'PlayerData' which extends ScriptableObject:


public class PlayerData : ScriptableObject
{
    public string m_name;
    public int m_health;
    public Color m_color;    
}

Saving Scriptable Object as Asset

To save our SO as an asset in the project, create a simple C# class and include UnityEditor namespace to access editor functions and it doesn't need to extend any class:


using UnityEditor;
using UnityEngine;

public class MyEditorUtils
{

}

Now create a static generic method CreateAsset<T>() inside the class. Generic methods have type parameters and they provide a way to use types as parameter in a method:


public class MyEditorUtils
{
    public static void CreateAsset<T>() where T : ScriptableObject
    {

    }
}

Add this code inside the method:


public static void CreateAsset<T>() where T : ScriptableObject
{
     T asset = ScriptableObject.CreateInstance<T>();

     string path = AssetDatabase.GetAssetPath(Selection.activeObject);
     string assetPathAndName = AssetDatabase.GenerateUniqueAssetPath(path + "/New " + typeof(T).ToString() + ".asset");

     AssetDatabase.CreateAsset(asset, assetPathAndName);
     AssetDatabase.SaveAssets();
     AssetDatabase.Refresh();
     EditorUtility.FocusProjectWindow();
     Selection.activeObject = asset;
}

Now let's breakdown the code line by line:

  • Line 3: We are creating an instance of scriptable object of type T we are getting as our method's parameter.

  • Line 5, 6: We are getting the path of the selected folder in the project and then constructing a path and name string for our SO asset.

  • Line 8: We are creating a '.asset' file with our asset name and our desired path.

  • Line 9: Saving our SO asset to our project.

  • Line 10: Refreshing and updating our assets view and data.

  • Line 11, 12: Making project window focused and our saved SO asset selected.

Now let's get it wrapped in a static method so that it can be triggered on a [MenuItem] attribute action and pass PlayerData in the parameters:


public class MyEditorUtils
{
    [MenuItem("Assets/Create/Create Player Data Object")]
    public static void CreateAsset()
    {
        CreateAsset<PlayerData>();
    }

    public static void CreateAsset<T>() where T : ScriptableObject
    {
        T asset = ScriptableObject.CreateInstance<T>();

        string path = AssetDatabase.GetAssetPath(Selection.activeObject);
        string assetPathAndName = AssetDatabase.GenerateUniqueAssetPath(path + "/New " + typeof(T).ToString() + ".asset");

        AssetDatabase.CreateAsset(asset, assetPathAndName);
        AssetDatabase.SaveAssets();
        AssetDatabase.Refresh();
        EditorUtility.FocusProjectWindow();
        Selection.activeObject = asset;
    }
}

Go to Unity editor now and see it in action:

This is how our PlayerData SO looks like in inspector:

[CreateAssetMenu] Attribute

From Unity 5.1 and onwards, you can create an asset in just one line of code using [CreateAssetMenu] attribute. This attribute allows you to create menu items in the Assets/Create context menu. This is pretty quick to create an asset and there is no need of the above class to do this. All you have to do is, just mark our PlayerData class with a [CreateAssetMenu] attibute like this:


[CreateAssetMenu(fileName = "My Scriptable Object", menuName = "My Content/Create Player Scriptable Object", order = 0)]
public class PlayerData : ScriptableObject
{
    public string m_name;
    public int m_health;
    public Color m_color;    
}
  • fileName: is the name of the asset file which will be created in the assets.

  • menuName: is the hierarchy of the menu inside the Asset/Create menu.

  • order: is the order of your menu item in the Asset/Create menu. I set it 0 to be shown on top of every menu.

and here is the result:

Linking and Referencing the Scriptable Object

Referencing and using the data from the scriptable object is pretty easy. All you have to do is just create a variable of the type PlayerData:


public class Player : MonoBehaviour
{
    public PlayerData data;
    
    //Player data
    public string _name;
    public int _health;
    public Color _color;

    private MeshRenderer _renderer;

    void Awake()
    {
        _renderer = GetComponent<MeshRenderer>();
        _name = "John";
        _health = 100;
        _renderer.material.color = Color.green;
    }

In Awake(), assign data to the class variables like this:


void Awake()
{
     _renderer = GetComponent<MeshRenderer>();
     _name = data.m_name;
     _health = data.m_health;
     _renderer.material.color = data.m_color;
}

 

Assign the reference from the editor in the inspector:

Let's give some values to our PlayerData scriptable object from the inspector:

I have also added a label to see the player info in the game view quickly, get its reference inside my Player class and updating it in the Awake() method:


void Awake()
{
     _renderer = GetComponent<MeshRenderer>();
     _name = data.m_name;
     _health = data.m_health;
     _renderer.material.color = data.m_color;
  
      UpdateUI();
}

void UpdateUI()
{
     _playerInfo.text = "Name: " + _name + "\n" + "Health: " + _health + "\n" + "Color: " + _color;
}

Result:

Let's make two more copies of the player data objects and assign them different data:

 

Taking an array of PlayerData and toggling it on an index, this is our complete code (replaced single data variable with array):


public class Player : MonoBehaviour
{
    public PlayerData data;

    public string _name;
    public int _health;
    public Color _color;

    public PlayerData[] dataArray;

    [Range(0, 2)]
    public int dataIndex;

    public Text _playerInfo;

    private MeshRenderer _renderer;

    void Update()
    {
        _renderer = GetComponent<MeshRenderer>();
        _name = dataArray[dataIndex].m_name;
        _health = dataArray[dataIndex].m_health;
        _renderer.material.color = dataArray[dataIndex].m_color;

        UpdateUI();
    }

    void UpdateUI()
    {
        _playerInfo.text = "Name: " + _name + "\n" + "Health: " + _health;
    }
}

 

and here is our final result:

 

Callbacks

Scriptable object class has almost the same commissioning/decommissioning callbacks as MonoBehavior. Some of them are worth mentioning for their use:

  • Awake(): called when the scriptable is instantiated.

  • OnEnable()called when the ScriptableObject is instantiated or loaded just like Awake() and is executed during ScriptableObject.CreateInstance() call. It is also called in the editor after script recompilation.

  • OnDestroy(): is called right before the ScriptableObject is destroyed and is manually executed when explicitly calling Object.Destroy() calls.

  • OnDisable(): called when the ScriptableObject is about to be destroyed and executes just before OnDestroy and before Object is garbage-collected.

Lifecycle

Scriptable objects are created and loaded like all other assets, such as Textures, AudioClips and FBX and by following the life-cycle callbacks, they can be kept alive just like other assets. They eventually get unloaded when we explicitly call Destroy() or when Assets garbage collector runs.

We gave you a simple hands on ScriptableObject and its uses and benefits. In our next part, we will guide you through how to improve asset import pipeline using editor scripting.

Read more about:

Featured Blogs
Daily news, dev blogs, and stories from Game Developer straight to your inbox

You May Also Like