本篇教程将教你扩展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.