technical architecture.

Unity: toying with Scriptable Objects

by on Oct.19, 2010, under Architecture, csharp, Scripting, Unity3D

In architecture there is often a need to keep track of and have handy many different viewpoints of a model at a time. with my early unity experiments, I made the mistake of creating a Camera for every Viewpoint that I wanted to have available in my scene. this was faulty for several reasons, not the least of which was that every ‘Camera’ object in a scene is actually rendering everything in the scene at once. Each camera has a few fun/useful options in the Clear Flags that makes this useful for things like rendering UI overlays with 3d objects and other assorted multipass ideas, but thats a whole ‘nother story.

Next attempt was to just import a large list of dummy objects in from 3dsmax that would represent camera orientations and allow the end user to select from these dummys and align the camera with the dummy objects using some UI bits in unity. This worked fine, but there is the base problem that the user cannot modify/edit these objects easily and have the changes save across play sessions.

This is where Scriptable Objects started to come in to play. (Taken partly from the unity Character Demo where they were explaining some of the joys of AssetBundles if you wish to look up more in depth examples.) this may not be the most useful demo of Scriptable objects, but this is a case study for when you are trying to store Runtime data for use at editortime.

There are several parts: First and probably the only important part is the Holder, this is really just a list of ‘Somethings’ that we save to disk. in our case, CameraLocations.

//CameraLocationHolder.cs
using System.Collections.Generic;
using UnityEngine;
 
public class CameraLocationHolder : ScriptableObject {
    public List<CameraLocation> content;
    public CameraLocationHolder(List content)
    {
        this.content = content;
    }
}

the above empty list consists of ‘CameraLocation’s which is just a simple class I used to store a few variables that are needed to re-create a Viewpoint. Camera locations is a Serializable Class (Serializable in this case means that its viewable in the inspector in Unity the same way you would view for instance a Vector3..) and it has some default values in there of pulling the maincameras location/rotation and field of view.

//CameraLocation.cs
using UnityEngine;
using System.Collections;
 
[System.Serializable]
public class CameraLocation
{
    public string name = "New Camera";
    public float fov = Camera.main.fov;
    public Vector3 position = Camera.main.transform.position;
    public Quaternion rotation = Camera.main.transform.rotation;
}

Now, we create an asset in the editor where we will store our list of CameraLocation’s, this can be placed anywhere, its a small file so i tend to just store it in Resources for easy finding later.

//CreateCameraLocationAsset.cs
using UnityEngine;
using UnityEditor;
using System;
using System.Collections.Generic;
 
public class CreateCameraLocationAsset
{
    [MenuItem("Custom/Cameras/Create camera location holder")]
    public static void CreateMyAsset()
    {
        ListList<CameraLocation> content = new List<CameraLocation>();
        CameraLocationHolder asset = new CameraLocationHolder(content);  //scriptable object 
        AssetDatabase.CreateAsset(asset, "Assets/CameraLocationDatabase.asset");
        AssetDatabase.SaveAssets();
        EditorUtility.FocusProjectWindow();
        Selection.activeObject = asset;
    }
}

Now you have an ‘Asset’ where you can store/load/erase/whatever in a list of CameraLocations at runtime, and the results will still be available at Editor time, and at future Runs of the application.

For instance, you can browse around your scene as you would normally with your FlyingCameraScriptOfDoom, and occasionally call a script to add your current camera.main viewpoint to the list.

or accessing it as a runtime resource for use by a normal GUI pulldown:

//Excerpts from CameraListBox.cs
 
//declaring a list of stuff
            public List<CameraLocation> camList = new List<CameraLocation>(); //Declare our list of stuff
 
//....
 
//somewhere in Start() we load the list from the DB
            Object o = Resources.Load("CameraLocationDatabase", typeof(CameraLocationHolder));
            CameraLocationHolder CameraLocationDB = (CameraLocationHolder)o;
//fill our list of stuff, with the content from the Holder in the asset
            camList = CameraLocationDB.content;
 
//....
 
//and somewhere in our OnGUI()
            GUILayout.BeginVertical(GUILayout.Width(120));
            for (int i = 0; i &lt; camList.Count; i++)
            {
                if (GUILayout.Button(camList[i].name))
                {
                    SelectedListItem = i;//Set the index for our currrently selected item
                    CameraLocation cam = camList[SelectedListItem];
                    StartCoroutine(SmoothMoveCamLoc(cam.position, cam.rotation, cam.fov, duration));
                   }
            }
            GUILayout.EndVertical();
 
//the SmoothMoveCamLoc just lerps between the current positions/rotations/fov's 
//and the desired ones that were stored in the CameraLocation.
//although mine has a few other random logic items in there to decide
//which style of camera i have currently and deal with any parents/targets/initializing/etc
//needed if we move the camera.

For instance, editing the list by an Editor script, that builds a list for you to Add/Remove/Rename/and Re-Order the list, because thats oddly hard to do with the built in object interface? Maybe this is where a custom inspector should be for the .asset type? hmm! things to play with in the future.

//CameraListboxWizard.cs
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
 
public class CameraListboxWizard : ScriptableWizard
{
    public string DBLocation = "Assets/Resources/CameraLocationDatabase.asset";
    private CameraLocation camLocation;
    static private Object dbaseAsset;
    static private CameraLocationHolder db;
 
    [MenuItem("Custom/Cameras/CameraListBox - Database Helper")]
    static void DoSet()
    {
        ScriptableWizard.DisplayWizard("Add view to CameraListBox", typeof(CameraListboxWizard), "DONE");
    }
 
    void OnWizardUpdate()
    {
        helpString = "This will control the Camera Location asset database\n, and allow you to 'save' the Camera.main's position\n while in the player mode\n";
 
        UpdateMyAssetLocation();
    }
 
