Friday, May 16, 2014

ScriptableObjects in Unity and how/why you should use them!

I relatively recently discovered Unity's ScriptableObjects, and fell in love with them immediately. For some reason, you don't see them around a whole lot, yet they solve a number of important problems, like... How do you store data in Unity? I assure you, my early attempts were quite horrific.

The problem

Lets imagine you have a shooting game. Guns are an integral part of a game like this, and so you need lots of guns in your game! In theory, you could just prefab all your guns, and work with them that way, and in a simple world, this generally works pretty well! But in this game, you need to interact with these guns in a lot of different ways, selecting them, customizing them, stat tweaking them, admiring their detail, shooting them, etc. Having all that in a prefab becomes ridiculous quite quickly!

I did mention lots of guns, right?

Soo, the next intuitive leap is to do some sort of data file, XML, JSON, plain text, or whatever. The problem with this is zero tie-in with the Unity editor itself! At least, not without a substantial amount of extra work. That might be acceptable after enough work, but ScriptableObjects provide a significantly better alternative!

The solution

ScriptableObject is a class, very much like the MonoBehaviour, without a lot of the component specific items. It gets serialized, saved, and loaded automatically through Unity, as well as standard inspector window support! You can easily spot Unity using ScriptableObjects in the Edit->Project Settings menu, and other similar locations. Unfortunately, Unity didn't make it quite as simple to use, you really won't find anything mentioned about ScriptableObjects in the UI.

So how do we use 'em? Lucky for us, it's relatively trivial~
Step 1. Inherit from ScriptableObject
Step 2. Treat it like a MonoBehaviour without events.
Step 3. Tie it into the editor.

Tying it into the editor can be fun, but here is where you get some handy-dandy code =D Unity treats ScriptableObjects the same way it does everything else, so you have to create an asset file, and add it to the file system!

The code

This is a little function I use that will create an asset for any ScriptableObject that I throw at it. I usually keep it around in a tiny little utility file I call SOUtil.cs (click here to download it!) It looks like this:
#if UNITY_EDITOR
    public static void CreateAsset(System.Type aType, string aBaseName) {
        ScriptableObject style = ScriptableObject.CreateInstance(aType);

        string path = UnityEditor.AssetDatabase.GetAssetPath(UnityEditor.Selection.activeInstanceID);
        if (System.IO.Path.GetExtension(path) != "") path = System.IO.Path.GetDirectoryName(path);
        if (path                              == "") path = "Assets";

        string name = path + "/" + aBaseName + ".asset";
        int    id   = 0;
        while (UnityEditor.AssetDatabase.LoadAssetAtPath(name, aType) != null) {
            id  += 1;
            name = path + "/" + aBaseName + id + ".asset";
        }

        UnityEditor.AssetDatabase.CreateAsset(style, name);
        UnityEditor.AssetDatabase.SaveAssets();

        UnityEditor.EditorUtility.FocusProjectWindow();
        UnityEditor.Selection    .activeObject = style;
    }
#endif

As you can see, this is a pretty darned generic function. It creates an object, finds a path for it, gives it a default name, saves it, and then sets it as the user's focus! One thing to keep in mind when working with ScriptableObject, you shouldn't create them using the 'new' keyword. Always use ScriptableObject.CreateInstance.

Ok, cool! That's the generic part, now, what about actually tying in specific objects? Well, here's a super simple object to give you a good idea about that.
public class SOString : ScriptableObject {
    public string stringValue = "";

#if UNITY_EDITOR
    const string editorMenuName = "String";
    [UnityEditor.MenuItem("GameObject/Create Scriptable Object/Create " + editorMenuName, false, 0  ), 
     UnityEditor.MenuItem("Assets/Create/Create Scriptable Object/"     + editorMenuName, false, 101)]
    public static void CreateAsset() {
        SOUtil.CreateAsset(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType, editorMenuName);
    }
#endif
}

Not that hard. A couple of menu tie-ins, for the GameObject menu, and the project window context menu, and then a call to our CreateAsset method, automatically picking type through reflection, and providing a default asset name! Now you can drag and drop these little suckers all around your inspectors, just like any other GameObject or MonoBehaviour!

Editor code is darned cool.
Enjoy =D