some wise words
Want to be notified of new blog posts?


Elwin Verploegen

Tutorial: Extending The Unity3D Editor Even MoreDecember 22, 2014

By Elwin

Last time we made a custom window and got it to interact with a GameObject. This time we’ll go a bit further and start implementing some more features there. Our starting off point will be the previous tutorial, so be sure to check that out first (over here).

Chaining events

Something that’s particularly useful for Fragments of Him, is the chaining of certain events after the previous one triggered. Keeping track of them however is a massive pain. We’ll add some functionality to our DataHolder to allow for chaining and then work all of that into our custom window to make it look all fancy. Let’s start by adding some simple functionality to allow for chaining in the DataHolder.

using UnityEngine;
using System.Collections;

public class DataHolder : MonoBehaviour {
    	public DataHolder previous;
    	public DataHolder next;
    	public bool enableonload = false;
}

We’ll be linking interactions together by dragging DataHolders into the previous and next variables. The enableonload boolean is used to make sure you can tag which interaction is the first one. We’ve removed the other variables, because we don’t need those anymore (they were just there for show). Let’s make sure the new variables show up in the editor.

using UnityEditor;
using UnityEngine;
using System.Collections;

    public class ObjectSetter : EditorWindow {

    public static ObjectSetter window;
    private static GameObject obj;

    [MenuItem ("Tools/ObjectSetter")]
    public static void OpenWindow(){
        window = (ObjectSetter)EditorWindow.GetWindow(typeof(ObjectSetter));  //create a window
        window.title = "Object Setter";	//set a window title
    }

    void OnGUI(){
        if(window == null)
            OpenWindow();

        if(Selection.activeGameObject != null){
            //gets the object you currently have selected in the scene view
            obj = Selection.activeGameObject;
            GUI.Label(new Rect(5, 5, position.width-10, 25), "Current selected object: " + obj.name);

            //make sure to only show the interface 
            DataHolder comp = obj.GetComponent<DataHolder>();
            if(comp != null){
                comp.next = (DataHolder) EditorGUI.ObjectField(
                    new Rect(5, 50, position.width-10, 16),
                    "Next Object",
                    comp.next, typeof(DataHolder), true
                );

                comp.enableonload = EditorGUI.Toggle(
                    new Rect(5, 70, position.width-10, 16), 
                    "Enable On Load", 
                    comp.enableonload
                );
            }
            else{
                if(GUI.Button(new Rect(5, 30, position.width-10, position.height-40), 
                    "Add DataHolder")){
                    obj.AddComponent<DataHolder>();
                }
            }
        }
    }

    void Update(){
        Repaint();
    }
}

The added code also uses the EditorGUI.ObjectField, but instead of using a GameObject type it will use a DataHolder type. Whenever you drag in a GameObject from the scene into that field it will automatically grab the DataHolder component. We also added an EditorGUI.Toggle, which does uses the same formatting as pretty much any other EditorGUI field. This uses a boolean to create a toggle box.

Displaying the chain

Next up we’ll want to show how all of these link to each other. We’ll start by adding this to the editor in a simple list.

using UnityEditor;
using UnityEngine;
using System.Collections;

    public class ObjectSetter : EditorWindow {

    public static ObjectSetter window;
    private static GameObject obj;

    [MenuItem ("Tools/ObjectSetter")]
    public static void OpenWindow(){
        window = (ObjectSetter)EditorWindow.GetWindow(typeof(ObjectSetter));  //create a window
        window.title = "Object Setter";	//set a window title
    }

    void OnGUI(){
        if(window == null)
            OpenWindow();

        if(Selection.activeGameObject != null){
            //gets the object you currently have selected in the scene view
            obj = Selection.activeGameObject;
            GUI.Label(new Rect(5, 5, position.width-10, 25), "Current selected object: " + obj.name);

            //make sure to only show the interface 
            DataHolder comp = obj.GetComponent<DataHolder>();
            if(comp != null){
                comp.next = (DataHolder) EditorGUI.ObjectField(
                    new Rect(5, 50, position.width-10, 16),
                    "Next Object",
                    comp.next, typeof(DataHolder), true
                );

                comp.enableonload = EditorGUI.Toggle(
                    new Rect(5, 70, position.width-10, 16), 
                    "Enable On Load", 
                    comp.enableonload
                );
            }
            else{
                if(GUI.Button(new Rect(5, 30, position.width-10, 70), 
                    "Add DataHolder")){
                    obj.AddComponent<DataHolder>();
                }
            }
        }

        int i = 0;
        foreach(DataHolder dataobj in GameObject.FindObjectsOfType(typeof(DataHolder))){
            if(GUI.Button(new Rect(5, 110 + i * 20, position.width-10, 16), dataobj.gameObject.name)){
                Selection.activeGameObject = dataobj.gameObject;
            }
            i++;
        }
    }

    void Update(){
        Repaint();
    }
}

