unity-编辑器扩展

unity编辑器扩展很方便,可以省掉很多制作其他工具的时间(比如技能编辑器,特效编辑器等,都是数据结构的导入导出)

可以直接在 SceneInspector 或者新建一个 Window 中编辑


前篇

可以参考以下几个工程


Inspector

  1. Inspector 中最简单的就是直接把脚本中的成员变量设置为 public,直接就可以在显示出来编辑。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class Actor : MonoBehaviour {

    #region public member
    public Vector3 lookAtPoint = Vector3.zero;
    public int mAge;
    public string mName;
    public bool mIsMale;
    public string mPath;
    public AnimationCurve mCurve;
    public Color mColor;
    #endregion
    }

  2. 针对脚本定制编辑panel

    • 新建一个 Editor 文件夹(啥地方都行),unity会自动监测,自动生成vs的工程目录结构。
    • Editor 文件夹下create个编辑器脚本 ActorEditor.cs (可以随意,一般以Editor为后缀),脚本继承 Editor,重写 OnInspectorGUI 方法,这个方法每帧调用绘制 编辑panel
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      using UnityEngine;
      using UnityEngine.UI;
      using System.Collections;
      using UnityEditor;

      [CustomEditor(typeof(Actor))] //编辑对应的脚本
      public class ActorEditor : Editor
      {

      //private string mPath1 = "";
      private Actor mActor;

      public override void OnInspectorGUI()
      {
      Actor t = (Actor)target;
      mActor = t;

      t.mName = EditorGUILayout.TextField("name", t.mName);
      t.mAge = EditorGUILayout.IntField("age", t.mAge);
      t.mIsMale = ETCGuiTools.Toggle("male", t.mIsMale);
      t.transform.position = EditorGUILayout.Vector3Field("position", t.transform.position);

      EditorGUILayout.Space();
      EditorGUILayout.BeginHorizontal(); //被Horizontal包的会显示在同一行
      GUILayout.Label("资源路径:");
      t.mPath = EditorGUILayout.TextField(t.mPath);
      if (GUILayout.Button("浏览"))
      EditorApplication.delayCall += Save; //点击按钮调用的方法
      EditorGUILayout.EndHorizontal();

      EditorGUILayout.Space();
      EditorGUILayout.BeginHorizontal();
      t.mCurve = EditorGUILayout.CurveField("Curves:", t.mCurve);
      EditorGUILayout.EndHorizontal();

      EditorGUILayout.Space();
      EditorGUILayout.BeginHorizontal();
      EditorGUILayout.LabelField("Color:");
      t.mColor = EditorGUILayout.ColorField(t.mColor);
      EditorGUILayout.EndHorizontal();
      }

      /// <summary>
      /// 选择资源存储路径
      /// </summary>
      void Save()
      {
      string path = EditorUtility.OpenFolderPanel("选择要存储的路径", "", "");
      if (path.Length != 0)
      {
      //mPath1 = path;
      mActor.mPath = path; //貌似这里只能通过成员实现,不能传参数。可能是异步的原因
      EditorUtility.FocusProjectWindow();
      }
      }

Window

同样也是create个脚本 TestDrag 丢在 Editor 文件夹下。TestDrag 继承 EditorWindow ,写个静态方法 Init 上面用属性标注 [MenuItem("MyWindow/TestDrag")],意思就是会在菜单栏 MyWindow/TestDrag中点击触发弹出TestDrag 的编辑 window,然后重写 OnGUI() 方法具体定制显示需要编辑的东东

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
using UnityEngine;
using System.Collections;
using UnityEditor;

public class TestDrag : EditorWindow
{
string path;
Rect rect;
bool groupEnabled = true;
string myString = "Hello World";
bool myBool = true;
float myFloat = 1.23f;

[MenuItem("MyWindow/TestDrag")] //触发弹出TestDrag window的按钮
static void Init()
{
EditorWindow.GetWindow(typeof(TestDrag));
}

void OnGUI() //绘制window
{
EditorGUILayout.LabelField("路径", EditorStyles.boldLabel);
//获得一个长300的框
rect = EditorGUILayout.GetControlRect(GUILayout.Width(300));
//将上面的框作为文本输入框
path = EditorGUI.TextField(rect, path);

//如果鼠标正在拖拽中或拖拽结束时,并且鼠标所在位置在文本输入框内
if ((Event.current.type == EventType.DragUpdated
|| Event.current.type == EventType.DragExited)
&& rect.Contains(Event.current.mousePosition))
{
//改变鼠标的外表
DragAndDrop.visualMode = DragAndDropVisualMode.Generic;
if (DragAndDrop.paths != null && DragAndDrop.paths.Length > 0)
{
path = DragAndDrop.paths[0];
}
}

//---------
GUILayout.Label("Base Settings", EditorStyles.boldLabel);
myString = EditorGUILayout.TextField("Text Field", myString);

groupEnabled = EditorGUILayout.BeginToggleGroup("Optional Settings", groupEnabled);
myBool = EditorGUILayout.Toggle("Toggle", myBool);
myFloat = EditorGUILayout.Slider("Slider", myFloat, -3, 3);
EditorGUILayout.EndToggleGroup();
}
}


Scene

同样也是create个脚本 SceneRealTimeFocusEditor 丢在 Editor 文件夹下。
在场景中显示一些按钮、文字什么的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using UnityEditor;

/// <summary>
/// Scene中的按钮扩展:
/// </summary>
[CustomEditor(typeof(MonoBehaviour), true)]
public class SceneRealTimeFocusEditor : Editor
{
public void OnEnable()
{
active = false;
}
public void OnDisable()
{
active = false;
}

private bool active = false;
void OnSceneGUI()
{
GameObject[] gos = Selection.gameObjects;
if (gos != null)
//if (Selection.activeTransform != null)
{

Handles.BeginGUI();
GUILayout.BeginArea(new Rect(0, 0, 200, 200));
if (!active)
{
if (GUILayout.Button("Active Real Time Trace", GUILayout.Height(30)))
{
active = true;
}
}
else
{
if (GUILayout.Button("Close Real Time Trace", GUILayout.Height(30)))
{
active = false;
}
}
GUILayout.EndArea();
Handles.EndGUI();

if (active)
{
SceneView.lastActiveSceneView.pivot = gos[0].transform.position;
//SceneView.lastActiveSceneView.pivot = Selection.activeTransform.position;
SceneView.lastActiveSceneView.Repaint();
}
}
}
}


4、附上一个场景对象实时追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Text;

/// <summary>
/// 实时显示场景中的对象
/// </summary>
public class SceneEditorWindow : EditorWindow
{

RaycastHit _hitInfo;
SceneView.OnSceneFunc _delegate;
static SceneEditorWindow _windowInstance;

private GameObject[] mSelectObjs = null;

[MenuItem("MyWindow/Scene Editor #`")] //快捷方式:shift+`
static void Init()
{
if (_windowInstance == null)
{
_windowInstance = EditorWindow.GetWindow(typeof(SceneEditorWindow)) as SceneEditorWindow;
_windowInstance._delegate = new SceneView.OnSceneFunc(OnSceneFunc);
SceneView.onSceneGUIDelegate += _windowInstance._delegate;

}
}

void OnEnable()
{
}

void OnDisable()
{
}

void OnDestroy()
{
if (_delegate != null)
{
SceneView.onSceneGUIDelegate -= _delegate;
}
}

void OnSelectionChange()
{
mSelectObjs = Selection.gameObjects;
}

void OnGUI()
{
EditorGUILayout.LabelField("All Selected Objects", EditorStyles.boldLabel);

if (mSelectObjs != null)
{
for (int i = 0; i < mSelectObjs.Length; ++i)
{
EditorGUILayout.BeginHorizontal();
GameObject go = mSelectObjs[i];
EditorGUILayout.Vector3Field(go.name, go.transform.position);
EditorGUILayout.EndHorizontal();
}
}
}

void OnInspectorGUI()
{
Debug.Log("OnInspectorGUI");
}

static public void OnSceneFunc(SceneView sceneView)
{
_windowInstance.CustomSceneGUI(sceneView);
}

void CustomSceneGUI(SceneView sceneView)
{
Camera cameara = sceneView.camera;
Ray ray = HandleUtility.GUIPointToWorldRay(Event.current.mousePosition);
if (Physics.Raycast(ray, out _hitInfo, 10000, -1))
{
//Debug.DrawRay(ray.origin, ray.direction, Color.yellow);
Vector3 origin = _hitInfo.point;
origin.y += 100;
if (Physics.Raycast(origin, Vector3.down, out _hitInfo))
{
Handles.color = Color.yellow;
Handles.DrawLine(_hitInfo.point, origin);
float arrowSize = 1;
Vector3 pos = _hitInfo.point;
Quaternion quat;
Handles.color = Color.green;
quat = Quaternion.LookRotation(Vector3.up, Vector3.up);
Handles.ArrowCap(0, pos, quat, arrowSize);
Handles.color = Color.red;
quat = Quaternion.LookRotation(Vector3.right, Vector3.up);
Handles.ArrowCap(0, pos, quat, arrowSize);
Handles.color = Color.blue;
quat = Quaternion.LookRotation(Vector3.forward, Vector3.up);
Handles.ArrowCap(0, pos, quat, arrowSize);
//Handles.DrawLine(pos + new Vector3(0, 3, 0), pos);
}
}
SceneView.RepaintAll();
}
}


  • MenuItem

    • 静态方法的

      1
      2
      3
      [MenuItem("Terrain/测试Lightmapping信息 ")]
      static void TestLightmapingInfo() {
      }

  • ContextMenu

    • 实例方法的

      1
      2
      3
      4
      //保存光照信息
      [ContextMenu("Save")]
      public void SaveLightmap() {
      }


自定义样式

  • 方式一: 修改内置

    可以直接修改内置的样式参数, 不过这是全局修改,默认样式, 会导致所有的 ui 控件都默认使用.

    1
    EditorStyles.textArea.wordWrap = true;
  • 方式二: 实例化自定义

    使用内置样式实例化出来新的样式.

    参考: https://answers.unity.com/questions/37310/editorguilayouttextarea-wrap.html

    1
    2
    3
    4
    5
    GUIStyle mStyleArea = null;
    mStyleArea = new GUIStyle(EditorStyles.textArea); // 貌似这个不能 某个自定义绘制类 构造函数 里面实例化, 只能第一次 update 的时候实例化
    mStyleArea.wordWrap = true;
    // 样式实例
    EditorGUILayout.TextArea(mGGToken, mStyleArea);
    • 某个自定义绘制类 构造函数 里面实例化 会报错: you are pushing more guiclips than you are popping. make sure they are balanced

ScriptableObject 对象的数据改变后,关闭编辑器才会改变磁盘上相应文件的问题

先设置 dirty,再 save

1
2
EditorUtility.SetDirty(targ);
AssetDatabase.SaveAssets();

Odininspector 插件

这个插件使得编辑器扩展非常方便