    void AddExistingCameras()
    {
        int count = Camera.allCameras.Length;
        for (int i = 0; i &lt; count; ++i)
        {
            //except for the main camera, which we'll use to transition between the others
            if (Camera.allCameras[i] != Camera.main)
            {
                camLocation = new CameraLocation();
                camLocation.name = Camera.allCameras[i].name;
                camLocation.fov = Camera.allCameras[i].fov;
                camLocation.position = Camera.allCameras[i].transform.position;
                camLocation.rotation = Camera.allCameras[i].transform.rotation;
                AddCamera(camLocation);
            }
        }
    }
 
    void OnWizardCreate()
    {
        Debug.Log("done");
    }
 
    void AddCamera(CameraLocation cam)
    {
        db.content.Add(cam);
        EditorUtility.SetDirty(dbaseAsset);
    }
 
    void OnGUI()
    {
        EditorGUILayout.BeginVertical();
         EditorGUILayout.BeginHorizontal();
        GUILayout.Label(name, GUILayout.MaxWidth(0));
        DBLocation = EditorGUILayout.TextField(DBLocation);
        if (GUILayout.Button("Browse"))
        {
            DBLocation = EditorUtility.OpenFilePanel(name, DBLocation, "asset");
            UpdateMyAssetLocation();
        }
        EditorGUILayout.EndHorizontal();
 
        if (GUILayout.Button("Update Database Location"))
        {
            UpdateMyAssetLocation();
        }
 
        EditorGUILayout.Space();
        if (GUILayout.Button("Add current Camera.main position", GUILayout.Height(50)))
        {
            camLocation = new CameraLocation();
            AddCamera(camLocation);
        }
        EditorGUILayout.Space();
        DisplayCurrentCamList();
        EditorGUILayout.Space();
 
        if (GUILayout.Button("Add all existing 'Cameras' to the list\n this can 'import' max camera locations", GUILayout.Height(50)))
        {
            AddExistingCameras();
        }
        EditorGUILayout.EndVertical();
    }
 
    void DisplayCurrentCamList()
    {
        for (int i = 0; i &lt; db.content.Count; i++ )
        {
            EditorGUILayout.BeginHorizontal();
            db.content[i].name = GUILayout.TextField(db.content[i].name, GUILayout.Width(200));
            if (GUILayout.Button("UP"))
            {
                if (i &gt; 0)
                {
                    CameraLocation item = db.content[i];
                    db.content.RemoveAt(i);
                    db.content.Insert(i - 1, item);
                }
            }
            if (GUILayout.Button("DN"))
            {
                if (i &lt; db.content.Count)
                {
                    CameraLocation item = db.content[i];
                    db.content.RemoveAt(i);
                    db.content.Insert(i + 1, item);
                }
            }
            if (GUILayout.Button("Remove"))
            {
                db.content.Remove(db.content[i]);
            }
            //db.SetDirty();
            EditorUtility.SetDirty(dbaseAsset);
            EditorGUILayout.EndHorizontal();
        }
    }
 
    public void UpdateMyAssetLocation()
    {
        dbaseAsset = AssetDatabase.LoadAssetAtPath(DBLocation, typeof(CameraLocationHolder));
        CameraLocationHolder CameraLocationDB = (CameraLocationHolder)dbaseAsset;
        db = CameraLocationDB;
    }
    public void CreateMyAsset()
    {
        List content = new List();
        CameraLocationHolder asset = new CameraLocationHolder(content);  //scriptable object 
        AssetDatabase.CreateAsset(asset, "CameraLocationDatabase");
        AssetDatabase.SaveAssets();
        EditorUtility.FocusProjectWindow();
        Selection.activeObject = asset;
    }
}

10 Comments for this entry

  • Sandeep Gadhvi

    hey Dave, many thanks to you for explaining this in detail. As part of editing a Unity project I came across a complex code which creates .asset files by reading xmls,which contains points on a png image, while surfing I found about scriptable object and then this blog. It has increased my understanding of this issue. Thanks a log.

  • Nick Wiggill

    Bookmarked. After looking for hours, I found this post, and you described exactly what I need:

    “Now you have an ‘Asset’ where you can store/load/erase/whatever in a list of CameraLocations at runtime, and the results will still be available at Editor time, and at future Runs of the application.”

    Couldn’t believe my luck. Thanks again!

  • Awesome

    Finally i understand ScriptableObject, thank you very much.

  • Martin

    This is incredibly useful, but I’m a little confused as to how you’ve created a list with no type. Or is this simply being used an an example, and the type (in this instance) would be List content?

  • Martin

    Ah, it seems to omit content within greater than and less than symbols, so that was meant to come out:
    List (less than) CameraLocation (Greater than) content.

  • Dave Buchhofer

    Hey Martin,

    Thanks, I hadn’t realized that the <'s had gotten stripped at some point?!

  • Colton

    I have been struggling for two full days trying to get my custom Unity editor working like I needed it to. I have to say, you Mr. Buchhofer have just saved the day for me. This was exactly what I needed and it works perfectly. You sir, deserve a medal!

  • Jarmo

    Thank you for very informative block post. Could you, please, state a license for re-usage of the code? MIT license would be nice, however, you are free to choose another one.

    All the best

  • Dave Buchhofer

    Free to use however you wish!

  • Nikolaos Patsiouras

    In CreateCameraLocationAsset.CreateMyAsset you have List written twice on the type of the CameraLocation List.

2 Trackbacks / Pingbacks for this entry

Leave a Reply

Looking for something?

Use the form below to search the site:

Still not finding what you're looking for? Drop a comment on a post or contact us so we can take care of it!