Unity 编辑器扩展七 Property Attributes
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity 编辑器扩展七 Property Attributes相关的知识,希望对你有一定的参考价值。
参考技术A 参考
Unity Editor 基础篇(七):Property Attributes
我们可以对ReadOnly这种标签在面板上的显示做自定义,对比效果如下:
在Scripts文件夹中创建两个C#脚本,分别命名为:“ReadOnlyAttribute.cs”和“TestPropertyAttr.cs”
继承PropertyAttribute这个类,就能用一个自定义的PropertyDrawer类去控制继承PropertyAttribute类的变量在Inspector面板中的显示。在IDE中,按下CTRL,再点击TestPropertyAttr 中的[ReadOnly]标签,会发现跳到了ReadOnlyAttribute.cs中。 可以理解为,我们继承PropertyAttribute这个类,实现了自己定义一个标签。
在Editor文件夹中创建一个名为“ReadOnlyAttributeDrawer.cs”的脚本
MyString的颜色未生效
Unity编辑器扩展
文章目录
继承自 MonoBehaviour 的类的一些扩展
一方面继承自MonoBehaviour的类都会默认被添加到 Component->Scripts 节点下。
另一方面MonoBehaviour类都可以添加到GameObject作为Component,在Inspector界面可以显示该类的一些信息。
所有继承自UnityEngine.Object的类,如GameObject,Component,MonoBehaviour,Texture2D,AnimationClip;所有基本类型,如int,string,float,bool;一些内建类型,如Vector2,Vector3,Quaternion,Color,Rect,Layermask;序列化类型的Array,序列化类型的List;枚举Enum
。默认情况下这些类型的public变量是可以在Inspector界面显示的,同时编辑器对其进行序列化。
一些涉及到变量显示和序列化的属性
- [HideInInspector] 变量依旧可以被序列化,但是在Inspector界面上不可见
- [NonSerialized] 变量不可被序列化,且在Inspector面板上不可见
- [SerializeField] 可以把private类型变量变成可序列化的,在Inspector面板可见。(虽然Inspector面板可见,但仍然是private变量)
ContextMenu属性
继承自MonoBehaviour的类的内部,可以为其函数添加ContextMenu
属性,在编辑器界面下执行一些操作。
public class TestContextMenu : MonoBehaviour
[ContextMenu("DoLogTest")]
void DoLogTest()
Debug.Log("i am function DoLogTest");
如上述代码,当把TestContextMenu脚本附加到GameObject上后,在GameObject的Inspector界面中会有TestContextMenu组件的信息,右键该组件(或者左键点击3个.按钮)会看到DoLogTest菜单项,点击后会按照预期打印日志。
ContextMenuItem属性
可以为变量(public)添加右键弹出命令,从而执行相关的操作。
该标识接受两个变量,1个是display name,一个是右键弹出菜单点击后的方法。
public class Test : MonoBehaviour
[ContextMenuItem("Random Age", "RandomAge")]
public int Age;
void RandomAge()
Age = new System.Random(DateTime.Now.Millisecond).Next(1, 100);
[ContextMenuItem("Random Name", "RandomName")]
public string Name;
private void RandomName()
string[] names = new string[] "Jack", "Jim", "Tomas", "Han", "Ann" ;
Name = names[new System.Random(DateTime.Now.Millisecond).Next(0, 4)];
上述代码分别为Age和Name这两个字段添加了右键弹出菜单的功能。
Range属性
[Range(min,max)]或者[RangeAttribute(min,max)]可以对变量的输入范围进行限定,使得Inspector检视面板内的数值输入框变成Slider,且范围为(min,max)。
Header属性
[Header(“”)] Inspector面板中在目标字段顶部展示额外的说明文字
窗口扩展基础:MenuItem,ScriptableWizard,EditorWindow
MenuItem
使用MenuItem标识可以为编辑器添加新的菜单,也可以为Inspector上下文添加菜单。点击后执行一些特定的逻辑,没有额外的操作界面。只有静态方法可以使用该标识,该标识可以把静态方法转换为菜单命令。
另外可以为菜单创建快捷方式(hotkey),你可以使用如下特殊修饰字符:%(Windows系统上的ctrl,OS X系统上的cmd),#(shift),&(alt)。比如快捷方式“shift-alt-g”的写法为[MenuItem(“XX/XX/XXX #&g”)]。如果不需要为hotkey提供修饰符需要用字符_修饰,如快捷方式“g”的写法为[MenuItem(“XX/XX/XXX _g”)]
另外还可以支持一些特殊的字符:LEFT、RIGHT、UP、DOWN、F1…F12、HOME、END、PGUP、PGDN。
示例代码:
using UnityEngine;
using System.Collections;
using UnityEditor;
public class AddChild
[MenuItem("Custom/Create Child For Selected GameObjects")]
static void MenuAddChild()
Transform[] transforms = Selection.GetTransforms(SelectionMode.TopLevel | SelectionMode.OnlyUserModifiable);
foreach (Transform transform in transforms)
GameObject aChild = new GameObject("_Child");
aChild.transform.parent = transform;
[MenuItem("Custom/Create Child For Selected GameObjects", true)]
static bool ValidateMenuAddChild()
return Selection.activeGameObject != null;
上述示例可以在Unity菜单项创建新的目录菜单:Custom/Create Child For Selected GameObjects
,同时提供了Validate函数确保当前若没有物体被选中的话,该菜单项是灰化不可点击的。
需要注意的是,入口函数和Validate函数都必须是静态的,Validate函数名由Validate和入口函数名组合而成,Validate函数的MenuItem标识中的第1个参数和入口函数的一致,第2个参数为true。
排序、分组
MenuItem函数原型为:public MenuItem(string itemName, bool isValidateFunction, int priority);易知通过控制第3个参数便可以设置分组。
第3个int参数,值越小越排在上面,Unity会自动分组,50为单位。如下Option1和Option2会自动被分成不同的组,具体表现就是有个分割线。
[MenuItem("NewMenu/Option1", false, 3)]
private static void NewMenuOption1()
[MenuItem("NewMenu/Option2", false, 51)]
private static void NewMenuOption2()
勾选
下述代码实现Test1和Test2两项,互斥选择,且默认情况下选择Test1
[InitializeOnLoad]
public class TestMenuItem
static string menuPath1 = "Custom/Test1";
static string menuPath2 = "Custom/Test2";
static TestMenuItem()
//默认选择Test1
if (Menu.GetChecked(menuPath1) == false && Menu.GetChecked(menuPath2) == false)
Menu.SetChecked(menuPath1, true);
[MenuItem("Custom/Test1", false, 1)]
static void Test1()
if (Menu.GetChecked(menuPath1)==false)
Menu.SetChecked(menuPath1, true);
Menu.SetChecked(menuPath2, false);
[MenuItem("Custom/Test2", false, 2)]
static void Test2()
if (Menu.GetChecked(menuPath2)==false)
Menu.SetChecked(menuPath1, false);
Menu.SetChecked(menuPath2, true);
一些特定目录结构
上述提到通过MenuItem标识可以为Unity创建新的目录,不仅如此还可以为已有的菜单增加内容。
- MenuItem(“GameObject/”) 新建菜单项出现在GameObject节点下。
- MenuItem(“Assets/”) 新建菜单项出现在Assets节点下(Project面板下右键弹出的面板内容和Assets节点展开的是一致的)
- MenuItem(“CONTEXT/组件名/XXX”) 可以为某个具体组件的Inspector上下文菜单增加内容。打开方式为右键组件或者左键组件最右边的“齿轮”按钮,需要注意的是Inspector上下文添加菜单不支持多重层次。
如下代码为Transform组件扩展其Inspector上下文菜单:
public class AddChild
[MenuItem("CONTEXT/Transform/Create Child For Selected GameObjects")]
static void MenuAddChild()
Transform[] transforms = Selection.GetTransforms(SelectionMode.TopLevel | SelectionMode.OnlyUserModifiable);
foreach (Transform transform in transforms)
GameObject aChild = new GameObject("_Child");
aChild.transform.parent = transform;
[MenuItem("CONTEXT/Transform/Create Child For Selected GameObjects", true)]
static bool ValidateMenuAddChild()
return Selection.activeGameObject != null;
如下代码为获得Inspector上下文菜单对应的组件:
[MenuItem("CONTEXT/Rigidbody/DoSomething")]
static void DoSomething(OnInspectorGUI command)
Rigidbody body=command.context;
body.mass=5;
代码执行菜单项
EditorApplication.ExecuteMenuItem(“XX/XX/XX”)
ScriptableWizard
通过继承ScriptableWizard可以创建编辑器向导,Unity已经为我们封装好了一些变量、方法、消息。如:
- 变量:errorString(设置向导的错误提示信息)、helpString(设置向导的帮助提示)、isValid(可以控制向导的Create Button和Other Button能否点击)。
- 静态方法:DisplayWizard
- 消息:OnWizardCreate(当点击Create按钮的时候触发)、OnWizardOtherButton(当点击Other按钮的时候)、OnWizardUpdate(当向导打开的瞬间或者向导中有输入参数的变化时触发)。
示例:
public class CreateACube : ScriptableWizard
public float size = 1f;//声明为public可以被向导序列化显示在界面上,且可以改变其值
[MenuItem("Custom/CreateACube")]
static void CreateACubeWizard()
ScriptableWizard.DisplayWizard("创建一个Cube", typeof(CreateACube), "确定", "取消");
//OR
//ScriptableWizard.DisplayWizard<CreateACube>("创建一个Cube", "确定", "取消");
void OnWizardCreate()
GameObject obj = GameObject.CreatePrimitive(PrimitiveType.Cube);
obj.transform.localScale = new Vector3(size, size, size);
void OnWizardOtherButton()
Close();//点击otherButton,需要调用Close()方法,才关闭向导。但是点击createButton,并无需调用Close()方法而自动关闭。
void OnWizardUpdate()
helpString = "输入Cube的Size,Size要大于等于3,创建一个Cube";
if (size < 3)
errorString = "size要大于等于3";
isValid = false;
else
errorString = "";
isValid = true;
EditorWindow
继承自EditorWindow的类,可以实现更复杂的编辑器窗口功能。且这种窗口是可以自由内嵌到Unity编辑器内,共同组成编辑器的Layout。
通过在OnGUI()函数内调用GUILayout、EditorGUILayout、GUI等类的一些方法来实现复杂的界面。
下面通过EditorWindow来实现上述的编辑器向导,相关代码如下:
public class CreateACube2 : EditorWindow
float size = 1f;
string helpString = "输入Cube的Size,Size要大于等于3,创建一个Cube";
string errorString = "size需要大于等于3";
bool enableConfromButton = false;
static Color originColor;
[MenuItem("Custom/CreateACube2")]
static void Init()
//获取已经打开的window,如果不存在则new一个
CreateACube2 window = EditorWindow.GetWindow(typeof(CreateACube2)) as CreateACube2;
originColor = GUI.color;
void OnGUI()
GUILayout.Label(helpString, EditorStyles.boldLabel);
size = EditorGUILayout.FloatField("size:", size);
enableConfromButton = size >= 3 ? true : false;
if (enableConfromButton)
GUI.enabled = true;
else
GUI.enabled = false;//设置error的时候Button按钮不可点击
GUI.color=Color.red;//设置errorString的颜色为红色
GUILayout.Label(errorString);
GUI.color = originColor;
if (GUILayout.Button("确定"))
DoCreate();
void DoCreate()
GameObject obj = GameObject.CreatePrimitive(PrimitiveType.Cube);
obj.transform.localScale = new Vector3(size, size, size);
除了可以使用OnGUI()回调,还有如下一些函数可以丰富EditorWindow:
- OnDestroy()当EditorWindow关闭的时候触发。
- OnFocus()当Window获得焦点触发。
- OnHierarchyChange()当Hierarchy面板内有改变的时候触发。
- OnInspectorUpdate()每秒十帧的触发,来判断Inspector界面是否有改变。
- OnLostFocus()当Window失去焦点触发。
- Update()每秒100次的触发,在所有可见的Windows。
更多参考:Editor Windows
Property Drawers
//TODO:
–TODO:https://docs.unity3d.com/2021.3/Documentation/Manual/editor-PropertyDrawers.html
Custom Editors
Custom Editors是相对来说更强大的一种编辑器扩展。主要实现对Inspector界面和SceneView界面的扩展。
下面以一个最简单的实例来说明如何对Inpector界面扩展。
我们定义一个Player:
public class Player : MonoBehaviour
public int armor = 75;
public int damage = 20;
public GameObject gun;
默认情况下,其对应的Inspector界面如下:
要自定义Player组件的Inspector界面,需要新建脚本PlayerEdiotr.cs并继承自Editor。把PlayerEditor.cs放到Editor目录下,代码如下:
[CanEditMultipleObjects]
[CustomEditor(typeof(Player))]
public class PlayerEditor : Editor
SerializedProperty armorProp;
SerializedProperty damageProp;
SerializedProperty gunProp;
void OnEnable()
armorProp = serializedObject.FindProperty("armor");
damageProp = serializedObject.FindProperty("damage");
gunProp = serializedObject.FindProperty("gun");
public override void OnInspectorGUI()
//更新serializedProperty,一般在OnInspector函数的一开始使用。
serializedObject.Update();
//依次重写Player.cs脚本中的armor,damage,gun
EditorGUILayout.IntSlider(armorProp, 0, 100, new GUIContent("Armor"));
if (!armorProp.hasMultipleDifferentValues)//当该值相同的时候才显示ProgressBar
ProgressBar(armorProp.intValue / 100.0f, "ArmorCount");
EditorGUILayout.IntSlider(damageProp, 0, 100, new GUIContent("Damage"));
if (!damageProp.hasMultipleDifferentValues)
ProgressBar(damageProp.intValue / 100.0f, "DamagePower");
EditorGUILayout.PropertyField(gunProp, new GUIContent("Gun Object"));
//使更改生效
serializedObject.ApplyModifiedProperties();
private void ProgressBar(float value, string str)
Rect rect = GUILayoutUtility.GetRect(18, 18, "TextField");
EditorGUI.ProgressBar(rect, value, str);
EditorGUILayout.Space();
重写后界面如下:
上述代码使用了较常用的SerializedProperty和SerializedObject来实现对Inspector界面的重写。
-
[CustomEditor(typeof(Player))] 这个编辑器属性可以把Editor脚本和原始cs脚本建立关系,从而实现重写。
-
[CanEditMultipleObjects] 使用了这个编辑器属性,可以确保当选择的多个物体具有相同组件的时候不会报错“Multi-object editing not supported”,而且可以支持多个物体编辑。
-
OnInspectorGUI() 重写该方法可以实现对默认Inspector界面的重写。该方法也是最常用的一个方法。
还有一种重写Inspector界面的方式
代码如下:
[CanEditMultipleObjects]
[CustomEditor(typeof(Player))]
public class PlayerEditor : Editor
Player player;
//or: Player player get return target as Player;
void OnEnable()
player = target as Player;
public override void OnInspectorGUI()
//base.OnInspectorGUI(); //调用父类方法绘制一次GUI,Player中原本的可序列化数据等会在这里绘制一次。 如果不调用父类方法,则这个Mono的Inspector全权由下面代码绘制。
player.armor = EditorGUILayout.IntSlider("Armor", player.armor, 0, 100);
ProgressBar(player.armor / 100f, "ArmorCount");
player.damage = EditorGUILayout.IntSlider("Damage", player.damage, 0, 100);
ProgressBar(player.damage / 100f, "DamagePower");
bool allowSceneObjects = !EditorUtility.IsPersistent(player);
player.gun = EditorGUILayout.ObjectField("Gun Object", player.gun, typeof(GameObject), allowSceneObjects) as GameObject;
//数据有改变的话SetDirty
if (GUIUnity开发:Material xxx doesn‘t have _Stencil property问题记录