Unity3D游戏开发之Unity3D场景编辑器扩展开发

2015年10月16日 13:22 0 点赞 0 评论 更新于 2017-05-07 00:06

今天,我将和大家分享Unity3D场景编辑器的扩展开发。在之前的文章《Unity3D游戏开发之编辑器扩展程序开发实例》中,我们已经对相关话题有所涉及。今天,我将特别针对场景编辑器的扩展开发进行深入研究。

对于场景编辑器而言,其主要作用是在3D场景视图中实现实时显示、输入反馈以及相关信息的更新。在Unity3D里,提供了EditorEditorWindowGUILayoutEditorGUILayoutGUIUtilityEditorGUIUtilityHandlesEvent等类来完成这些工作。其中,基于EditorWindow的扩展方式我们已经研究过,这种方式拥有独立窗口,通过OnGUI方法进行界面绘制。而今天,我们要探讨的是基于Editor的扩展方式,该方式只能针对脚本,从脚本内容在Inspector里的显示布局到变量在Scene视图的可视化编辑,它都能出色完成。特别要提到的是HandlesEvent这两个类,它们分别提供了3D显示和输入反馈的功能。下面,我们就来学习如何使用这些类扩展Unity3D的场景编辑器。

创建一个扩展的Transform组件

Transform是Unity3D中的一个基本组件,接下来我们要创建一个扩展的Transform组件,该组件可以对游戏体的坐标、旋转、缩放进行重置。

首先,创建一个ExtendTransform类,该类继承自Editor类:

using UnityEngine;
using System.Collections;
using UnityEditor;

[CustomEditor(typeof(Transform), true)]
public class ExtendTransform : Editor
{
/// <summary>
/// Position属性
/// </summary>
private SerializedProperty mPos;

/// <summary>
/// Scale属性
/// </summary>
private SerializedProperty mScale;

void OnEnable()
{
mPos = serializedObject.FindProperty("m_LocalPosition");
mScale = serializedObject.FindProperty("m_LocalScale");
}

/// <summary>
/// Inspector相关GUI函数
/// </summary>
public override void OnInspectorGUI()
{
EditorGUIUtility.labelWidth = 15;
// 获取最新的可序列化对象
serializedObject.Update();
// 绘制物体的坐标、旋转和缩放
DrawPosition();
DrawRotate();
DrawScale();
// 更新可序列化对象的属性
serializedObject.ApplyModifiedProperties();
}

/// <summary>
/// 绘制位置
/// </summary>
private void DrawPosition()
{
GUILayout.BeginHorizontal();
{
bool Reset = GUILayout.Button("P", GUILayout.Width(20f));
EditorGUILayout.LabelField("Position");
EditorGUILayout.PropertyField(mPos.FindPropertyRelative("x"));
EditorGUILayout.PropertyField(mPos.FindPropertyRelative("y"));
EditorGUILayout.PropertyField(mPos.FindPropertyRelative("z"));
if (Reset) mPos.vector3Value = Vector3.zero;
}
GUILayout.EndHorizontal();
}

/// <summary>
/// 绘制旋转
/// </summary>
private void DrawRotate()
{
Vector3 eulerAngles = ((Transform)target).eulerAngles;
GUILayout.BeginHorizontal();
{
bool Reset = GUILayout.Button("R", GUILayout.Width(20f));
EditorGUILayout.LabelField("Rotation", GUILayout.Width(70f));
EditorGUILayout.LabelField("X", GUILayout.Width(13f));
float angleX = EditorGUILayout.FloatField(eulerAngles.x, GUILayout.Width(56f));
EditorGUILayout.LabelField("Y", GUILayout.Width(13f));
float angleY = EditorGUILayout.FloatField(eulerAngles.y, GUILayout.Width(56f));
EditorGUILayout.LabelField("Z", GUILayout.Width(13f));
float angleZ = EditorGUILayout.FloatField(eulerAngles.z, GUILayout.Width(56f));
((Transform)target).eulerAngles = new Vector3(angleX, angleY, angleZ);
if (Reset)
{
eulerAngles = Vector3.zero;
((Transform)target).eulerAngles = Vector3.zero;
}
}
GUILayout.EndHorizontal();
}

/// <summary>
/// 绘制缩放
/// </summary>
private void DrawScale()
{
GUILayout.BeginHorizontal();
{
bool Reset = GUILayout.Button("S", GUILayout.Width(20f));
EditorGUILayout.LabelField("Scale");
EditorGUILayout.PropertyField(mScale.FindPropertyRelative("x"));
EditorGUILayout.PropertyField(mScale.FindPropertyRelative("y"));
EditorGUILayout.PropertyField(mScale.FindPropertyRelative("z"));
if (Reset) mScale.vector3Value = Vector3.one;
}
GUILayout.EndHorizontal();
}
}

这里需要注意两点:

  1. ExtendTransform继承自Editor,这是开发这类编辑器扩展的首要前提。
  2. 在该类的声明位置有[CustomEditor(typeof(Transform), true)]标记,此标记表明这个编辑器扩展是针对Transform组件进行的,即当物体存在Transform组件时,会在编辑器中响应这个扩展程序。

