目录

前言

P1

P2


前言

本文将简单介绍UnityEditor扩展的知识内容与基本操作,将会持续更新未来使用或可能使用到的一些编辑器扩展内容。

扩展编辑器的方法

  • MenuItem 与 EditorWindow
  • OnDrawGizmos
  • OnInspectorGUI
  • OnSceneGUI
  • ScriptableWizard
  • ScriptObject
  • Attributes
  • AssetProcess


P1扩展编辑器的方法

UnityEditor相关类

  • Editor:编辑器
  • EditorWindow :编辑窗口(类似Console就是一个编辑窗口),其中初始化方法应为static,否则在Unity编辑过程中不能找到匹配的选项
  • GenericMenu :编辑窗口中菜单
  • EditorGUILayout :编辑窗口中的界面布局类
  • MenuItem属性:添加菜单项在控制面板主菜单和检视面板上下文菜单
  • GUILayout : 可以在编辑窗口用
  • MenuCommand :用于提取上下文菜单项。MenuCommand对象被传递给自定义菜单项功能使用菜单项定义的属性。
  • Event : 事件,Event.current.可以获取到当前的系统处理的事件

MenuItem

在using UnityEditor里

/*
     * static void MenuItem (itemName : string, isValidateFunction : bool,  priority : int) : MenuItem
     * itemName : 菜单名,以"/"分级,第一个是和主菜单同级的位置,该级菜单不能以中文命名,后面的都可以用中文
     * isValidateFunction : 这个参数标志是否是验证函数
     * priority : 优先级,值越小,在菜单中越靠上
     */
    //主菜单下的一级菜单menu1
    [MenuItem("Menu/menu1" , false, 1)]
    static public void Menu1()
    {
        Debug.Log("menu1");
    }
    //主菜单下的二级菜单menu2
    [MenuItem("Menu/menu2/menu2", false, 2)]
    static public void Menu2()
    {
        Debug.Log("menu2/menu2");
    }

    /*
     * _%H 表示快捷键为ctrl + H
     * 创建快捷键方式 : 在对应的MenuItem后面以" _"开头,后面添加快捷键标志
     * 快捷键标志规则 : 
     *      % = ctrl 
     *      # = Shift
     *      & = Alt 
     *      F1…F2 = F... 
     *      LEFT/RIGHT/UP/DOWN = 上下左右 
     *      HOME, END, PGUP, PGDN = 键盘上的特殊功能键
     */
    [MenuItem("Menu/menu2/menu2 _%H", false, 2)]
    static public void Menu2()
    {
        Debug.Log("menu2/menu2");
    }

交换两个物体位置

using UnityEditor;
using UnityEngine;

public class MenuCommand
{
    [MenuItem("MenuCommand/SwapGameObject")]
    protected static void SwapGameObject()
    {
        //只有两个物体才能交换
        if( Selection.gameObjects.Length == 2 )
        {
            Vector3 tmpPos = Selection.gameObjects[0].transform.position;
            Selection.gameObjects[0].transform.position = Selection.gameObjects[1].transform.position;
            Selection.gameObjects[1].transform.position = tmpPos;
            //处理两个以上的场景物体可以使用MarkSceneDirty
            UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty( UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene() );
        }
    }
}

EditorWindows

利用OnGUI绘制窗口Unity - Scripting API: EditorGUILayout

  • GUIStyle guiStyle = new GUIStyle {fontStyle = FontStyle.Bold};
  • EditorGUILayout.LabelField("新的标签",guiStyle);
  • EditorGUILayout.TextField("这是输入框的标签", "默认文字");
  • EditorGUILayout.BeginToggleGroup("可选设置", toggleGroupEnable);
  • EditorGUILayout.Toggle("开关", toggleEnable);
  • EditorGUILayout.Slider( "滑动条",siderValue, 0f, 1f);
  • EditorGUILayout.EndToggleGroup();

也可以

GetWindow().Show()来显示Window

using UnityEngine;
using System.Collections;
using UnityEditor;
using UnityEngine.Tizen;
 
public class EditWindow : EditorWindow
{
    static string myString = "Hello World";
    bool groupEnabled;
    bool myBool = true;
    float myFloat = 1.23f;
    static GenericMenu menu;
    
    //初始化,也就是一个MenuItem,当点击时调用Init()
    [MenuItem("Window/EditWindow _%&#B")]
    static void Init()
    {
        Debug.Log("init");
        // Get existing open window or if none, make a new one:
        GetWindow(typeof(EditWindow));   //获取到窗口,并且显示
        //实例化并且设置menu
        menu = new GenericMenu();   //初始化menu,但是没有显示
        menu.AddItem(new GUIContent("MenuItem1"), false, Callback, "item 1");
        menu.AddItem(new GUIContent("MenuItem2"), false, Callback, "item 2");
        menu.AddSeparator("");
        menu.AddItem(new GUIContent("SubMenu/MenuItem3"), false, Callback, "item 3");
    }
    
