Making a custom editor tool for Unity3d

Hi,
In this article we will see how to create your own editor window inside Unity3d’s editor, to help you select any prefab in your project using a specific component.
Project view filter allows a lot of things but I never found a way to select all prefabs using a component from it, so why not just making it ?

To do so we will need to create an empty editor window in wich we will expose a way to select the targeted component we want to work on, then we will parse the project assets to find the matching elements and fill the current selection with those assets.

First we need to create a new script inside an “Editor” folder. I will name my script “AssetUpdater” because at the end I will not only use it as a selection tool. Placing the script in such folder will allow Unity to know this is an editor only script, this way it will be ignore when building your application, avoiding compilation issues and useless code to be embeded to your executable, smart.

Create a window

Our script needs to inherit from UnityEditor.EditorWindow, wich will allow us to open a new dockable window inside the editor. We also need an init method with a ContextMenu attribute to assign it a menu entry so we can open the window.

using UnityEngine;
using UnityEditor;

public class AssetUpdater : EditorWindow
{
    [MenuItem("Demo/Light Asset Updater %h")]
    static void Init()
    {
        EditorWindow window = GetWindow<LightAssetUpdater>("AssetUpdater");
    }

    public void OnGUI()
    {
        GUILayout.Label("Hello world");
    }
}
Our first hello world window

Noticed the “%h” at the end of the MenuItem attribute ? This will create a shortcut inside Unity. You can see the reminder while inside the menu itself on the right.
You can also edit shortcuts with the dedicated shortcuts window : Edit > Shortcuts

Exposing fields

Now we need to find a way to select the component we want to filter. Thanks to the assembly we can retrieve any kind of component we want but that’s too much to expose if we include builtin components. So let’s expose a text filter first

public void OnGUI()
{
	EditorGUI.BeginChangeCheck();

	m_typeFilter = EditorGUILayout.TextField("Type filter", m_typeFilter);

	if (EditorGUI.EndChangeCheck())
	{
		GetTypeCandidates(m_typeFilter);
	}
}

private void GetTypeCandidates( string _filter = "" )
{
}

Using the “BeginChangeCheck” and “EndChangeCheck” allow us to make sure the process of GetTypeCandidates won’t be executed every frame as it could leads so useless computations, this way it will reprocess everytime we change the textfield value but no more.

Getting types from assembly

We need to retrieve all types that match the filter, to to this we will write the GetTypeCandidate() method to parse the current assembly looking for any type inheriting from MonoBehaviour, wich is not abstract and match our filter.

private List<System.Type> m_typeCandidates = new List<System.Type>();

private const int MAX_CANDIDATES_COUNT = 6;

private void GetTypeCandidates( string _filter )
{
    m_typeCandidates.Clear();   // First make sure list is empty before starting

    if( !string.IsNullOrEmpty( _filter ) )  // No filter would lead to too much results and would be useless
    {
        _filter = _filter.ToLower();    // Lower filter once to ignore case later

        System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies();
        foreach ( System.Reflection.Assembly assembly in assemblies )
        {
            System.Type[] candidates = assembly.GetTypes();
            foreach( System.Type type in candidates )
            {
                if ( type.IsSubclassOf(typeof(MonoBehaviour) )
                    && !type.IsAbstract
                    && type.Name.ToLower().Contains(_filter) )
                {
                    m_typeCandidates.Add(type);

                    if ( m_typeCandidates.Count >= MAX_CANDIDATES_COUNT )
                        break; // Don't fill the list with too much data
                }
            }
        }
    }
}

Now we have the candidates, let’s select the good one.

private string m_typeFilter = string.Empty;
private List<System.Type> m_typeCandidates = new List<System.Type>();
private System.Type m_typeSelected = null;

public void OnGUI()
{
    EditorGUILayout.Space();

    #region Type Filter
    if ( m_typeSelected == null )
    {
        EditorGUI.BeginChangeCheck();

        m_typeFilter = EditorGUILayout.TextField("Type filter", m_typeFilter);

        if ( EditorGUI.EndChangeCheck() )
        {
            GetTypeCandidates(m_typeFilter);
        }

        for ( int i = 0; i < m_typeCandidates.Count; i++ )
        {
            if ( GUILayout.Button(m_typeCandidates[i].Name) )
            {
                m_typeSelected = m_typeCandidates[i];
                m_typeFilter = string.Empty;
                m_typeCandidates.Clear();
                break;  // Don't forget to exit the loop as we just changed the list we are iterating on
            }
        }
    }
    else
    {
        EditorGUILayout.BeginHorizontal();
        EditorGUILayout.LabelField("Selected component ", m_typeSelected.Name);
        EditorGUILayout.Space();

        if (GUILayout.Button("Clear"))
        {
            m_typeFilter = string.Empty;
            m_typeSelected = null;
        }

        EditorGUILayout.EndHorizontal();
    }
    #endregion Type Filter
}

AssetDatabase search

