Unity编辑器扩展 | 编辑器扩展基础入门-腾讯云开发者社区
编辑器拓展
必要操作
- 在Assets下新建Editor目录
菜单
添加
using UnityEditor;
public class Chap1Menu
{
[MenuItem("Learn/TestMenu")]
static void TestMenu()
{
// 菜单项点击回调
}
}
验证菜单项是否可点(第2个参数)
[MenuItem("Learn/TestMenu")]
static void TestMenu()
{
// 菜单项点击回调
}
[MenuItem("Learn/TestMenu", true)]
static bool TestMenuValidate()
{
return false;
}
排序(第3个参数)
[MenuItem("Learn/TestMenu", false, 1)]
static void TestMenu()
{
// 菜单项点击回调
}
[MenuItem("Learn/TestMenu2", false, 0)]
static void TestMenu2()
{
// 菜单项点击回调
}
⬆️添加分隔栏
[MenuItem("Learn/TestMenu", false, 11)]
static void TestMenu()
{
// 菜单项点击回调
}
[MenuItem("Learn/TestMenu2", false, 0)]
static void TestMenu2()
{
// 菜单项点击回调
}
在Inspector指定组件菜单中添加
[MenuItem("CONTEXT/Transform/Got/Qux")]
static void ContextMenu(MenuCommand cmd)
{
var transform = cmd.context as Transform;
if (transform != null)
{
Undo.RecordObject(transform, "Translate");
transform.Translate(1, 0, 0);
}
}
在层级视图中添加
[MenuItem("GameObject/Foo/Bar")]
static void HierarchyMenu() { }
在Project视图中添加
[MenuItem("Assets/Cat/Dog")]
static void ProjectMenu() { }
场景视图中添加
⬇️SceneView.duringSceneGui添加回调
using UnityEditor;
public class Chap2ViewMenu
{
[InitializeOnLoadMethod]
static void OnLoad()
{
SceneView.duringSceneGui += (sv) =>
{
if (Event.current != null
&& Event.current.button == 1
&& Event.current.type == EventType.MouseUp)
{
// 定义菜单位置
Vector2 mousePos = Event.current.mousePosition;
Rect menuPos = new(mousePos.x, mousePos.y, 0, 0);
// 定义菜单项列表数据
GUIContent[] menuOpts = new GUIContent[]{
new("Foo"),
new("Bar/Qux"),
};
// 显示菜单
EditorUtility.DisplayCustomMenu(menuPos, menuOpts, -1,
(data, opt, selected) =>
{
Debug.LogFormat(@"data: {0}, opt: {1},
selected: {2}, value: {3}",
data, opt, selected, opt[selected]);
}, null);
// 解决右键后光标显示异常的问题
Event.current.Use();
}
};
}
}
快捷键
[MenuItem("Learn/TestMenu %9")]
static void TestMenu()
{
}
[MenuItem("Learn/TestMenu2 %#&i")]
static void TestMenu2()
{
}
| windows | macos | |
|---|---|---|
| % | ctrl | ⌘ |
| \# | shift | ⇧ |
| & | alt | ⌥ |
| UP/DOWN/LEFT/RIGHT | ||
| F1-F12 | ||
| HOME/END | ||
| PGUP/PGDN |
拓展Project视图
选中项后添加按钮
⬇️EditorApplication.projectWindowItemOnGUI添加回调
[InitializeOnLoadMethod]
static void OnLoad()
{
EditorApplication.projectWindowItemOnGUI += (string guid, Rect rect) =>
{
Object actObj = Selection.activeObject;
if (actObj != null)
{
// 有选中项的话
string objPath = AssetDatabase.GetAssetPath(actObj);
string selGuid = AssetDatabase.AssetPathToGUID(objPath);
if (selGuid == guid)
{
// 自身是选中项
rect.x = rect.width / 2;
rect.width = 100;
if (GUI.Button(rect, "删除"))
{
// 添加按钮
// 点击弹出确认框
if (EditorUtility.DisplayDialog(
"",
$"确认删除 {actObj.name} 吗?",
"确定",
"取消"
))
{
// 删除资源
AssetDatabase.DeleteAsset(objPath);
}
}
}
}
};
}
资源操作回调(增/改/删/移)
⬇️继承AssetModificationProcessor
using UnityEditor;
public class Chap4ProjectViewEvt : AssetModificationProcessor
{
public static void OnWillCreateAsset(string path)
{
Debug.Log("资源被创建: " + path);
}
public static string[] OnWillSaveAssets(string[] paths)
{
Debug.Log("资源被保存: " + string.Join(", ", paths));
return paths;
}
public static AssetMoveResult OnWillMoveAsset(string sourcePath, string destinationPath)
{
Debug.Log("资源被移动: " + sourcePath + " -> " + destinationPath);
return AssetMoveResult.DidNotMove;
}
public static AssetDeleteResult OnWillDeleteAsset(string path, RemoveAssetOptions options)
{
// 每次删除前都会确认一下
string objName = System.IO.Path.GetFileNameWithoutExtension(path);
if (EditorUtility.DisplayDialog(
"",
$"确认删除 {objName} 吗?",
"确定",
"取消"
))
{
// AssetDatabase.DeleteAsset(path);
Debug.Log("资源被删除: " + path);
return AssetDeleteResult.DidNotDelete;
}
return AssetDeleteResult.DidDelete;
}
}
拓展Hierarchy视图
选中项后添加按钮
⬇️EditorApplication.hierarchyWindowItemOnGUI添加回调
using UnityEditor;
public class Chap5ExtendHierarchyView
{
[InitializeOnLoadMethod]
static void OnLoad()
{
EditorApplication.hierarchyWindowItemOnGUI += (int instanceID, Rect rect) =>
{
Object actObj = Selection.activeObject;
if (actObj != null && instanceID == actObj.GetInstanceID())
{
// 自身是选中的对象
rect.x = rect.xMax / 2 + 70;
rect.width = 70;
if (GUI.Button(rect, "删除"))
{
// 支持撤销的删除操作
Undo.DestroyObjectImmediate(actObj);
}
}
};
}
}
自定义菜单(Alt+鼠标左键)
⬇️EditorUtility.DisplayPopupMenu
using UnityEditor;
public class Chap6MyViewMenu
{
[InitializeOnLoadMethod]
static void OnLoad()
{
EditorApplication.hierarchyWindowItemOnGUI +=
(int instanceID, Rect rect) =>
{
if (Event.current != null
&& Event.current.modifiers.HasFlag(EventModifiers.Alt)
// mac 上鼠标右键好像检测不了
// && Event.current.button == 1
&& Event.current.type == EventType.MouseUp)
{
// 消费掉事件,避免事件传递到Hierarchy上
Event.current.Use();
// 显示自己的菜单
var mousePos = Event.current.mousePosition;
Rect popupRect = new Rect(mousePos.x, mousePos.y, 0, 0);
// 显示的是之前创建好的 Learn 菜单
EditorUtility.DisplayPopupMenu(popupRect, "Learn", null);
}
};
}
}
⬆️已经创建好的菜单
拓展Inspector(指定组件)
using UnityEditor;
[CustomEditor(typeof(Transform))]
public class Chap7ExtendInspectorView : Editor
{
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
var transform = target as Transform;
var btnStyle = new GUIStyle(GUI.skin.button)
{
fixedWidth = 100,
};
if (GUILayout.Button("Foo", btnStyle))
{
// 点击按钮移动目标(支持撤销)
Undo.RecordObject(transform, "Move Transform");
transform.Translate(1, 0, 0);
}
}
}
📌UI Toolkit的实现方法:扩展至脚本的Inspector中
拓展Scene视图
自定义Gizmos
⬆️挂了脚本的对象,显示自定义Gizmos
public class Chap8Gizmos : MonoBehaviour
{
// 注意脚本不放在Editor目录中
// 只在编辑器下生效
#if UNITY_EDITOR
// 选中对象才会绘制
private void OnDrawGizmosSelected()
{
Gizmos.color = Color.red;
Gizmos.DrawLine(
transform.position,
transform.position + Vector3.up * 2
);
}
// 任何情况都绘制
private void OnDrawGizmos()
{
// 在对象上面绘制一个半透明的圆
Gizmos.color = new Color(0, 0, 1, 0.5f);
var pos = transform.position + Vector3.up * transform.localScale.y;
Gizmos.DrawSphere(pos, 0.5f);
// 绘制一个图标
Gizmos.DrawIcon(pos, "Assets/UI/Sprite/Star Center.png");
}
#endif
}
自定义UI
⬆️选中挂了指定组件的对象
⬇️实现OnSceneGUI方法
using UnityEditor;
[CustomEditor(typeof(BoxCollider))]
public class Chap9SceneViewGUI : Editor
{
public void OnSceneGUI()
{
// 下面这句注释掉好像也没问题(下同)
// Handles.BeginGUI();
GUILayout.BeginArea(new Rect(50, 10, 150, 50));
if (GUILayout.Button("Test Button"))
{
Debug.Log("Button clicked: " + target.name);
}
GUILayout.EndArea();
// Handles.EndGUI();
}
}
⬆️一直显示的UI
⬇️给SceneView.duringSceneGui添加回调
[InitializeOnLoadMethod]
static void OnLoad()
{
SceneView.duringSceneGui += (sceneView) =>
{
GUILayout.BeginArea(new Rect(50, 10, 150, 500));
if (GUILayout.Button("一直显示", GUILayout.Width(80)))
{
Debug.Log("Button clicked");
}
GUILayout.EndArea();
};
}
拓展Game视图
⬇️实现OnGUI方法,挂在场景对象上
[ExecuteInEditMode]
public class Chap10ExtendGameView : MonoBehaviour
{
private void OnGUI()
{
GUI.Box(new Rect(10, 10, 100, 90), "关卡列表");
if (GUI.Button(new Rect(20, 40, 80, 20), "关卡1"))
{
Debug.Log("开始关卡1");
}
if (GUI.Button(new Rect(20, 70, 80, 20), "关卡2"))
{
Debug.Log("开始关卡2");
}
}
}
编辑器窗口(EditorWindow)
添加自定义菜单
⬇️实现IHasCustomMenu接口
public class TestWindow : EditorWindow , IHasCustomMenu
{
public void AddItemsToMenu(GenericMenu menu)
{
menu.AddItem(new GUIContent("test1", false, () => {
Debug.Log("test1 clicked");
});
menu.AddDisabledItem(new GUIContent("test2"));
}
}
预览对象
⬇️Editor.CreateEditor(target)和targetEditor.OnPreviewGUI
public class TestPreview : EditorWindow
{
private Object target;
private Object currTarget;
private Editor targetEditor;
private void OnGUI()
{
// 选择目标对象
target = EditorGUILayout.ObjectField(target, typeof(Object), false);
if (target != null && currTarget != target)
{
// 给目标对象创建一个编辑器
targetEditor = Editor.CreateEditor(target);
currTarget = target;
}
if (targetEditor != null && targetEditor.HasPreviewGUI())
{
// 目标有预览的话,显示预览
targetEditor.OnPreviewGUI(
GUILayoutUtility.GetRect(400, 400),
EditorStyles.label
);
}
}
}
常用工具类
2024-02-25 Unity 编辑器开发之编辑器拓展5 —— Selection
2024-02-25 Unity 编辑器开发之编辑器拓展6 —— Event
2024-02-25 Unity 编辑器开发之编辑器拓展7 —— Inspector
2024-06-08 Unity 编辑器开发之编辑器拓展9 —— EditorUtility
2024-06-10 Unity 编辑器开发之编辑器拓展10 —— 其他常见工具类
UI Toolkit
优点
- 标准UI开发工作流
- Flexbox系统,轻松实现UI自动适配
- USS样式表,让UI样式的修改更便捷
- 高性能
- 高适用性
- 适合JRPG项目
缺点
- 不依赖GameObject,难以制作旋转在3D世界中的可互动UI
- 不支持Shader,难以制作特效
- 不支持Animator组件,无法制作实时循环动画(自带了Transition系统)
2023.2新功能
数据绑定
通过UI Builder
父元素共享数据给子元素
- 给父节点设置Data Source,所有子元素的默认Data Source会自动使用父节点的
创建属性(用于暴露想绑定的非序列化字段)
public class CharInfoData : ScriptableObject
{
[SerializeField] string charName;
[SerializeField] int charLvl;
// ...
// 不这样处理的话,UI Builder中将无法找到这个属性
[CreateProperty] string CharLvlString => $"lvl: {charLvl}";
}
隐藏属性(与上面的作用相反)
public class CharInfoData : ScriptableObject
{
// 隐藏下面2个属性
[DontCreateProperty, SerializeField] string charName;
[DontCreateProperty, SerializeField] int charLvl;
// ...
}
通过UXML进行绑定
其实就是通过UI Builder来绑定的结果
在脚本中进行绑定
- 挂了UIDocument组件的对象挂上此脚本
public class CharInfoUI : MonoBehaviour
{
[SerializeField] CharInfoData data;
void OnEnable()
{
VisualElement root = GetComponent<UIDocument>().rootVisualElement;
Label healthLabel = root.Q<Label>("charHealthText");
// 给目标UI控件设置数据绑定
healthLabel.SetBinding(nameof(healthLabel.text), new DataBinding
{
dataSource = data,
dataSourcePath = new PropertyPath(nameof(data.HealthString)),
bindingMode = BindingMode.ToTarget
})
}
}
创建自定义控件
- 使用
UxmlElement特性和partial关键字,就能将这个控件暴露至UI Builder
[UxmlElement]
public partial class HealthBar : VisualElement
{
readonly VisualElement background;
readonly VisualElement foreground;
public HealthBar()
{
background = new VisualElement();
foreground = new VisualElement();
Add(background);
background.Add(foreground);
// 将 foreground 的高设为 100%
foreground.style.height =
new StyleLength(new Length(100, LengthUnit.Percent));
}
}
声明自定义控件属性
- 使用
UxmlAttribute特性将属性暴露到UI Builder中 - 如果需要为该属性绑定数据,则需要添加
CreateProperty
[UxmlElement]
public partial class HealthBar : VisualElement
{
// ...
int fillPercent = 50;
[CreateProperty, UxmlAttribute, Range(0, 100)]
int FillPercent
{
get => fillPercent;
set
{
fillPercent = value;
// 更新宽度百分比
foreground.style.width =
new StyleLength(new Length(fillPercent, LengthUnit.Percent));
}
}
public HealthBar()
{
// ...
}
}
编辑器扩展
EditorWindow
EditorWindow - Unity 脚本 API 从此类派生以创建编辑器窗口
- 计划任务
var schedule = root.schedule.Execute(onSchedule);
// schedule.ExecuteLater
// schedule.Every
// schedule.ForDuration
扩展至脚本的Inspector中
Unity - Manual: Create a binding with the Inspector
⬆️效果图
步骤(以扩展TestScript脚本为例):
- (不在Assets/Editor下)新建组件脚本TestScript,并新建两个公共属性(myName和myScale)
⬇️TextScript.cs(挂对象上的)
[ExecuteInEditMode]
public class TestScript : MonoBehaviour
{
public string myName = "Foo";
public float myScale = 1.0f;
void Update()
{
gameObject.name = myName;
gameObject.transform.localScale = new Vector3(myScale, myScale, myScale);
}
}
- 在Assets/Editor下新建脚本
⬇️MyInspector.cs(拓展编辑器的)
using UnityEditor;
[CustomEditor(typeof(TestScript))]
public class MyInspector : Editor
{
[SerializeField]
VisualTreeAsset visualTree;
public override VisualElement CreateInspectorGUI()
{
var ve = visualTree.CloneTree();
return ve;
}
}
- 在Assets/Editor下新建UI文档并编辑
- 添加属性字段(Property Field)
- 绑定脚本中的属性名
- 添加属性字段(Property Field)
- 在Project视图中选中MyInspector脚本
- 将上面编辑好的UI文档(.uxml)拖到Inspector中的Visual Tree 中
- 将 .uss文件拖到Inspector中的Style Sheet 中(如有)
运行时UI
自定义控件中添加模板
- 将UI模板文件放在Resources目录下,以方便加载。如:
注意:Resources目录并非一定要在Assets下
using UnityEngine.UIElements;
public class CharDataItem : VisualElement
{
private TemplateContainer tpl;
public CharDataItem()
{
// 模板路径(相对于Resources)
string tplPath = "Foo/TplPropItem";
// 加载并实例化
tpl = Resources
.Load<VisualTreeAsset>(tplPath)
.Instantiate();
// 设置模板样式,让它自动撑满
tpl.style.flexGrow = 1f;
// 添加到自身上
hierarchy.Add(tpl);
}
}
将自定义控件暴露至UI Builder
⬆️可直接在UI Builder中添加自定义控件
方法:在类中添加一句代码即可
public class CharDataItem : VisualElement
{
// 暴露到UI Builder的Project中
public new class UxmlFactory : UxmlFactory<CharDataItem> { }
//...
}
自定义控件绑定数据
方法:
- 列表Item绑定
单个数据并展示
public class CharDataItem : VisualElement
{
// ...
private List<VisualElement> propList;
public CharDataItem() { ... }
// 添加带数据参数的构造函数
public CharDataItem(CharData data) : this()
{
// 绑定数据
userData = data;
// 更新固定信息(如形象、名字)
// ...
// 更新不固定信息
propList = tpl.Query("TplPropItem").ToList();
UpdateCharProps();
}
private void UpdateCharProps()
{
CharData data = (CharData)userData;
SetCharProp(0, "等级", data.Lvl);
SetCharProp(1, "行动力", data.CharStat.initiative);
// ...
}
private void SetCharProp(int propItemIdx, string propName, int propValue)
{
VisualElement propItem = propList[propItemIdx];
propItem.Q<Label>("propNameLbl").text = propName;
propItem.Q<Label>("propValueLbl").text = propValue.ToString();
}
}
- 面板展示
全部数据
public class PartyPanelCtrl : MonoBehaviour
{
[SerializeField] PartyData partyData;
VisualElement rootVe;
private void Awake()
{
rootVe = GetComponent<UIDocument>().rootVisualElement;
}
private void Start()
{
var bodyBox = rootVe.Q("bodyBox");
bodyBox.Clear();
foreach (CharData cdata in partyData.CharDataList)
{
var item = new CharDataItem(cdata);
bodyBox.Add(item);
}
}
}
- 给挂有UI Document组件的对象挂上上面的组件,设置好数据
用户交互处理-操纵器(Manipulator)
public CharDataItem(CharData data) : this()
{
// ...
UpdateCharProps();
// 添加鼠标左键点击的回调
Clickable leftClick = new Clickable(OnMouseClick);
leftClick.activators.Clear();
leftClick.activators.Add(new ManipulatorActivationFilter()
{
button = MouseButton.LeftMouse,
});
tpl.AddManipulator(leftClick);
}
private void OnMouseClick(EventBase evt)
{
var mevt = (MouseUpEvent)evt;
int key = mevt.button;
if (key == (int)MouseButton.LeftMouse)
{
// 让绑定的角色数据升一级
((CharData)userData).LvlUp();
}
else
{
((CharData)userData).LvlDown();
}
// 更新角色信息
UpdateCharProps();
}
用户交互处理-事件系统
public CharDataItem(CharData data) : this()
{
// ...
UpdateCharProps();
// 添加鼠标点击的回调
tpl.RegisterCallback<MouseUpEvent>(OnMouseClick);
}
private void OnMouseClick(MouseUpEvent evt)
{
if (evt.button == (int)MouseButton.LeftMouse)
{
((CharData)userData).LvlUp();
}
else
{
((CharData)userData).LvlDown();
}
UpdateCharProps();
}