在Unity编辑器上增加你自己的工具

本篇教程将教你扩展Unity编辑器,来更好的配合你开发项目.你将学习如何绘制Gizmo,在节点中创建删除对象,创建一个编辑器窗口,和使用组件,并且允许用户撤消任何在你工具上的操作.

首先我们看看最终我们要达成的工具样子:

正如图中所示,我们将创建一个编辑器窗口,还有颜色选择器来设置我们画出的栅格颜色,另外也可以创建和删除对象,对齐到这个栅格,以及对操作的撤消等等.
1:Gizmos
首先我们学习如何使用Gizmos,下面这个是几个常见的内建的Gizmos.

选择任何一个对象时都会显示这个Gizmo.

这是另外一个Gizmo,这使我们能看到所选的对象的boxCollider的大小.

2:创建一个栅格脚本
用C#脚本绘制Gizmo给一个对象,我们将在编辑器中画一个栅格作为示例.

using UnityEngine;
using System.Collections;

public class Grid : MonoBehaviour
{

    void Start ()
    {
    }

    void Update ()
    {
    }
}

这个栅格,我们需要添加长和宽的两个变量.

public class Grid : MonoBehaviour
{
    public float width = 32.0f;
    public float height = 32.0f;

    void Start ()
    {
    }

    void Update ()
    {
    }
}

要绘制栅格,我们需要用OnDrawGizmos,让我们创建它.

public class Grid : MonoBehaviour
{
    public float width = 32.0f;
    public float height = 32.0f;

    void Start ()
    {
    }

    void Update ()
    {
    }

    void OnDrawGizmos()
    {
    }
}

3:绘制栅格
要绘制栅格,我们需要在编辑器的相机位置绘制一些横线竖线.首先我们用一个变量来储存相机的位置.

void OnDrawGizmos()
{
    Vector3 pos = Camera.current.transform.position;
}

我们可以使用Camera.current来获取编辑器的相机.

现在我们需要用两个循环来绘制横线和竖线.

void OnDrawGizmos()
{
    Vector3 pos = Camera.current.transform.position;

    for (float y = pos.y - 800.0f; y < pos.y + 800.0f; y+= height)
    {
        Gizmos.DrawLine(new Vector3(-1000000.0f, Mathf.Floor(y/height) * height, 0.0f),
                        new Vector3(1000000.0f, Mathf.Floor(y/height) * height, 0.0f));
    }

    for (float x = pos.x - 1200.0f; x < pos.x + 1200.0f; x+= width)
    {
        Gizmos.DrawLine(new Vector3(Mathf.Floor(x/width) * width, -1000000.0f, 0.0f),
                        new Vector3(Mathf.Floor(x/width) * width, 1000000.0f, 0.0f));
    }
}

我们用Gizmos.DrawLine()来画线.Gizmos类里面有很多其他绘制方法,所以它可以绘制立方体,球体,甚至线框图.也可以绘制图像.

栅格应该是无限长,但是没有无穷远的浮点数给我们用,所以我们可以简单的用任意大的数字代替.行数取决于循环的次数.

要看到栅格,旧创建一个空对象并将我们这个脚本赋予它.

4:自定义检视面板

接下来我们自定义检视面板,我们需要创建在创建一个C#脚本文件并命名为GridEditor.放在脚本编辑器Editor文件夹里.如果没有这个文件夹,就创建一个.

using UnityEngine;
using UnityEditor;
using System.Collections;

[CustomEditor (typeof(Grid))]
public class GridEditor : Editor
{
}

我们需要用UnityEditor的函数来覆盖栅格默认的检视面板,使用 [CustomEditor (typeof(Grid))] 让Unity知道我们将定制栅格的检视面板.

public class GridEditor : Editor
{
    public override void OnInspectorGUI()
    {
    }
}

如果现在你看编辑器的检视面板,你将看到一个空的窗口.这是因为我们自定义了一个面板用OnInspectorGui()的方法覆盖掉了默认的检视面板.

5:使用GuiLayout填充自定义检视面板.
上面我们创建了一个空的面板,首先我们要给这个面板指定一个目标对象.实际上,已经有个target给这个检视面板.但是方便起见,我们将创建一个引用栅格组件的对象.我们先来声明它.

public class GridEditor : Editor
{
    Grid grid;

在检视面板激活的时候可以用OnEnable()方法来执行一些事件.

public class GridEditor : Editor
{
    Grid grid;