Let’s add a button to trigger the selection at the end of our OnGUI method and use the UnityEditor.AssetDataBase class to retrieve our targets :

 public void OnGUI()
{
    #region Type Filter
    ...
    #endregion Type Filter

    EditorGUILayout.Space();
    
    if ( GUILayout.Button("Select all") )
    {
        SelectAll();
    }
}

private void SelectAll()
{
    List<GameObject> selection = new List<GameObject>();
    string[] GUIDs = AssetDatabase.FindAssets("t:Prefab");
    foreach ( string guid in GUIDs )
    {
        string assetPath = AssetDatabase.GUIDToAssetPath(guid);
        if ( !string.IsNullOrEmpty(assetPath) )
        {
            GameObject target = AssetDatabase.LoadAssetAtPath(assetPath, typeof(GameObject)) as GameObject;
            if (target)
            {
                if( m_typeSelected == null || target.GetComponent(m_typeSelected) != null )
                    selection.Add(target);
            }
        }
    }

    // Apply current selection as main Unity's selection
    Selection.objects = selection.ToArray();
}

Now we have a selection of our prefabs matching our filter criteria, great ! but there some little annoying things on this window :

  • The project window doesn’t display wich elements are selected making it unsafe to use or large dataset
  • If you close the window or recompile, you will lost your selection
  • You can filter by component, but you lost the ability to filter by name or label

Let’s fix this !

Display current selection

We can use multiple way to display the selection. If decided to make the list not editable but to be able to reach any item independantly I used ObjectFields inside a scrollview allowing the display of a lot of elements

private GameObject[] m_selection;

public void OnGUI()
{
        ...

        m_scrollPosition = EditorGUILayout.BeginScrollView(m_scrollPosition);
        EditorGUI.BeginDisabledGroup(true);
        {
            if (m_selection != null && Selection.objects.Length > 0)
            {
                foreach (GameObject target in Selection.objects)
                {
                    EditorGUILayout.ObjectField(target, typeof(GameObject), false);
                }
            }
        }
        EditorGUI.EndDisabledGroup();
        EditorGUILayout.EndScrollView();
}

private void SelectAll( string _filter = "" )
{
    ...

    // Apply current selection as main Unity's selection
    Selection.objects = m_selection = selection.ToArray();
}

By doing so clicking on any element displayed will highlight the asset in the ProjectWindow but will not change the current selection itselft. Very useful when you want to target only some elements from the list.

Use EditorPrefs to save your setting

The same way PlayerPrefs can be used to save you game data in build, Unity3d offers an EditorPrefs class wich could be very usefull to memorize some settings. Let’s memorize the selected type.

private const string EDITORPREFS_TYPESELECTED = "AssetUpdater.TypeSelected";

public void OnEnable()
{
    if( EditorPrefs.HasKey(EDITORPREFS_TYPESELECTED) )
    {
        m_typeSelected = System.Type.GetType(EditorPrefs.GetString(EDITORPREFS_TYPESELECTED));
    }
}

Then just add this line when the type selection button is pressed

EditorPrefs.SetString( EDITORPREFS_TYPESELECTED, m_typeSelected.AssemblyQualifiedName );

This way closing window or recompile won’t lost track of your settings anymore !

Restore filter abilities

You may have noticed that AssetDatabase.FindAsset() method has several options. We first decided to pass it “t:prefab” parameter to make sure only prefabs objects will be parsed. But as this works exactly as the filter field you have in the project window we can use it the same way. Just adding a new textfield and passing the parameter will bring us back the full filter feature.

public void OnGUI()
{
    ...

    EditorGUI.BeginChangeCheck();
    m_selectionFilter = EditorGUILayout.DelayedTextField( "Selection Filter", m_selectionFilter );
    if ( EditorGUI.EndChangeCheck() || GUILayout.Button("Select all") )
    {
        SelectAll(m_selectionFilter);
    }
}
        
private void SelectAll(string _filter = "")
{
    List<GameObject> selection = new List<GameObject>();
    string[] GUIDs = AssetDatabase.FindAssets("t:Prefab " + _filter);
    foreach (string guid in GUIDs)
    {
        string assetPath = AssetDatabase.GUIDToAssetPath(guid);
        if (!string.IsNullOrEmpty(assetPath))
        {
            GameObject target = AssetDatabase.LoadAssetAtPath(assetPath, typeof(GameObject)) as GameObject;
            if (target)
            {
                if (m_typeSelected == null || target.GetComponent(m_typeSelected) != null)
                    selection.Add(target);
            }
        }
    }

    // Apply current selection as main Unity's selection
    Selection.objects = selection.ToArray();
}

Notice the use of “EditorGUILayout.DelayedTextField” here. This is a variant of the previous textfield, except to avoid parsing the whole asset database at every character change this one will triggers only when the field lost focus or when the user press enter. Fast and easy !

Go futher

Well, we now have a custom editor window to find all our components in any asset from the project database working, but there’s some improvements we can still do later :

  • Call a method on each object
  • Handle prefabs variants
  • Handle Version control

This window can be a start for any asset database automation tool you need, have fun with it, and thanks for reading.

Download on GitHub

I created a GitHub repository where you can access the complete source code : https://github.com/Kookyoo/AssetUpdater