    //通过GUI渲染,每帧调用
    void OnGUI()
    {
        //创建要显示在编辑窗口的内容
        GUILayout.Label("Base Settings", EditorStyles.boldLabel);
        myString = EditorGUILayout.TextField("Text Field", myString);
        EditorGUILayout.Space();    //空行:间隙
        //ToggleGroup
        groupEnabled = EditorGUILayout.BeginToggleGroup("Optional Settings", groupEnabled);
        myBool = EditorGUILayout.Toggle("Toggle", myBool);
        myFloat = EditorGUILayout.Slider("Slider", myFloat, -3, 3);
        myString = EditorGUILayout.TextField("Field", myString);
        EditorGUILayout.EndToggleGroup();
 
        EditorGUILayout.BeginVertical("Button");
        GUILayout.Label("I'm inside the button");
        GUILayout.Label("So am I");
        EditorGUILayout.EndVertical();
 
        Event currentEvent = Event.current;
        Rect contextRect = new Rect(0, 200, 100, 100);
        //EditorGUI.DrawRect(contextRect, Color.green);
        EditorGUI.LabelField(contextRect, "Hello World Text");
 
        if (currentEvent.type == EventType.MouseDown)   //点击事件发生在当前EditorWindow上
        {
            Vector2 mousePos = currentEvent.mousePosition;
            if (contextRect.Contains(mousePos)) //点在矩形范围内
            {
                menu.ShowAsContext();   //显示menu
                currentEvent.Use();
            }
        }
    }
 
    //回调
    static void Callback(object obj)
    {
        Debug.Log("Selected: " + obj.ToString());
    }
}

EditorWindow如何绘制Scene界面UI

在EditorWindow中如果需要对Scene绘制一些UI,这个时候使用Editor那种OnSceneGUI是无效的,这个时候则需要在Focus或者OnEnable时候加入SceneView的事件回调中,并且在OnDestroy时候去除该回调:

private void OnFocus() 
{
    //在2019版本是这个回调
    SceneView.duringSceneGui -= OnSceneGUI;
    SceneView.duringSceneGui += OnSceneGUI;

    //以前版本回调
    // SceneView.onSceneGUIDelegate -= OnSceneGUI
    // SceneView.onSceneGUIDelegate += OnSceneGUI
}

private void OnDestroy() 
{
    SceneView.duringSceneGui -= OnSceneGUI;
}
private void OnSceneGUI( SceneView view ) 
{
}

OnDrawGizmos

OnDrawGizmos是在MonoBehaviour下的一个方法,通过这个方法可以可以绘制出一些Gizmos来使得其一些参数方便在Scene窗口查看。

比如我们有一个沿着路点移动的平台,一般的操作可能是生成一堆新的子物体来确定和设置位置,但其实这样会有点赘余,我们需要的只是一个Vector2/Vector3数组。而这个时候我们就可以通过OnDrawGizmos方法在编辑器绘制出这些Vector2/Vector3的数组点。

unity如何注册系统软键盘的方向按键_unity如何注册系统软键盘的方向按键

代码如下:

public class DrawGizmoTest : MonoBehaviour
{
    public Vector2[] poses;

    private void OnDrawGizmos() 
    {
        Color originColor = Gizmos.color;
        Gizmos.color = Color.red;
        if( poses!=null && poses.Length>0 )
        {
            //Draw Sphere
            for (int i = 0; i < poses.Length; i++)
            {
                Gizmos.DrawSphere( poses[i], 0.2f );
            }
            //Draw Line
            Gizmos.color = Color.yellow;
            Vector2 lastPos = Vector2.zero;
            for (int i = 0; i < poses.Length; i++)
            {
                if( i > 0 )
                {
                    Gizmos.DrawLine( lastPos, poses[i] );
                }
                lastPos = poses[i];
            }
        }
        Gizmos.color = originColor;
    } 
}

OnInspectorGUI

在开发过程中常常需要在编辑器上对某个特定的Component进行一些操作,比如在Inspector界面上有一个按钮可以触发一段代码。

unity如何注册系统软键盘的方向按键_ide_02