What we’re doing here is looping through all DataHolder components in the current scene, this is done using GameObject.FindObjectsOfType. We then create a button for each of those. Whenever you click on one of these buttons, we use the previously used Selection.activeGameObject to set the currently selected GameObject. We will also change the “Add DataHolder” button to be smaller, as to not obstruct the list.

Automating the previous variable

We still have that previous variable in the DataHolder component, and we could manually set that variable in the editor window if we wanted to, but that defeats the purpose of writing custom tools. So let’s automate that. This is how my current hierarchy looks (nothing changed in the project panel so far):

Project Hierarchy

Project Hierarchy

Each of those cubes are the same as the one we made in the previous tutorial, except that they were renamed for usability.

using UnityEditor;
using UnityEngine;
using System.Collections;

    public class ObjectSetter : EditorWindow {

    public static ObjectSetter window;
    private static GameObject obj;
    private static DataHolder data;

    [MenuItem ("Tools/ObjectSetter")]
    public static void OpenWindow(){
        window = (ObjectSetter)EditorWindow.GetWindow(typeof(ObjectSetter));  //create a window
        window.title = "Object Setter";	//set a window title
    }

    void OnGUI(){
        if(window == null)
            OpenWindow();

        if(Selection.activeGameObject != null){
            //gets the object you currently have selected in the scene view
            obj = Selection.activeGameObject;
            GUI.Label(new Rect(5, 5, position.width-10, 25), "Current selected object: " + obj.name);

            //make sure to only show the interface 
            data = obj.GetComponent<DataHolder>();
            if(data != null){
                DataHolder tmpdata = data.next;
                EditorGUI.BeginChangeCheck();
                data.next = (DataHolder) EditorGUI.ObjectField(
                    new Rect(5, 50, position.width-10, 16),
                    "Next Object",
                    data.next, typeof(DataHolder), true
                );
                if(EditorGUI.EndChangeCheck()){
                    if(data.next != null){
                        data.next.previous = obj.GetComponent<DataHolder>();
                    }
                    else{
                        tmpdata.previous = null;
                    }
                }

                data.enableonload = EditorGUI.Toggle(
                    new Rect(5, 70, position.width-10, 16), 
                    "Enable On Load", 
                    data.enableonload
                );
             }
            else{
                if(GUI.Button(new Rect(5, 30, position.width-10, 70), 
                    "Add DataHolder")){
                    obj.AddComponent<DataHolder>();
                }
            }

            int i = 0;
            foreach(DataHolder dataobj in GameObject.FindObjectsOfType(typeof(DataHolder))){
                if(GUI.Button(new Rect(5, 110 + i * 20, position.width-10, 16), 
                    dataobj.gameObject.name)){
                    Selection.activeGameObject = dataobj.gameObject;
                }
                i++;
            }
        }        
    }

    void Update(){
        Repaint();
    }
}

The main thing we added here is the EditorGUI.BeginChangeCheck function, one of the very useful functions that you have access from with the EditorGUI (you could easily manually create the same functionality, but this makes your code more readable). The BeginChangeCheck function will check if anything changed in the GUI between the BeginChangeCheck and EndChangeCheck, if it does it will return true.

We use this to start setting the previous variable. If the next variable isn’t set we will set the previous variable to null, this will be used when the user removes the next variable from the currently selected object. To do so we have to keep a reference of data.next, this would otherwise give you an error when you try to try to set the previous variable of that to null. When the next variable is set to something, it will set the previous variable to the currently selected object.