    public void OnEnable()
    {
        grid = (Grid)target;
    }

我们用GUILayout和EditorGUILayout类来创建一些表单.

public override void OnInspectorGUI()
{
    GUILayout.BeginHorizontal();
    GUILayout.Label(" Grid Width ");
    grid.width = EditorGUILayout.FloatField(grid.width, GUILayout.Width(50));
    GUILayout.EndHorizontal();
}

第一行中的GUILayout.BeginHorizontal()表示下面的元素都将水平排列,同样的,在最后有GUILayout.EndHorizontal()来说明结束上面的排列方式.第一个是一个标签.用来显示网格宽度.然后旁边创建了一个EditorGUILayout.FloatField字段来分配grid.width,并设置默认值为50像素.

下面是在检视面板中实际的效果:

6:填充检视面板和重绘场景.
让我们添加grid.height的字段到检视面板中.

public override void OnInspectorGUI()
{
    GUILayout.BeginHorizontal();
    GUILayout.Label(" Grid Width ");
    grid.width = EditorGUILayout.FloatField(grid.width, GUILayout.Width(50));
    GUILayout.EndHorizontal();

    GUILayout.BeginHorizontal();
    GUILayout.Label(" Grid Height ");
    grid.height = EditorGUILayout.FloatField(grid.height, GUILayout.Width(50));
    GUILayout.EndHorizontal();
}

这就是控制栅格对象的所有字段,如果想了解其他类型的字段项,可以看脚本参考的EditorGUILayout和GUILayout,这个时候只有在我们选择场景视图窗口的时候,检视面板中做的新变化才是可见的,为了保持实时变化,一旦我们做了修改,我们就可以用SceneView.RepaintAll()来重绘场景视图.

public override void OnInspectorGUI()
{
    GUILayout.BeginHorizontal();
    GUILayout.Label(" Grid Width ");
    grid.width = EditorGUILayout.FloatField(grid.width, GUILayout.Width(50));
    GUILayout.EndHorizontal();

    GUILayout.BeginHorizontal();
    GUILayout.Label(" Grid Height ");
    grid.height = EditorGUILayout.FloatField(grid.height, GUILayout.Width(50));
    GUILayout.EndHorizontal();

    SceneView.RepaintAll();
}

现在我们就不需要在点击检视面板以外的区域查看改变了.

7:处理编辑器的输入
现在我们来尝试处理编辑器的输入,就像我们做的游戏获得键盘或鼠标的状态.我们用onSceneGUIDelegate来实现场景视图上的交互,让我们来调用GridUpdate().

public void OnEnable()
{
    grid = (Grid)target;
    SceneView.onSceneGUIDelegate = GridUpdate;
}

void GridUpdate(SceneView sceneview)
{
}

现在我们只需要获得输入事件.

void GridUpdate(SceneView sceneview)
{
    Event e = Event.current;
}

8:创建预设
为了进一步学习编辑器脚本,我们需要一个游戏对象,让我们创建一个立方体预设.

可以缩放立方体或者其他方式让它与网格对齐.

你可以在层次面板中看到立方体的名字是蓝色的,这意味着它与一个预设有关联.在项目视图中也可以看到这个预设.

9:从编辑器脚本创建对象
现在我们要做到从编辑器脚本创建对象,打开GirdeEditor.cs脚本来改进GridUpdate()函数.
当按下按键时创建对象.

void GridUpdate(SceneView sceneview)
{
    Event e = Event.current;

    if (e.isKey && e.character == 'a')
    {
        GameObject obj;
    }
}

脚本会监听是否按下a键,同样创建了一个用来引用的新对象.

void GridUpdate(SceneView sceneview)
{
    Event e = Event.current;

    if (e.isKey && e.character == 'a')
    {
        GameObject obj;
        if (Selection.activeObject)
        {
            obj = (GameObject)Instantiate(Selection.activeObject);
            obj.transform.position = new Vector3(0.0f, 0.0f, 0.0f);
        }
    }
}

Selection.activeObject 是目前在编辑器选择的对象.只要有对象被选择,我们就会复制它并改变复制出来的对象的坐标到空间原点.

现在我们来测试一下,你要注意当资源重导入或者刷新时GridUpdate()将停止运行,如果要重新运行,你要选择一个对象(例如从层次视图中),我们这例子要选择网格对象.还需要记得场景视图获得焦点时才能捕获输入事件.

10:从编辑器脚本实例化预设
虽然我们现在可以克隆对象,但克隆的对象是不会有预设关联的.

你可以看到Cube(Clone)是纯黑色的字体,代表它没有连接到原来立方体的预设.而我们如果在编辑器中手动复制原始立方体,克隆出来的立方体将拥有原立方体一样的预设.如果想在脚本里面克隆出来的立方体也有原立方体的预设关联,我们需要使用EditorUtility类里面的InstantiatePrefab() 函数.

使用这个函数前,我们需要得到所选对象的预设,我们需要用到EditorUtility类里面的GetPrefabParent()函数.

void GridUpdate(SceneView sceneview)
{
    Event e = Event.current;

    if (e.isKey&& e.character == 'a')
    {
        GameObject obj;
        Object prefab = EditorUtility.GetPrefabParent(Selection.activeObject);

        if (prefab)
        {

我们也可以停止判断 Selection.activeObject了,因为如果它不存在,预设会为空,因此我们可以只检查预设引用是否存在就可以了.
现在让我们来实例化预设并设置它的位置.

void GridUpdate(SceneView sceneview)
{
    Event e = Event.current;

    if (e.isKey && e.character == 'a')
    {
        GameObject obj;
        Object prefab = EditorUtility.GetPrefabParent(Selection.activeObject);

        if (prefab)
        {
            obj = (GameObject)EditorUtility.InstantiatePrefab(prefab);
            obj.transform.position = new Vector3(0.0f, 0.0f, 0.0f);
        }
    }
}

看,现在克隆出来的有预设关联了.

11:转换屏幕上鼠标坐标到世界坐标系
Event类没有让我们获得鼠标在世界空间的位置,它只提供了屏幕空间坐标系的鼠标位置.下面我们将转换它们,让我们得到鼠标在世界空间里的位置.

void GridUpdate(SceneView sceneview)
{
    Event e = Event.current;

    Ray r = Camera.current.ScreenPointToRay(new Vector3(e.mousePosition.x, -e.mousePosition.y + Camera.current.pixelHeight));
    Vector3 mousePos = r.origin;

首先,我们使用编辑器相机的ScreenPointToRay从屏幕坐标系上获取射线,不过在这之前我们需要转换屏幕坐标供ScreenPointToRay()使用.

e.mousePosition的原点是从屏幕左上角(等于Camera.current.pixelWidth, -Camera.current.pixelHeight).我们需要转换为原点在屏幕左下角的坐标(Camera.current.pixelWidth, Camera.current.pixelHeight).
接下来我们要保存鼠标位置的向量.
现在我们可以指定克隆出来的对象到鼠标的位置.

if (prefab)
{
    obj = (GameObject)EditorUtility.InstantiatePrefab(prefab);
    obj.transform.position = new Vector3(mousePos.x, mousePos.y, 0.0f);
}

12:对齐立方体到栅格
让我们将创建的立方体对齐到鼠标位置的网格.

if (prefab)
{
    obj = (GameObject)EditorUtility.InstantiatePrefab(prefab);
    Vector3 aligned = new Vector3(Mathf.Floor(mousePos.x/grid.width)*grid.width + grid.width/2.0f,
                                  Mathf.Floor(mousePos.y/grid.height)*grid.height + grid.height/2.0f, 0.0f);
    obj.transform.position = aligned;
}

结果下图这样.

13:销毁从编辑器脚本创建的对象.

可以使用DestroyImmediate()来删除对象,本例中我们将通过按下”d”键来删除所有被选中的对象.

if (e.isKey && e.character == 'a')
{
    GameObject obj;
    Object prefab = EditorUtility.GetPrefabParent(Selection.activeObject);

    if (prefab)
    {
        obj = (GameObject)EditorUtility.InstantiatePrefab(prefab);
        Vector3 aligned = new Vector3(Mathf.Floor(mousePos.x/grid.width)*grid.width + grid.width/2.0f,
                                      Mathf.Floor(mousePos.y/grid.height)*grid.height + grid.height/2.0f, 0.0f);
        obj.transform.position = aligned;
    }
}
else if (e.isKey && e.character == 'd')
{
    foreach (GameObject obj in Selection.gameObjects)
        DestroyImmediate(obj);
}

当”D”键按下时,我们遍历所有被选中的对象,并逐一删除.当然Delete键依旧可用的.

14:撤消对象实例化

这一节我们将利用Undo类来实现撤消编辑器脚本的撤消.为了实现撤消,我们需要调用在编辑器中创建Undo.RegisterCreatedObjectUndo().它有两个参数,第一个是已经创建的对象,第二个是撤消的动作名称.会在”Edit-Undo name”中显示.

if (prefab)
{
    obj = (GameObject)EditorUtility.InstantiatePrefab(prefab);
    Vector3 aligned = new Vector3(Mathf.Floor(mousePos.x/grid.width)*grid.width + grid.width/2.0f,
                                  Mathf.Floor(mousePos.y/grid.height)*grid.height + grid.height/2.0f, 0.0f);
    obj.transform.position = aligned;
    Undo.RegisterCreatedObjectUndo(obj, "Create " + obj.name);
}

如果你按下按键,创建了多个立方体,然后尝试撤消,你会发现,刚才所有创建的立方体都被删除了.这事因为所有被创建的立方体放在了一个单一撤消事件中.

15:撤消单对象实例

如果我们需要对每个创建的对象都要有自身的撤消事件,我们需要使用Undo.IncrementCurrentEventIndex().

if (prefab)
{
    Undo.IncrementCurrentEventIndex();
    obj = (GameObject)EditorUtility.InstantiatePrefab(prefab);
    Vector3 aligned = new Vector3(Mathf.Floor(mousePos.x/grid.width)*grid.width + grid.width/2.0f,
                                  Mathf.Floor(mousePos.y/grid.height)*grid.height + grid.height/2.0f, 0.0f);
    obj.transform.position = aligned;
    Undo.RegisterCreatedObjectUndo(obj, "Create " + obj.name);
}

现在测试脚本你会看到会一个个的撤消被创建的立方体.

16:撤消对像删除

要撤消被删除的对象可以使用 Undo.RegisterSceneUndo(),它很慢,基本上是保存场景的状态,以便我们重做被撤消的动作,不过目前没有其他直接的办法来重做撤消.

else if (e.isKey && e.character == 'd')
{
    Undo.IncrementCurrentEventIndex();
    Undo.RegisterSceneUndo("Delete Selected Objects");
    foreach (GameObject obj in Selection.gameObjects)
        DestroyImmediate(obj);
}

Undo.RegisterSceneUndo() 只有一个参数,那就是撤消的名称.当用D键删除后.你可以用这个命令撤消删除.

17:创建编辑器窗口脚本

用一个新脚本创建一个新编辑器窗口,脚本名为GridWindow.cs.

using UnityEngine;
using UnityEditor;
using System.Collections;

public class GridWindow : EditorWindow
{
    public void Init()
    {
    }
}

通过栅格对象来访问这个窗口.

public class GridWindow : EditorWindow
{
    Grid grid;

    public void Init()
    {
        grid = (Grid)FindObjectOfType(typeof(Grid));
    }
}

18:创建GridWindow

我们在OnInspectorGUI()中为GridWindow添加一个打开窗口按钮.

public override void OnInspectorGUI()
{
    GUILayout.BeginHorizontal();
    GUILayout.Label(" Grid Width ");
    grid.width = EditorGUILayout.FloatField(grid.width, GUILayout.Width(50));
    GUILayout.EndHorizontal();

    GUILayout.BeginHorizontal();
    GUILayout.Label(" Grid Height ");
    grid.height = EditorGUILayout.FloatField(grid.height, GUILayout.Width(50));
    GUILayout.EndHorizontal();

    if (GUILayout.Button("Open Grid Window", GUILayout.Width(255)))
    {
       GridWindow window = (GridWindow) EditorWindow.GetWindow(typeof(GridWindow));
       window.Init();
    }

    SceneView.RepaintAll();
}

用GUILayout来创建按钮,并且设置按钮的名称和宽度.打开GridWindow窗口点下这个按钮,将返回真.

回到检视面板点击打开Grid Window按钮.

你会看到GridWindos弹出了.

19:在GridWindos中创建颜色字段.

可以在这个窗口中添加任何东西,现在让我们为栅格类添加一个颜色字段,以便后面我们来编辑它.

public class Grid : MonoBehaviour
{
    public float width = 32.0f;
    public float height = 32.0f;

    public Color color = Color.white;

在OnDrawGizmos()中赋给Gizmos.color.

void OnDrawGizmos()
{
    Vector3 pos = Camera.current.transform.position;
    Gizmos.color = color;

在GridWindos脚本里的OnGUI()添加一个颜色拾取器字段用来我们拾取颜色.

public class GridWindow : EditorWindow
{
    Grid grid;

    public void Init()
    {
        grid = (Grid)FindObjectOfType(typeof(Grid));
    }

    void OnGUI()
    {
        grid.color = EditorGUILayout.ColorField(grid.color, GUILayout.Width(200));
    }
}

点击测试一下改变栅格的颜色.

20:添加Delegate

我们设置了一个Delegate,用=号得到场景视图的输入事件,这样做不太好,因为它涵盖了所有其他回调.我们可以用+=符号代替=号.下面修改GridEditor.cs脚本.

public void OnEnable()
{
    grid = (Grid)target;
    SceneView.onSceneGUIDelegate += GridUpdate;
}

我们还需要创建一个OnDisable()的回调来替换掉GridUpdate(),否则会被执行很多次.

public void OnEnable()
{
    grid = (Grid)target;
    SceneView.onSceneGUIDelegate += GridUpdate;
}

public void OnDisable()
{
    SceneView.onSceneGUIDelegate -= GridUpdate;
}

结论:
这篇是编辑器编程的基础教程,如果你想扩展这方面的知识,可以查看脚本参考(http://unity3d.com/support/documentation/ScriptReference/index.html)中的Resources,和AssetDatabase和FileUtil类.

原文:http://active.tutsplus.com/tutorials/workflow/how-to-add-your-own-tools-to-unitys-editor/
由威阿翻译,转载请注明来自1Vr.Cn,否则MJJ.

发布于 :未分类

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注