Search code examples
c#unity-game-engineunity-editor

Shortcut to automatically add custom editor for Script in Unity


The Unity context menu for Assets has all kinds of useful shortcuts, but there is no shortcut to quickly create a custom editor. Instead, if I want to create a custom editor, I have to go through all these annoying boilerplate steps:

  1. Create Editor directory (if not created yet), and go to the right sub-directory
  2. Create -> C# Script (which should actually be called C# Component Script)
  3. Remove all the MonoBehavior stuff
  4. Append Editor to the class name
  5. Add using UnityEditor
  6. Add the CustomEditor attribute
  7. Inherit from Editor

...before being able to actually start writing my stuff...

Is there a shortcut for that somewhere?


Solution

  • New "Add Custom Editor" MenuItem

    I wrote this little script which adds a new Add Custom Editor MenuItem to the Assets context menu:

    new MenuItem

    Usage

    1. Right-click a Component script in the Scripts folder.
    2. Select Add Custom Editor.
    3. The component now has an Editor which gets automatically selected, so you can modify it as you like. It has the same name (with Editor appended) and is at the same relative path, but in the Scripts/Editor folder

    The screenshots below show an example: Given script Scripts/Test/TestScript, it creates a new editor at Scripts/Editor/Test/TestScriptEditor.

    NOTE: The MenuItem will be disabled unless you have selected a Script that:

    1. Has a .cs file ending
    2. Is located somewhere under the Scripts folder (can be nested in any sub-directory)
    3. Is not in the Editor folder
    4. Does not have an editor yet

    after using it

    Setup

    1. Create -> C# Script
    2. Call it AddCustomEditorMenuItem
    3. Replace its contents with the code from this gist
    4. Done!
    5. Test it: Right-click a script file in the Scripts directory.

    Code Highlights

    • Figuring out all the paths took most of the time:
    scriptName = scriptAsset.name;
    
    // get system file path
    scriptPath = Path.GetFullPath(ProjectRoot + AssetDatabase.GetAssetPath (scriptAsset));
    
    // get file name of the editor file
    editorFileName = GetEditorFileNameFor (scriptName);
    
    // split the script path
    var results = scriptPathRegex.Matches (scriptPath).GetEnumerator ();
    results.MoveNext ();
    var match = (Match)results.Current;
    scriptsPath = match.Groups [1].Value;
    scriptRelativePath = match.Groups [2].Value;
    
    // re-combine editor path
    editorPath = Path.Combine (scriptsPath, "Editor");
    editorPath = Path.Combine (editorPath, scriptRelativePath);
    editorPath = Path.Combine (editorPath, editorFileName);
    
    // nicely formatted file path
    editorPath = Path.GetFullPath(editorPath);
    editorRelativeAssetPath = editorPath.Substring(ProjectRoot.Length);
    
    • Once, paths are figured out, actually writing the file is nice and easy!
    public void WriteCustomEditorFile ()
    {
      // create all missing directories in the hierarchy
      Directory.CreateDirectory (Path.GetDirectoryName (editorPath));
    
      // write file
      File.WriteAllText (editorPath, BuildCustomEditorCode(scriptName));
    
      // let Asset DB pick up the new file
      AssetDatabase.Refresh();
    
      // highlight in GUI
      var os = AssetDatabase.LoadAllAssetsAtPath(editorRelativeAssetPath);
      EditorGUIUtility.PingObject (os[0]);
    
      // log
      Debug.Log("Created new custom Editor at: " + editorRelativeAssetPath);
    }
    
    // ...
    
    /// <summary>
    /// The menu item entry
    /// </summary>
    [MenuItem ("Assets/Add Custom Editor %#e", false, 0)]
    public static void AddCustomEditor ()
    {
      var scriptAsset = Selection.activeObject;
    
      // figure out paths
      var scriptPathInfo = new ScriptPathInfo (scriptAsset);
    
      // write file
      scriptPathInfo.WriteCustomEditorFile ();
    }
    
    • If you don't like the default contents of a newly created editor, feel free to edit this part:
    static string BuildCustomEditorCode (string name)
    {
      return @"using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEditor;
    [CustomEditor(typeof(" + name + @"))]
    public class " + name + @"Editor : Editor {
    
    public override void OnInspectorGUI ()
    {
      base.OnInspectorGUI ();
      var obj = (" + name + @") target;
      if (GUILayout.Button (""Hi!"")) {
        // do something with obj when button is clicked
        Debug.Log(""Button pressed for: "" + obj.name);
        EditorGUIUtility.PingObject (obj);
      }
    }
    }";
    }
    
    • If your menu item is always grayed out, consider first checking out my explanations above, before debugging the code that determines if a valid script is selected:
    [MenuItem ("Assets/Add Custom Editor %#e", true, 0)]
    public static bool ValidateAddCustomEditor ()
    {
      var scriptAsset = Selection.activeObject;
    
      if (scriptAsset == null) {
        // nothing selected? (should probably not happen)
        return false;
      }
    
      var path = ProjectRoot + AssetDatabase.GetAssetPath (scriptAsset);
    
      if (!scriptPathRegex.IsMatch (path)) {
        // not a Script in the Script folder
        return false;
      }
    
      if (editorScriptPathRegex.IsMatch (path)) {
        // we are not interested in Editor scripts
        return false;
      }
        
    
      if (Directory.Exists (path)) {
        // it's a directory, but we want a file
        return false;
      }
    
      var scriptPathInfo = new ScriptPathInfo (scriptAsset);
    
      //        Debug.Log (scriptPathInfo.scriptPath);
      //        Debug.Log (Path.GetFullPath(AssetsPath + "/../"));
      //        Debug.Log (scriptPathInfo.editorRelativeAssetPath);
      //        Debug.Log (scriptPathInfo.editorPath);
    
      if (File.Exists (scriptPathInfo.editorPath)) {
        // editor has already been created
        return false;
      }
    
      // all good!
      return true;
    }