文章目录
前言
本篇文章是对前文关于编辑器拓展的探讨的延伸。即使内置的Unity编辑器再强大,也无法满足所有不同产品和游戏的需求。为了解决这个问题,Unity提供了编辑器拓展的API接口。我们可以通过代码反射的方式修改内置的系统编辑器,同时,游戏开发者也可以利用EditorGUI接口编写适合自己的专属游戏编辑器。这涵盖了从简单的一键换字体、材质、一键打包、管理、优化,到复杂的技能编辑器、关卡编辑器等功能。
特别需要注意的是,由于内容涉及较多且较为复杂,会分2节进行详细讨论。在本文的第一部分中,我们将总结最基础和最实用的编辑器拓展知识。
本文所有代码均在Gitee参考工程,如有需要请自取。
一、扩展Project视图
Project视图是掌握Unity项目的生死大权的地方,包括创建、删除等重要操作。在这里,我们可以通过右键点击实现Asset菜单的拓展。在进行这项任务之前,首先需要将脚本文件保存到名为Editor的文件夹下,并引入UnityEditor命名空间。
1、右键扩展菜单(Asset)
右键创建物体
csharp
using UnityEngine;
using UnityEditor;
public class AssetEditor
{
[MenuItem("Assets/Tools/CreateSphere",false,1)]//数值越小越靠前
static void Createxx() {
GameObject.CreatePrimitive(PrimitiveType.Sphere);
}
}
如图我点击CreateSphere按钮就创建了一个球体到场景当中。
2、监听事件
在大型或规范的项目中,通常会有严格的项目规范,包括对资源的归类等方面。例如,如果你将贴图移动到了脚本文件夹,项目可能会判断这样的操作是不合法的,并阻止你进行修改。
1、监听资源的删除、创建、移动、保存等操作,在进行操作后会输出绑定的委托。
csharp
//监听事件
[InitializeOnLoadMethod]
static void InitializeOnLoadMethod() {
EditorApplication.projectChanged += delegate ()
{
Debug.Log("怎么回事,老弟。你是不是刚动了资源?");
};
}
嘿嘿,知识点还没完,[InitializeOnLoadMethod]写在方法 前则会使该方法在C#代码编译完成后首先调用。
2、当需要重新具体的删除、创建方法时必须继承UnityEditor.AssetModificationProcessor,具体方法如下:
csharp
public class AssetEventEditor : UnityEditor.AssetModificationProcessor
{
//监听事件
[InitializeOnLoadMethod]
static void InitializeOnLoadMethod()
{
EditorApplication.projectChanged += delegate ()
{
Debug.Log("怎么回事,老弟。你是不是刚动了资源?");
};
}
//监听"双击左键打开资源"事件
public static bool IsOpenForEdit(string assetPath, out string message)
{
message = null;
Debug.LogFormat("assetPath:{0}", assetPath);
return true;//true表示该资源可以打开,false表示不允许打开
}
//监听"资源即将被创建"事件
public static void OnWillCreateEdit(string path)
{
Debug.LogFormat("创建资源的路径:{0}", path);
}
//监听"资源即将被保存"事件
public static string[] OnWillSaveAssets(string[] paths) {
if (paths != null)
{
Debug.LogFormat("保存资源的路径:{0}",string.Join(",",paths));
}
return paths;
}
//监听"资源即将被移动"事件
public static AssetMoveResult OnWillMoveAsset(string oldPath,string newPath) {
Debug.LogFormat("资源从路径{0}移动到路径{1}", oldPath,newPath);
return AssetMoveResult.DidNotMove;//DidNotMove表示可以移动,DidMove表示不可以移动
}
//监听"资源即将被删除"事件
public static AssetDeleteResult OnWillDeleteAsset(string assetPath) {
Debug.LogFormat("资源从路径{0}删除", assetPath);
return AssetDeleteResult.DidNotDelete;//DidNotDelete表示可以移动,DidDelete表示不可以移动
}
}
3、拓展布局
选中资源后出现按钮,并监听按钮的点击事件
csharp
//选中资源后出现按钮,并监听按钮的点击事件
[InitializeOnLoadMethod]
static void InitializeOnLoadMethod()
{
EditorApplication.projectWindowItemOnGUI = delegate (string guid, Rect selectionRect)
{
//在Project试图中选择一个资源
if (Selection.activeObject && guid == AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(Selection.activeObject)))
{
//设置拓展按钮区域
float width = 80f;
selectionRect.x += (selectionRect.width - width);
selectionRect.y += 2;
selectionRect.width = width;
GUI.color = Color.red;
//点击事件
if (GUI.Button(selectionRect,"click"))
{
Debug.LogFormat("点击:{0}", Selection.activeObject.name);
}
GUI.color = Color.white;
}
};
}
二、扩展Hierarchy视图
在Hierarchy(层次)视图中,右键点击相当于打开菜单栏的GameObject栏目。
1、拓展菜单(GameObject)
细心的读者已经看出来了,下面代码对比上面写的仅仅将菜单栏目从"Assets"换成了"GameObject"。
csharp
//右键创建物体
[MenuItem("GameObject/Tools/CreateSphere", false, 1)]//数值越小越靠前
static void Createxx()
{
GameObject.CreatePrimitive(PrimitiveType.Sphere);
}
2、拓展布局
粗心的读者这下也已经看出来了,下面的代码复刻了之前的代码,将 EditorApplication 后的 GUI 委托修改为 Hierarchy 窗口专属的,并将参数从资源的 GUID 变为 instanceID 实例 ID。此外,按钮引入了本地图片。在各种插件中,编辑器引入图片的操作屡见不鲜,有时为了资源规范会整理插件的图标和图片位置,别忘了根据实际情况修改相关代码。
3、重写菜单
通过以上学习,我们了解了如何在原有基础上扩展编辑器。那么,能否完全重写呢?当然可以。
1、下面,我们将学习如何重新创建 Image 的逻辑。因为在创建 Image 时,Unity 默认会自动勾选 RaycastTarget,如果我们不需要它具有点击功能,就会有额外的性能开销。使用下面的代码,我们可以创建不勾选 RaycastTarget 的 Image。
csharp
//创建Image默认不勾选RaycastTarget
[MenuItem("GameObject/UI/Image0")]
static void CreateImage() {
if (Selection.activeTransform)
{
if (Selection.activeTransform.GetComponentInParent<Canvas>())
{
Image image = new GameObject("image").AddComponent<Image>();
image.raycastTarget = false;
image.transform.SetParent(Selection.activeTransform, false);
//设置选中状态
Selection.activeTransform = image.transform;
}
}
}
完整版会有检测视图是否有Canvas组件,没有则自动创建等功能。
2、重写菜单:
csharp
//重写菜单
[InitializeOnLoadMethod]
static void StartInitializeOnLoadMethod()
{
EditorApplication.hierarchyWindowItemOnGUI += OnHierarchyGUI;
}
static void OnHierarchyGUI(int instanceID, Rect selectionRect)
{
//Event.current监听当前事件,如果监听到鼠标抬起则执行自定义事件(也就是我们的自定义菜单)
if (Event.current != null && selectionRect.Contains(Event.current.mousePosition) && Event.current.button == 1 && Event.current.type <= EventType.MouseUp)
{
GameObject selectedGameObject = EditorUtility.InstanceIDToObject(instanceID) as GameObject;
//判断是否满足条件
if (selectedGameObject)
{
Vector2 mousePosition = Event.current.mousePosition;
EditorUtility.DisplayPopupMenu(new Rect(mousePosition.x, mousePosition.y, 0, 0), "Window/Test", null);
Event.current.Use();
}
}
}
[MenuItem("Window/Test/Test1")]
static void Test1()
{
}
[MenuItem("Window/Test/Test2")]
static void Test2()
{
}
重写完成后,右键视图中的实例将会弹出自定义菜单
三、扩展Inspector视图
Inspector(检视)视图是用来展示组件及资源的详细信息面板。unity自身提供的各类组件的面板能够满足我们正常的需求,但我们偶尔会希望在某些面板上添加快捷按钮或者某些逻辑。
1、扩展原生组件
摄像机是典型的原生组件,我们CustomEditor进行自定义组件,重写OnInspectorGUI在base.OnInspectorGUI()这个原有元素接口上下添加按钮。
csharp
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(Camera))]
public class CameraEditor : Editor
{
public override void OnInspectorGUI()
{
if (GUILayout.Button("拓展按钮-上"))
{
}
base.OnInspectorGUI();
if (GUILayout.Button("拓展按钮-下"))
{
}
}
}
如下便绘制了两个按钮,不过要注意该组件限制了按钮必须加在最上面或者最下面。
2、扩展继承组件
1、Unity将大量的Editor绘制方法封装进了DLL,通常来讲我们无法调用其中方法。想要解决可以使用反射获取内部对象,然后调用想要使用的未公开的方法。
csharp
using System.Reflection;
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(Transform))]
public class TransformEditor : Editor
{
private Editor m_Editor;
private void OnEnable()
{
m_Editor = Editor.CreateEditor(target, Assembly.GetAssembly(typeof(Editor)).GetType("UnityEditor.TransformInspector", true));
}
public override void OnInspectorGUI()
{
if (GUILayout.Button("拓展按钮"))
{
}
m_Editor.OnInspectorGUI();//原有信息面板
// base.OnInspectorGUI();
}
}
2、Context菜单
点击组件的设置按钮(或鼠标右键),会弹出Context菜单,里面有Copy、Reset等操作按钮。我们有时候想对特定组件进行自定义的操作,例如我想在Transform的Context菜单添加NewContext按钮,只需更改MenuItem里第一个参数为"CONTEXT/Transform/NewContext"接口。想给Camer加就将Transform替换成Camera,想给所有组件加就替换成"Compoment"。
csharp
[MenuItem("CONTEXT/Transform/New Context")]
static void NewContext(MenuCommand menuCommand)
{
Debug.LogFormat("组件名称:{0}",menuCommand.context.name);
}
下面演示如何重写特定脚本的系统方法。作者建议最好延迟一帧以防止在编辑模式下代码同步出现问题。在测试 Unity 2021 版本时,貌似没有出现问题。此外,需要注意的是,书中的部分接口可能已经过时,个人已经替换成最新的版本(以2021.3为准)。
csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ContextScript : MonoBehaviour
{
[ContextMenu("Remove Component")]
void RemoveComponent()
{
Debug.Log("RemoveComponent");
//等一帧再删除自己,防止引擎底层错误
UnityEditor.EditorApplication.delayCall = delegate () {
DestroyImmediate(this);
};
}
}
脚本中使用宏定义(使用宏定义的原因是为了在发布后剔除无效代码),联动脚本中的变量在编辑模式下实现功能。下面代码就让NewContext按钮操作了脚本中的变量------将ContextScript脚本中的str变量从"原始"改成了"原神"。
csharp
using System.Collections;
using System.Collections.Generic;
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;
public class ContextScript : MonoBehaviour
{
public string str = "原始";
#if UNITY_EDITOR
//宏定义操作脚本变量
[MenuItem("CONTEXT/ContextScript/New Context")]
static void NewContext(MenuCommand menuCommand)
{
ContextScript contextScript = menuCommand.context as ContextScript;
contextScript.str = "原神";
}
#endif
}
四、扩展Scene视图
Unity的Scene视图是一个用于编辑场景的窗口。在Scene视图中,你可以直观地查看、编辑和组织你的游戏场景。
1、绘制辅助元素
场景编辑中我们有时会需要线段、不同形状的元素来帮助我们快速编辑。下面我们将使用Gizmos.cs工具类绘制简单元素。
csharp
using UnityEngine;
public class GizmoScirpt : MonoBehaviour
{
//在鼠标点击到脚本挂载的物体的身上的时候运行
private void OnDrawGizmosSelected()
{
Gizmos.color = Color.red;
//画线
Gizmos.DrawLine(transform.position, Vector3.one);
//立方体
Gizmos.DrawCube(Vector3.one, Vector3.one);
}
}
我们发现未点击挂载脚本的物体时,立方体和线条消失了。如果想让绘制的物体一直出现,可以使用 OnDrawGizmos 方法。具体用法有很多,比如技能范围展示、地形和陷阱的实际范围绘制等,都能使项目更高效进行。
csharp
//不依赖对象,会一直执行
private void OnDrawGizmos()
{
Gizmos.DrawSphere(transform.position, 2.0f);
}
2、辅助UI
我们在Scene视图中可以在各种组件中添加EditorGUI以获得便利。下面演示如何在Scene中给Camer添加位置信息。
csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(Camera))]
public class GUIEditor : Editor
{
private void OnSceneGUI()
{
Camera camera = target as Camera;
if (camera != null)
{
Handles.color = Color.red;
Handles.Label(camera.transform.position, camera.transform.position.ToString());
Handles.BeginGUI();
GUI.backgroundColor = Color.red;
if (GUILayout.Button("click",GUILayout.Width(200f)))
{
Debug.LogFormat("click = {0}", camera.name);
}
GUILayout.Label("Label");
Handles.EndGUI();
}
}
}
最后注意如果你的脚本不生效,可能是该脚本与CameraEditor脚本互斥冲突,因为都用了"[CustomEditor(typeof(Camera))]",默认先创建的脚本会生效。
3、常驻辅助UI
常驻辅助UI或者说固定辅助UI,顾名思义,无需游戏对象即可常驻Scene视图,有些类似OnDrawGizmosSelected和OnDrawGizmos。
csharp
using UnityEngine;
using UnityEditor;
//常驻辅助UI
public class GUIEditor2 : MonoBehaviour
{
[InitializeOnLoadMethod]
static void InitializeOnLoadMethod() {
SceneView.duringSceneGui += delegate (SceneView sceneView)
{
Handles.BeginGUI();
GUI.Label(new Rect(0, 0, 50f, 50f), "标题");
GUI.Button(new Rect(0, 20f, 50f, 50f), AssetDatabase.LoadAssetAtPath<Texture>("Assets/unity.png"));
Handles.EndGUI();
};
}
}
如下,Scene视图左上角多出了一个UI
五、扩展Game视图
通常来讲运行游戏才能执行脚本的生命周期。如果想在非运行模式下也可以执行脚本,在脚本上添加[ExecuteInEditMode],那么该脚本可以在编辑模式下生效,如果不想在发布后出现可以使用宏定义来剔除。
csharp
using UnityEngine;
#if UNITY_EDITOR
//编辑器模式下依然执行生命周期
[ExecuteInEditMode]
public class GameScript : MonoBehaviour
{
private void OnGUI()
{
if (GUILayout.Button("Click"))
{
Debug.Log("Click");
}
GUILayout.Label("Click!");
}
}
#endif
总结
累死了,本篇详细讲述了 Unity 编辑器的五大视图的拓展方法。原本想分为多篇,但为了整体性,将其整合在一起。下一篇文章字数减少但会更加深入地探讨,并详细解释面板和编辑器源码的相关内容。有不愿透露姓氏的杨姓砖家建议认真阅读完本篇并亲自进行代码试验,然后再查看下一篇。
创作不易,觉得有用的请大家多点赞、评论、收藏,毕竟不收钱,甚至说不定因为哪个知识点恰巧能在面试里帮助到你,提升你的薪资,哈哈。