在这个编辑器扩展程序中,我们主要做了两件事:一是实现了OnEnable()方法,该方法相当于初始化方法;二是重写了OnInspectorGUI()方法,此方法会覆盖默认的Inspector窗口外观。

现在,点击场景中默认的相机MainCamera,可以发现默认的Transform会变成具有重置功能的扩展型Transform。下面介绍这段程序的核心内容。

Unity3D中的可序列化对象

通常所说的序列化是将对象实例转化为字符串的过程,而在Unity3D中,可序列化对象更像一种智能对象,它能将脚本中的属性显示在Inspector窗口中,当场景发生变化时,这些属性值会自动更新。例如,我们可以定义如下简单的脚本:

/// <summary>
/// 定义一个可序列化类
/// </summary>
[System.Serializable]
public class ExampleClass
{
[SerializeField]
public int ID;
[SerializeField]
public string Name;
[SerializeField]
public Vector3[] Points;

private bool editable = false;
}

/// <summary>
/// 定义一个简单的脚本
/// </summary>
public class ExampleScript : MonoBehaviour
{
public ExampleClass Example;
}

如果给场景中的某个物体附加该脚本,在Inspector窗口中可以看到Example类的实例被序列化到编辑器面板中。同时,私有的editable字段未被序列化,这是因为在Unity3D中,公有字段默认支持序列化,私有字段除非显式添加[SerializeField]标记,否则不会被序列化。了解这部分内容,是因为它与Editor基类中的属性和方法密切相关。

Editor基类中的属性和方法

Editor基类有两个重要属性:

  • target:表示当前受检查的物体,可通过它获取当前物体。
  • serializedObject:表示当前物体的全部可序列化信息,可通过它获取指定的序列化字段及其数值。

Editor基类的重要方法有:

  • OnInspectorGUI():可对Inspector窗口面板进行扩展或重写。例如,可以通过DrawDefaultInspector()方法绘制默认Inspector窗口面板,再使用GUILayoutEditorGUILayout等辅助类进行自定义绘制。在上述示例中,我们对整个面板进行了重写。需要注意的是,若要重绘该窗口,需确保覆盖此方法,以保证Inspector窗口面板正常工作。
  • OnSceneGUI():可对场景视图进行绘制,实际使用中可配合Handles类和Event类进行网格编辑、地形绘制或高级Gizmos等工作。接下来,我们将利用这一特性编写一个用于NPC寻路的路径节点编辑工具。

对第一个示例的总结

在第一个示例中,我们使用FindProperty()方法获取可序列化物体的属性(字段),再通过EditorGUILayout.PropertyField()方法绘制各种属性框,这种方式能实现属性的自动更新。DrawRotate()方法与DrawPosition()DrawScale()方法实现方式略有不同,原因在于Transform组件的Rotation属性是四元数结构,与我们习惯的欧拉角不同,且目前尚未发现可直接绘制四元数的API接口。因此,我们通过TransformeulerAngles实现,但这种方式绘制的属性框大小与EditorGUILayout.PropertyField()方法绘制的不一致,且需要手动完成属性值的更新。更多细节可通过代码了解。

创建一个NPC寻路节点编辑工具

创建这个工具的想法源于我的实际工作体验。当我在Unity3D中使用的Tween动画库从iTween换成Dotween后,一直未找到类似iTweenPath的路径节点编辑工具。作为开发者,我决定自己设计一个类似的小工具。

以下是实现代码:

using UnityEngine;
using System.Collections;
using UnityEditor;

[CustomEditor(typeof(PatrolNPC))]
public class PatrolPathEditor : Editor
{
/// <summary>
/// 寻路节点
/// </summary>
private Vector3[] paths;

/// <summary>
/// 显示寻路信息的GUI
/// </summary>
private GUIStyle style = new GUIStyle();

/// <summary>
/// 初始化
/// </summary>
void OnEnable()
{
// 获取当前NPC的寻路路径
paths = ((PatrolNPC)target).Paths;
// 初始化GUIStyle
style.fontStyle = FontStyle.Normal;
style.fontSize = 15;
}

void OnSceneGUI()
{
// 获取当前NPC的寻路路径
paths = ((PatrolNPC)serializedObject.targetObject).Paths;
// 设置节点的颜色为红色
Handles.color = Color.red;
if (paths.Length <= 0 || paths.Length < 2) return;
// 在场景中绘制每一个寻路节点
// 可以在场景中编辑节点并将更新至对应的NPC
for (int i = 0; i < paths.Length; i++)
{
paths[i] = Handles.PositionHandle(paths[i], Quaternion.identity);
Handles.SphereCap(i, paths[i], Quaternion.identity, 0.25f);
Handles.Label(paths[i], "PathPoint" + i, style);
if (i < paths.Length - 1)
{
Handles.DrawLine(paths[i], paths[i + 1]);
}
}
}
}

这里的PatrolNPC是一个可寻路NPC类,其Paths字段是Vector3[]类型。当在场景中编辑这些路径节点时,对应NPC的路径节点信息会同步更新,这样就能自由规划NPC的移动路径。

今天的分享就到这里,希望对大家有所帮助。如果大家在开发过程中有任何问题或发现文中有不准确的地方,欢迎交流探讨。

作者信息

洞悉

洞悉

共发布了 3994 篇文章