这种属于编辑器的,所以一般是在Editor文件夹中新建一个继承自Editor的脚本:之后编辑继承自UnityEditor.Editor,这里注意是必须在类上加入[CustomEditor(typeof(编辑器脚本绑定的Monobehavior类)]然后重写它的OnInspectorGUI方法:

using UnityEditor;
[CustomEditor(typeof(InspectorTest))]
public class InspectorTestEditor : Editor 
{
    public override void OnInspectorGUI() 
    {
        base.OnInspectorGUI();
        if(GUILayout.Button("Click Me"))
        {
            //Logic
            InspectorTest ctr = target as InspectorTest;
        }
    }
}

方法一:

重打开场景后会变回原来的值

if(GUILayout.Button("Click Me"))
{
    //Logic
    InspectorTest ctr = target as InspectorTest;
    ctr.Name = "Codinggamer";
}

方法二:

重打开场景后值不变

if(GUILayout.Button("Click Me"))
{
    //Logic
    serializedObject.FindProperty("Name").stringValue = "Codinggamer";
    serializedObject.ApplyModifiedProperties();
}

OnSceneGUI

这个方法也是在Editor类中的一个方法,是用来在Scene视图上显示一个UI元素。其创建也是在Editor文件夹下新建一个继承自Editor的脚本

在OnSceneGUI中可以做出和OnDrawGizmo类似的功能,比如绘制出Vector2数组的路点:

unity如何注册系统软键盘的方向按键_快捷键_03

因为OnSceneGUI是在Editor上的方法,而Editor一般都是对应Monobehaviour,这意味它是只能是点击到对应物体才会生成的。而OnDrawGizmos则是可以全局可见。

而如果需要事件处理,比如需要在Scene界面可以直接点击增加或者修改这些路点,就需要在OnSceneGUI上处理事件来进行一些操作。

代码如下:

using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(SceneGUITest))]
public class SceneGUITestEditor : Editor 
{
    protected SceneGUITest ctr;

    private void OnEnable() 
    {
        ctr = target as SceneGUITest;
    }
    private void OnSceneGUI() 
    {
        Event _event = Event.current;

        if( _event.type == EventType.Repaint )
        {
            Draw();
        }
        else if ( _event.type == EventType.Layout )
        {
            HandleUtility.AddDefaultControl( GUIUtility.GetControlID( FocusType.Passive ) );
        }
        else
        {
            HandleInput( _event );
            HandleUtility.Repaint();
        }
    }

    void HandleInput( Event guiEvent )
    {
        Ray mouseRay = HandleUtility.GUIPointToWorldRay( guiEvent.mousePosition );
        Vector2 mousePosition = mouseRay.origin;
        if( guiEvent.type == EventType.MouseDown && guiEvent.button == 0 )
        {
            ctr.poses.Add( mousePosition );
        }
    }

    void Draw()
    {
        //Draw a sphere
        Color originColor = Handles.color;
        Color circleColor = Color.red;
        Color lineColor = Color.yellow;
        Vector2 lastPos = Vector2.zero;
        for (int i = 0; i < ctr.poses.Count; i++)
        {
            var pos = ctr.poses[i];
            Vector2 targetPos = ctr.transform.position;
            //Draw Circle
            Handles.color = circleColor;
            Vector2 finalPos = targetPos + new Vector2( pos.x, pos.y);

            Handles.SphereHandleCap(  GUIUtility.GetControlID(FocusType.Passive ) , finalPos , Quaternion.identity, 0.2f , EventType.Repaint );
            //Draw line
            if(i > 0) 
            {
                Handles.color = lineColor;
                Handles.DrawLine( lastPos, pos );
            }
            lastPos = pos;
        }
        Handles.color = originColor;
    }
}

ScriptWizard

Unity引擎的中的BuildSetting窗口(Ctr+Shift+B弹出的窗口)就是使用了SciptWizard,一般来开发过程中作为比较简单的生成器和初始化类型的功能来使用,比如美术给我一个序列帧,我需要直接生成一个带SpriteRenderer的GameObject,而且它还有自带序列帧的Animator。

其创建过程还是在Editor文件夹下创建一个继承自ScriptWizard的脚本,调用ScriptWizard.DisplayWizard方法即可生成并显示这个窗口,点击右下角的Create会调用OnWizardCreate方法:

public class TestScriptWizard: ScriptableWizard 
{

    [MenuItem("CustomEditorTutorial/TestScriptWizard")]
    private static void MenuEntryCall() 
    {
        DisplayWizard("Title");
    }

    private void OnWizardCreate() 
    {

    }
}

在ScriptWizard中如果你声明一个Public的变量,会发现在窗口可以直接显示,但是在EditorWindow则是不能显示。

ScriptObject