It’s important to point out here that we’ve also added a variable called “data” at the top of the script. Just like obj, this will keep track of the currently selected DataHolder.

I would like to point out an alternative way to creating the if – else statement. This is more concise and may or may not be easier for you to read. Remember that both of these statements will do the exact same thing:

if(comp.next != null)
    comp.previous = obj.GetComponent();
else 
    comp.previous = null;

--------------------------------------------------------

comp.previous = (comp.next != null) ? obj.GetComponent<DataHolder>() : null;

You could (and probably should) add additional checks to see if, for example, the user isn’t trying to set the next variable to itself (which could potentially crash the system). Let’s also highlight the button that’s next & previous in queue when you have an object selected, this makes it easier for the user to see the flow of the components. and allows you to quickly click through interactions to see what’s happening.

using UnityEditor;
using UnityEngine;
using System.Collections;

    public class ObjectSetter : EditorWindow {

    public static ObjectSetter window;
    private static GameObject obj;
    private static DataHolder data;

    [MenuItem ("Tools/ObjectSetter")]
    public static void OpenWindow(){
        window = (ObjectSetter)EditorWindow.GetWindow(typeof(ObjectSetter));  //create a window
        window.title = "Object Setter";	//set a window title
    }

    void OnGUI(){
        if(window == null)
            OpenWindow();

        if(Selection.activeGameObject != null){
            //gets the object you currently have selected in the scene view
            obj = Selection.activeGameObject;
            GUI.Label(new Rect(5, 5, position.width-10, 25), "Current selected object: " + obj.name);

            //make sure to only show the interface 
            data = obj.GetComponent<DataHolder>();
            if(data != null){
                DataHolder tmpdata = data.next;
                EditorGUI.BeginChangeCheck();
                data.next = (DataHolder) EditorGUI.ObjectField(
                    new Rect(5, 50, position.width-10, 16),
                    "Next Object",
                    data.next, typeof(DataHolder), true
                );
                if(EditorGUI.EndChangeCheck()){
                    if(data.next != null){
                        data.next.previous = obj.GetComponent<DataHolder>();
                    }
                    else{
                        tmpdata.previous = null;
                    }
                }

                data.enableonload = EditorGUI.Toggle(
                    new Rect(5, 70, position.width-10, 16), 
                    "Enable On Load", 
                    data.enableonload
                );
             }
            else{
                if(GUI.Button(new Rect(5, 30, position.width-10, 70), 
                    "Add DataHolder")){
                    obj.AddComponent<DataHolder>();
                }
            }

            int i = 0;
            foreach(DataHolder dataobj in GameObject.FindObjectsOfType(typeof(DataHolder))){
                 if(data != null){
                    if(data.previous != null && data.previous == dataobj){
                        GUI.backgroundColor = new Color(1f, 0f, 0f);
                    }
                    else if(data.next != null && data.next == dataobj){
                        GUI.backgroundColor = new Color(0f, 1f, 0f);
                    }
                    else if(dataobj == data){
                        GUI.backgroundColor = new Color(0f, 0f, 0f);
                    }
                }

                if(GUI.Button(new Rect(5, 110 + i * 20, position.width-10, 16), 
                    dataobj.gameObject.name)){
                    Selection.activeGameObject = dataobj.gameObject;
                }

                GUI.backgroundColor = new Color(1f, 1f, 1f);
                i++;
            }
        }        
    }

    void Update(){
        Repaint();
    }
}

Time to give the user some feedback! We’ve added some code that changes the background colour of the GUI using GUI.backgroundColor. Whenever the previous DataHolder is found, the colour of the button will become red. Whenever it is the same as the next variable it will turn green, when the loop hits the currently selected object, it will make that button black. At the end of each cycle it will reset the background color back to white (if you don’t do this, every button afterwards will remain the colour of whatever you last set the GUI.backgroundColor to).

You should end up with the following window:

Final Editor Window

Final Editor Window

In this case Cube 1 links to Cube 2 and Cube 2 links to Cube 3. If Cube 2 is selected the above window should look like the custom editor window.

If you have any questions or remarks, don’t hesitate to get in touch with me via Twitter.

Want to be notified of new blog posts?