对于游戏中一些数据和配置可以考虑用ScriptObject来保存,虽然XML之流也可以,但是ScriptObject相对比较简单而且可以保存UnityObject比如Sprite、Material这些。甚至你会发现上面说的几个类都是继承自SctriptObject。 因为其不再是只适用编辑器,所以不必放在Editor文件夹下 。

与ScriptWizard类似,也是声明Public可以在窗口上直接看到,自定义绘制GUI也是在OnGUI方法里面:

[CreateAssetMenu(fileName = "TestScriptObject", menuName = "CustomEditorTutorial/TestScriptObject", order = 0)]
public class TestScriptObject : ScriptableObject 
{
    public string Name;
}

Attributes

Attributes是C#的一个功能,它可以让声明信息与代码相关联,其与C#的反射联系很紧密。在Unity中诸如[System.Serializable],[Header],[Range]都是其的应用。一般来说他它功能也可以通过Editor来实现,但是可以绘制对应的属性来说会更好复用。

拓展Attribute相对来说稍微复杂一点,它涉及两个类:PropertyAttributePropertyDrawer,前者是定义它行为,后者主要是其在编辑器的显示效果。一般来说Attribute是放在Runtime,而Drawer则是放在Editor文件夹下。这里的例子是加入[Preview]的Attribute,使得我们拖拽Sprite或者GameObject可以显示预览图:

unity如何注册系统软键盘的方向按键_unity_04

public class AttributeSceneController : MonoBehaviour 
{
    [Preview]
    public Sprite sprite;
}

继承自PropertyAttribute的PreviewAttribute脚本:

public class Preview : PropertyAttribute
{
    public Preview()
    {

    }
}

在Editor文件夹下加入继承自PropertyDrawer的PreviewDrawer脚本:

using UnityEngine;
using UnityEditor;
namespace EditorTutorial
{
    [CustomPropertyDrawer(typeof(Preview))]
    public class PreviewDrawer: PropertyDrawer 
    {
        //调整整体高度
        public override float GetPropertyHeight( SerializedProperty property, GUIContent label )
        {
            return base.GetPropertyHeight( property, label ) + 64f;
        }
        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) 
        {
            EditorGUI.BeginProperty(position, label, property);
            EditorGUI.PropertyField(position, property, label);

            // Preview
            Texture2D previewTexture = GetAssetPreview(property);
            if( previewTexture != null )
            {
                Rect previewRect = new Rect()
                {
                    x = position.x + GetIndentLength( position ),
                    y = position.y + EditorGUIUtility.singleLineHeight,
                    width = position.width,
                    height = 64
                };
                GUI.Label( previewRect, previewTexture );
            }
            EditorGUI.EndProperty();
        }

        public static float GetIndentLength(Rect sourceRect)
        {
            Rect indentRect = EditorGUI.IndentedRect(sourceRect);
            float indentLength = indentRect.x - sourceRect.x;

            return indentLength;
        }

        Texture2D GetAssetPreview( SerializedProperty property )
        {
            if (property.propertyType == SerializedPropertyType.ObjectReference)
            {
                if (property.objectReferenceValue != null)
                {
                Texture2D previewTexture = AssetPreview.GetAssetPreview(property.objectReferenceValue);
                return previewTexture;
                }
            return null;
            }
            return null;
        }
    }
}

AssetPostprocessor

在开发过程中常常会遇到资源导入问题,比如我制作像素游戏图片要求是FilterMode为Point,图片不需要压缩,PixelsPerUnit为16,如果每次复制到一个图片到项目再修改会很麻烦。这里一个解决方案是可以用MenuItem来处理,但还需要多点几下,而使用AssetPostprocessor则可以自动处理完成。

在Editor文件夹下新建一个继承自AssetPostprocessor的TexturePipeLine:

public class TexturePipeLine : AssetPostprocessor
{
    private void OnPreprocessTexture()
    {
        TextureImporter importer = assetImporter as TextureImporter;
        if( importer.filterMode == FilterMode.Point ) return;

        importer.spriteImportMode = SpriteImportMode.Single;

        importer.spritePixelsPerUnit = 16;
        importer.filterMode = FilterMode.Point;
        importer.maxTextureSize = 2048;
        importer.textureCompression = TextureImporterCompression.Uncompressed;

        TextureImporterSettings settings = new TextureImporterSettings();
        importer.ReadTextureSettings( settings );
        settings.ApplyTextureType( TextureImporterType.Sprite );
        importer.SetTextureSettings( settings ) ;
    }
}

P2其他

Undo撤销

在之前说过在Editor里面直接改动原来的Monobehaviour脚本是变量是无法撤销的,但是使用 serializedObject来修改则可以撤销。这里可以自己写一个Undo来记录使其可以撤销,代码如下:

if(GUILayout.Button("Click Me"))
{
    InspectorTest ctr = target as InspectorTest;
    //记录使其可以撤销
    Undo.RecordObject( ctr ,"Change Name" );
    ctr.Name = "Codinggamer";
    EditorUtility.SetDirty( ctr );
}

不在Editor文件夹里面写编辑器代码

有的时候我们Monobehaviour本身很短小,拓展InspectorGUI的代码也很短小,没有必要在Editor上创建一个新的脚本,可以直接使用#UNITY_EDITOR 的宏来创建一个拓展编辑器,比如之前的拓展InspectorGUI可以这样写:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif

namespace EditorTutorial
{
    public class InspectorTest : MonoBehaviour
    {
        public string Name = "hello";
    }

    #if UNITY_EDITOR
    [CustomEditor(typeof(InspectorTest))]
    public class InspectorTestEditor : Editor 
    {
        public override void OnInspectorGUI() 
        {
            base.OnInspectorGUI();
            if(GUILayout.Button("Click Me"))
            {

                InspectorTest ctr = target as InspectorTest;
                //记录使其可以撤销
                Undo.RecordObject( ctr ,"Change Name" );
                ctr.Name = "Codinggamer";
                EditorUtility.SetDirty( ctr );
            }
        }
    }
    #endif
}

EditorWindow和Editor保存数据

这里需要使用EditorPrefs来保存和读取数据,在需要保存的数据上面加上[System.SerializeField]的Attribute,然后在OnEnable和OnDisable时候可以保存或者度序列化json:

[SerializeField]
public string Name = "Hi";

private void OnEnable() 
{
    var data = EditorPrefs.GetString( "WINDOW_KEY", JsonUtility.ToJson( this, false ) );
    JsonUtility.FromJsonOverwrite( data, this );
}

private void OnDisable() 
{
    var data = JsonUtility.ToJson( this, false  );
    EditorPrefs.SetString("WINDOW_KEY", data);
}

在代码中检索对应MonoBehaviour的Editor类

在EditorWindow的使用过程中,有的时候可能需要调用到对应拓展MonoBehaviour的Editor代码,这个时候可以使用Editor.CreateEditor方法来创建这个Editor。

在编辑器代码中生成 SerializedObject

上面说过,在编辑器代码中一般比较多使用SerializedObject,像Editor类中就内置了serializedObject。实际上所有继承自ScriptObject或者Monobehaviour的脚本都可以生成SerializedObject。其生成方式很简单只需要new的时候传入你需要序列化的组件即可:

SerializedObject serialized = new SerializedObject(this);

用路径加载对象

//这里用的路径是Assets/XXX/XXX/XXX.prefab
 GameObject go = AssetDatabase.LoadAssetAtPath<GameObject>(path);

 //根据类型在指定目录下查找资源
 string[] resPaths = { btPath };
 var graphList = AssetDatabase.FindAssets("t:graph", resPaths);

搜索指定路径下的所有文件

//这里还包含一个linq语句剔除掉了.meta文件~
var files = Directory.GetFiles(LEVEL_FOLDER_PATH, "*.*", SearchOption.AllDirectories);
SearchOption.AllDirectories).Where((s) => !s.EndsWith(".meta")).ToArray();

//这是搜索文件夹
Directory.GetDirectories(LEVEL_FOLDER_PATH, "*.*", SearchOption.AllDirectories);

保存修改完成后的资源

//先在循环内用setDirty将修改后的预制体进行标记
EditorUtility.SetDirty(go);

 //然后在循环结束的时候调用保存所有被标记的资源与刷新
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();

编辑器下卸载资源

EditorUtility.UnloadUnusedAssetsImmediate();

加速importing

/*
Starts importing Assets into the Asset Database. This lets you group several Asset imports together into one larger import.
*/
//在保存资源(save asset)前先调用Start
AssetDatabase.StartAssetEditing()

//然后在保存资源(save asset)后调用stop
AssetDatabase.StopAssetEditing

进度条

EditorUtility.DisplayProgressBar(标题, 内容, 进度);

//记得清,不然这个弹窗就关不掉了!!
EditorUtility.ClearProgressBar();

文件写入

FileStream fs = new
FileStream("Assets/Lua/config/GlobalLuaActionDescribe.lua.txt", FileMode.Create);
StreamWriter st = new StreamWriter(fs);
st.Write(sb.ToString());
st.Close();

文件改名

AssetDatabase.RenameAsset(path, newName);
AssetDatabase.Refresh();

ToBeContinued......