Unity PropertyDrawer 与 make your lists functional with ReorderableList

 

==============================================

Unity PropertyDrawer做泛型可排序列表

using System.Collections.Generic;

public class ReorderableListBase { }

public class ReorderableList<T> : ReorderableListBase
{
    public List<T> List;
}
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;

[CustomPropertyDrawer(typeof(ReorderableListBase), true)]
public class ReorderableListDrawer : PropertyDrawer
{
    private ReorderableList _list;

    private ReorderableList GetReorderableList(SerializedProperty property)
    {
        if (_list == null)
        {
            var listProperty = property.FindPropertyRelative("List");

            _list = new ReorderableList(property.serializedObject, listProperty, true, true, true, true);

            _list.drawHeaderCallback += delegate (Rect rect)
            {
                EditorGUI.LabelField(rect, property.displayName);
            };

            _list.drawElementCallback = delegate (Rect rect, int index, bool isActive, bool isFocused)
            {
                EditorGUI.PropertyField(rect, listProperty.GetArrayElementAtIndex(index), true);
            };
        }

        return _list;
    }


    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        return GetReorderableList(property).GetHeight();
    }


    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        var list = GetReorderableList(property);

        var listProperty = property.FindPropertyRelative("List");
        var height = 0f;
        for (var i = 0; i < listProperty.arraySize; i++)
        {
            height = Mathf.Max(height, EditorGUI.GetPropertyHeight(listProperty.GetArrayElementAtIndex(i)));
        }

        list.elementHeight = height;
        list.DoList(position);
    }
}
using System;
using UnityEngine;

[Serializable]
public class ReorderableVector3List : ReorderableList<Vector3>
{

}

[Serializable]
public class ReorderableColorList : ReorderableList<Color>
{

}

public class ReorderableListTest : MonoBehaviour
{
    public ReorderableVector3List vector3;
    public ReorderableColorList color;

    // Use this for initialization
    void Start ()
    {

    }

    // Update is called once per frame
    void Update ()
    {

    }
}

 

In Unity 4.5 we got a nice (undocumented) built-in tool to visualize lists in IDE. It's called ReorderableList, it's located in UnityEditorInternal namespace and looks like this:

 

Let's see how to code this beautiful interface in Unity IDE using ReorderableList.

Note: Though UnityEditorInternal namespace is public, it seems to be intended for Unity Team internal use (hence the name), it is not documented and might change in future versions of Unity. If you notice an API change please post it in comments section below.

Sources: Project sources are on GitHub.

Setting up the project

Let's pretend that we are making a Tower Defense-like game and need a decent interface for our game-designer to set up waves of monsters for a particular level.

First, create a new Unity project.

We will need some test assets: mobs and bosses. We'll be placing them in Prefabs/Mobs and Prefabs/Bosses folders. Create several prefabs and put them in these folders. Right now these can be just default cubes and spheres. Give them descriptive names. This is what I came up with:

 

Storing data

Create Scripts folder where we will store our C# scripts. Create two scripts in this folder: LevelData.cs and MobWave.cs.

Add this code to MobWave.cs. This is our value object for storing data for a wave of monsters.

using UnityEngine;
using System;

[Serializable]
public struct MobWave {
	public enum WaveType {
		Mobs,
		Boss
	}

	public WaveType Type;
	public GameObject Prefab;
	public int Count;
}

As you see, every wave can be either a wave of Mobs or one or more Bosses. Every wave has a link to a prefab to clone and a number of copies.

Note: Unity can serialize custom structs since version 4.5.

Add this code to LevelData.cs. This is just a container for our MobWaveobjects.

using UnityEngine;
using System.Collections.Generic;

public class LevelData : MonoBehaviour {
	public List<MobWave> Waves = new List<MobWave>();
}

Add a GameObject to your scene and call it Data. Add LevelData component to this game object and set it up as shown in the GIF animation below.

 

This is basically how lists in Unity IDE look by default. Of course they are not bad, but they become a major pain very quickly when you have many items in a list and want to move them around and add some in the middle.

Building a custom inspector

For every Component in Unity IDE you can create a custom inspector which will change how the component is shown in Inspector tab. Right now we will do this for our LevelData.cs script to change Unity's default list rendering to more functional ReorderableList.

Create Editor folder and a new script inside. Call it LevelDataEditor.cs.

Note: All custom inspectors must be in Editor folder and inherit from Editorclass.

Add this code to the new script:

using UnityEngine;
using UnityEditor;
using UnityEditorInternal;

[CustomEditor(typeof(LevelData))]
public class LevelDataEditor : Editor {
	private ReorderableList list;
	
	private void OnEnable() {
		list = new ReorderableList(serializedObject, 
        		serializedObject.FindProperty("Waves"), 
        		true, true, true, true);
	}
	
	public override void OnInspectorGUI() {
		serializedObject.Update();
		list.DoLayoutList();
		serializedObject.ApplyModifiedProperties();
	}
}

All custom inspector classes have the same important parts:

  1. A custom inspector must inherit from Editor class,
  2. To tell Unity that this is an inspector for LevelData component we must add [CustomEditor(typeof(LevelData))] attribute,
  3. private void OnEnable() method is used for initialization,
  4. public override void OnInspectorGUI() method is called when inspector is redrawn.

In our case in OnEnable we are creating an instance of ReorderableList to draw our Waves property. Don't forget to import UnityEditorInternal namespace:

using UnityEditorInternal;

ReorderableList works with standard C# lists as well as with SerializedProperties. It has two constructors and their more general variants:

  1. public ReorderableList(IList elements, Type elementType),
  2. public ReorderableList(SerializedObject serializedObject, SerializedProperty elements).

In this example we are using SerializedProperty because this is the recommended way of working with properties in custom inspectors. This makes the code smaller and works nicely with Unity and Undo system.

public ReorderableList(
	SerializedObject serializedObject, 
	SerializedProperty elements, 
    bool draggable, 
    bool displayHeader, 
    bool displayAddButton, 
    bool displayRemoveButton);

As you see just by using right parameters we can restrict adding, removing and reordering of items in our list.

Later in the code we just call list.DoLayoutList() to draw the interface.

 

Right now you are probably saying: "Wait, this looks even worse!". But wait a minute, our data structure is complex so Unity doesn't know how to draw it properly. Let's fix this.

Drawing list items

ReorderableList exposes several delegates we can use to customize our lists. The first one is drawElementCallback. It's called when a list item is drawn.

Add this code in OnEnable after the list is created:

list.drawElementCallback = 
	(Rect rect, int index, bool isActive, bool isFocused) => {
	var element = list.serializedProperty.GetArrayElementAtIndex(index);
	rect.y += 2;
	EditorGUI.PropertyField(
    	new Rect(rect.x, rect.y, 60, EditorGUIUtility.singleLineHeight),
        element.FindPropertyRelative("Type"), GUIContent.none);
	EditorGUI.PropertyField(
    	new Rect(rect.x + 60, rect.y, rect.width - 60 - 30, EditorGUIUtility.singleLineHeight),
        element.FindPropertyRelative("Prefab"), GUIContent.none);
	EditorGUI.PropertyField(
    	new Rect(rect.x + rect.width - 30, rect.y, 30, EditorGUIUtility.singleLineHeight),
        element.FindPropertyRelative("Count"), GUIContent.none);
};

Return to Unity. Now all the list items are pretty and functional. Try adding new ones and dragging them around. This is much better, right?

 

If you are not familiar with the syntax and APIs in the code you just pasted, this is a standard C# lambda-expression with Editor GUI methods.

Note: I't important to have a semicolon at the end of a lambda };

Here we are getting the list item being drawn:

var element = list.serializedProperty.GetArrayElementAtIndex(index);

We are using FindPropertyRelative method to find properties of the wave:

element.FindPropertyRelative("Type")

And after that we are drawing 3 properties in one line: TypePrefab and Count:

EditorGUI.PropertyField(
	new Rect(rect.x, rect.y, 60, EditorGUIUtility.singleLineHeight),
    element.FindPropertyRelative("Type"), GUIContent.none);

Note that list header says "Serialized Property". Let's change it to something more informative. To do this we need to use drawHeaderCallback.

Paste this code in OnEnable method:

list.drawHeaderCallback = (Rect rect) => {
	EditorGUI.LabelField(rect, "Monster Waves");
};

Now it feels right.

 

At this stage the list is fully functional but I want to show you how we can extend it even further. Let's see what other callbacks ReorderableList has to offer.

Callbacks

Here are all the callbacks exposed by an instance of ReorderableList:

  • drawElementCallback
  • drawHeaderCallback
  • onReorderCallback
  • onSelectCallback
  • onAddCallback
  • onAddDropdownCallback
  • onRemoveCallback
  • onCanRemoveCallback
  • onChangedCallback

drawElementCallback

Signature: (Rect rect, int index, bool isActive, bool isFocused)

Here you can specify exactly how your list elements must be drawn. Because if you don't this code is used to draw list elements:

EditorGUI.LabelField(rect,
	EditorGUIUtility.TempContent((element == null) ? listItem.ToString() : element.displayName));

drawHeaderCallback

Signature: (Rect rect)

Is used to draw list header.

onReorderCallback

Signature: (ReorderableList list)

Called when an element is moved in the list.

onSelectCallback

Signature: (ReorderableList list)

Called when an element is selected in the list.

onAddCallback

Signature: (ReorderableList list)

Called when the + button is pressed. If this callback is assigned it must create an item itself, in this case default logic is disabled.

onAddDropdownCallback

Signature: (Rect buttonRect, ReorderableList list)

Called when the + button is pressed. If this callback is assigned + button changes to Add more button and onAddCallback is ignored. As with onAddCallback you must create a new list element yourself.

onRemoveCallback

Signature: (ReorderableList list)

Called to remove selected element from the list. If this callback is defined default logic is disabled.

onCanRemoveCallback

Signature: bool (ReorderableList list)

Called when  button is drawn to determine if it should be active or disabled.

onChangedCallback

Signature: (ReorderableList list)

Called when the list is changed, i.e. an item added, removed or rearranged. If data within an item is changed this callback is not called.

Adding selection helper

You can add Debug.Log() to all these callbacks to see when they are called. But now let's make something useful with some of them.

The first one will be onSelectCallback. We'll make that when you select an element the corresponding mob prefab is highlighted in Project panel.

Add this code to OnEnable method:

list.onSelectCallback = (ReorderableList l) => {
	var prefab = l.serializedProperty.GetArrayElementAtIndex(l.index).FindPropertyRelative("Prefab").objectReferenceValue as GameObject;
	if (prefab)
    	EditorGUIUtility.PingObject(prefab.gameObject);
};

The code is pretty simple. We are looking for Prefab property of selected wave and if it's defined we are calling EditorGUIUtility.PingObject method to highlight it in Project panel.

Checking how many waves have left

Let's say that we want to have at least one wave in our list at all times. In other words we want to disable  button if there's only one element in the list. We can do this using onCanRemoveCallback.

Add this code to OnEnable method:

list.onCanRemoveCallback = (ReorderableList l) => {
	return l.count > 1;
};

Now try deleting all elements from the list. You will see that if there's only one element the  button is disabled.

Adding a warning

We don't really want to accidentally delete an item in our list. Using onRemoveCallback we can display a warning and make a user press Yes button if he really wants to delete an element.

list.onRemoveCallback = (ReorderableList l) => {
	if (EditorUtility.DisplayDialog("Warning!", 
    	"Are you sure you want to delete the wave?", "Yes", "No")) {
		ReorderableList.defaultBehaviours.DoRemoveButton(l);
	}
};

Note how we used ReorderableList.defaultBehaviours.DoRemoveButton method here. ReorderableList.defaultBehaviours contains all default implementations for various functions which sometimes can be handy if you don't want to reinvent the wheel.

 

Initializing a newly created element

What if we wanted to add a preconfigured element when a user presses + button instead of copying the last one in the list? We can intercept the logic of adding elements using onAddCallback.

Add the following code to OnEnable method:

list.onAddCallback = (ReorderableList l) => {
	var index = l.serializedProperty.arraySize;
	l.serializedProperty.arraySize++;
	l.index = index;
	var element = l.serializedProperty.GetArrayElementAtIndex(index);
	element.FindPropertyRelative("Type").enumValueIndex = 0;
	element.FindPropertyRelative("Count").intValue = 20;
	element.FindPropertyRelative("Prefab").objectReferenceValue = 
    		AssetDatabase.LoadAssetAtPath("Assets/Prefabs/Mobs/Cube.prefab", 
            typeof(GameObject)) as GameObject;
};

Here we are adding an empty wave to the end of the list and with the help of element.FindPropertyRelative method we are setting its properties to predefined values. In case of Prefab property we are looking for the specific prefab at path Assets/Prefabs/Mobs/Cube.prefab. Make sure that you created the folder structure accordingly.

Now return to Unity and try adding new elements to the list. You will see that new elements are always added as Cubes with Count equal to 20.

Adding drop-down menu

The last example will be the most interesting. We will make a dynamic drop-down menu activated by + button using onAddDropdownCallback.

In Prefabs folder we have 3 mobs and 3 bosses, so it would be natural to be able to add a specific one from a menu instead of manually dragging prefabs in the list.

Let's first add a new data type which we will later use in our menu system. Add this code at the end of the file before the last }.

private struct WaveCreationParams {
	public MobWave.WaveType Type;
	public string Path;
}

Next, define a callback which will be called by built-in Unity menu system. Right now it's empty but we'll fix this later. Add the following code after OnInspectorGUI method.

private void clickHandler(object target) {}

And now is the time to define our onAddDropdownCallback. Add this code in OnEnable method:

list.onAddDropdownCallback = (Rect buttonRect, ReorderableList l) => {
	var menu = new GenericMenu();
	var guids = AssetDatabase.FindAssets("", new[]{"Assets/Prefabs/Mobs"});
	foreach (var guid in guids) {
		var path = AssetDatabase.GUIDToAssetPath(guid);
		menu.AddItem(new GUIContent("Mobs/" + Path.GetFileNameWithoutExtension(path)), 
        false, clickHandler, 
        new WaveCreationParams() {Type = MobWave.WaveType.Mobs, Path = path});
	}
	guids = AssetDatabase.FindAssets("", new[]{"Assets/Prefabs/Bosses"});
	foreach (var guid in guids) {
		var path = AssetDatabase.GUIDToAssetPath(guid);
		menu.AddItem(new GUIContent("Bosses/" + Path.GetFileNameWithoutExtension(path)), 
        false, clickHandler, 
        new WaveCreationParams() {Type = MobWave.WaveType.Boss, Path = path});
	}
	menu.ShowAsContext();
};

Here we are building dynamic drop-down menu from the files in Mobs and Bosses folders, assigning instances of WaveCreationParams to them to be able to find out what menu element was clicked later in clickHandler.

If you set up your folder structure right and didn't forget to add using System.IO; in the beginning of the file, you can now return to Unity and try pressing + button (which is now Plus More) to see how it works.

 

Now all what's left is to add actual wave creation logic to clickHandler:

private void clickHandler(object target) {
	var data = (WaveCreationParams)target;
	var index = list.serializedProperty.arraySize;
	list.serializedProperty.arraySize++;
	list.index = index;
	var element = list.serializedProperty.GetArrayElementAtIndex(index);
	element.FindPropertyRelative("Type").enumValueIndex = (int)data.Type;
	element.FindPropertyRelative("Count").intValue = 
    	data.Type == MobWave.WaveType.Boss ? 1 : 20;
	element.FindPropertyRelative("Prefab").objectReferenceValue = 
    	AssetDatabase.LoadAssetAtPath(data.Path, typeof(GameObject)) as GameObject;
	serializedObject.ApplyModifiedProperties();
}

When a menu item is clicked this method is called with the value object we specified earlier. This value object containing in data variable is used to set up properties of the newly created wave. To assing the right Prefab to the wave we are using AssetDatabase.LoadAssetAtPath(data.Path, typeof(GameObject)) method with the path we found in onAddDropdownCallback.

In conclusion

Of the all available callbacks we haven't touched only onChangedCallbackand onReorderCallback because they are not really interesting. But you must know that they exist.

If you've been working with Unity for a long time you should know how hard it is to make a proper interface for a collection of things in Unity IDE. Especially when this wasn't in your time budget from the beginning. I've been using another implementation of ReorderableList by rotorz for a while. But now when we have an implementation from Unity Team there's no excuse not to use it.

If you want to find out how this list is implemented you can use ILSpy to decompile UnityEditor.dll which is (thankfully) not obfuscated or otherwise protected.

May the pretty lists be with you!

P.S. Alexei in comments proposed a solution to list item height problem which is described here: https://feedback.unity3d.com/suggestions/custom-element-size-in-reorderable-list
Code: http://pastebin.com/WhfRgcdC

https://va.lent.in/unity-make-your-lists-functional-with-reorderablelist/

================================================

PropertyDrawer

public class Recipe: MonoBehaviour
{
    public Ingredient PotionResult;
    public Ingredient[] PotionIngredients;
}
 
public enum IngredientUnit {Cup,Bowl,Spoon,Piece}
 
[Serializable]//这个特性表示该类可以被序列化,但是不加好像也没关系
public class Ingredient  {
    //需要重新设计属性面板的类
    public string Name;
    public int Amount;
    public IngredientUnit Unit;
}

[CustomPropertyDrawer(typeof(Ingredient))]
public class IngredientDrawer:PropertyDrawer
{
    //重新绘制面板
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUI.BeginProperty(position, label, property);
        
        
        //每个实例的名字 本来应该是Element0123 ,但这里显示了Name字符串
        position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label);
        
        //本来是1层,要缩进,设置成0和他的上级size同级,使之与其对齐;
        var index = EditorGUI.indentLevel;
        EditorGUI.indentLevel = 0;
        
        //给三个属性安排位置
        var amountRect = new Rect(position.x, position.y, 30, position.height);
        var unitRect = new Rect(position.x + 35, position.y, 50, position.height);
        var nameRect = new Rect(position.x + 90, position.y, position.width - 90, position.height);
        
        //绘制属性
        EditorGUI.PropertyField(amountRect, property.FindPropertyRelative("Amount"), GUIContent.none);
        EditorGUI.PropertyField(unitRect, property.FindPropertyRelative("Unit"), GUIContent.none);
        EditorGUI.PropertyField(nameRect, property.FindPropertyRelative("Name"), GUIContent.none);
        
        //重新设置为原来的层级
        EditorGUI.indentLevel = index;
        
        EditorGUI.EndProperty();
 
    }
}

public class MyRangeAttribute : PropertyAttribute
{
    public float min;
    public float max;
 
    public MyRangeAttribute(float min, float max)
    {
        this.min = min;
        this.max = max;
    }
}

[CustomPropertyDrawer(typeof(MyRangeAttribute))]
public class RangeDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        MyRangeAttribute range = (MyRangeAttribute) attribute;
        if (property.propertyType == SerializedPropertyType.Float)
            EditorGUI.Slider(position, property, range.min, range.max, label);
        else if (property.propertyType == SerializedPropertyType.Integer)
            EditorGUI.IntSlider(position, property, (int) range.min, (int) range.max, label);
        else
            EditorGUI.LabelField(position, label.text, "Use MyRange with float or int.");
    }
}

Scence Look at

[ExecuteInEditMode]
public class LookAtPoint : MonoBehaviour
{
 
    public Transform lookPoint;
    public Vector3 lookPos;
 
    public void Update()
    {
        if (lookPoint)
        {
            transform.LookAt(lookPos);
        }
        
    }
}

[CustomEditor(typeof(LookAtPoint))]
[CanEditMultipleObjects]
public class LookAtPointEditor : Editor
{
    private SerializedProperty lookAtPoint;
    private SerializedProperty lookPos;
 
    private void OnEnable()
    {
        lookAtPoint = serializedObject.FindProperty("lookPoint");
        lookPos = serializedObject.FindProperty("lookPos");
        
    }
 
    //调整在Inspector上的布局
    public override void OnInspectorGUI()
    {
        
        serializedObject.Update();//更新面板显示
        //选择的transform
        EditorGUILayout.PropertyField(lookPos);
        serializedObject.ApplyModifiedProperties();//将面板值应用到属性
        
        /*
        //转化成实际的类型
        Transform a=(Transform)lookAtPoint.objectReferenceValue;
        if (a.position.y > ((LookAtPoint)target).transform.position.y)
        {
            EditorGUILayout.LabelField("(Above this object)");
        }
        if (a.position.y < ((LookAtPoint)target).transform.position.y)
        {
            EditorGUILayout.LabelField("(Below this object)");
        }
        */
 
    }
    
    //在Scene场景里添加绘制一些需要的UI布局
    public void OnSceneGUI()
    {
        var t = (target as LookAtPoint);
        EditorGUI.BeginChangeCheck();
        //绘制一个图形(移动物体的那个三箭头),位置对应lookPos
        Vector3 pos = Handles.PositionHandle(t.lookPos, Quaternion.identity);
        if (EditorGUI.EndChangeCheck())
        {//有调整就修改数值
            Undo.RecordObject(target, "Move point");
            t.lookPos = pos;
            t.Update();
        }
    }
 
    //protected override void OnHeaderGUI();
    //public override void OnPreviewGUI(Rect r, GUIStyle background)
    //public override void OnInteractivePreviewGUI(Rect r, GUIStyle background)
}

 

=================================================

Unity Editor 控件

GUILayout.FlexibleSpace();
GUILayout.Space(100);//空格,没什么好说的

GUILayout.Label("label");

GUILayout.Box(new GUIContent("一个200x200的BOX"),new []{GUILayout.Height(200),GUILayout.Width(200)});

num = EditorGUILayout.Slider ("Slider", num, -10, 10);

scrollBarValue1 = GUILayout.HorizontalScrollbar(scrollBarValue1,0,0,100,new[]{GUILayout.Width(100)});
scrollBarValue2 = GUILayout.VerticalScrollbar(scrollBarValue2,0,0,100,new[]{GUILayout.Height(100)});

scrollValue1=GUILayout.HorizontalSlider(scrollValue1, 0, 100);
scrollValue2=GUILayout.VerticalSlider(scrollValue2, 0, 100);

bool a=GUILayout.Button("一个Button");

bool b= GUILayout.RepeatButton("RepeatButton");
//RepeatButton 在按着的时候一直返回true,松开才返回false

gridId = GUILayout.SelectionGrid(gridId, new[] {"1", "2", "3","4","5","6"}, 4);

toolbarid=GUILayout.Toolbar(toolbarid, new[] {"1", "2", "3"});

GUILayout.TextField("TextField只能一行");
GUILayout.TextArea("TextArea可以多行\n 第二行");

password = GUILayout.PasswordField(password, '*');

isToggle = GUILayout.Toggle (isToggle,"Toggle");

//保证块里写的控件只在Area规定的范围内,这里设置放到右下角
GUILayout.BeginArea(new Rect(position.width-250,position.height-150,250,150));
//...
GUILayout.EndArea();

GUILayout.BeginHorizontal();
//...水平布局
GUILayout.EndHorizontal();
 
GUILayout.BeginVertical();
//...垂直布局
GUILayout.EndVertical();

//显示的范围不够块内控件时可以滑动 这里限制高度75最多同时显示3个
scrollPos=GUILayout.BeginScrollView(scrollPos,false,true,GUILayout.Height(75));
//...
GUILayout.EndScrollView();

groupEnabled = EditorGUILayout.BeginToggleGroup ("ToogleGroup",groupEnabled);
//...
EditorGUILayout.EndToggleGroup ();

//可以选择一个最大最小的范围的slider
EditorGUILayout.MinMaxSlider("MinMaxSlider",ref sliderValue2,ref sliderValue3,10,20);
//int类型的slider
intSliderValue=EditorGUILayout.IntSlider(intSliderValue,0,10);
//和guilayout重复的
sliderValue=EditorGUILayout.Slider(sliderValue,0f,1);

//这个可以多选 需要枚举类型
flagtype=(flagTestType)EditorGUILayout.EnumFlagsField(flagtype);

//这个单选 需要枚举类型
popuptype=(popupTestType)EditorGUILayout.EnumPopup(popuptype);

//一个选择框,每个选择框里表示一个Int数
popupindex=EditorGUILayout.IntPopup("IntPopup", popupindex, new[] {"a", "b"}, new[] {1, 2});

//和IntPopup类似但是可以多选
maskindex=EditorGUILayout.MaskField("Mask",maskindex,new []{"a","b"});

//正常按钮在鼠标按键抬起MouseUp时返回true,他在MouseDown时就立即返回true
if (EditorGUILayout.DropdownButton(new GUIContent("DropdownButton"),FocusType.Keyboard))
{
    Debug.Log("鼠标按下时出现");
}

EditorGUILayout.Toggle("toggle",false);
EditorGUILayout.ToggleLeft("ToggleLeft",true);

i1=EditorGUILayout.IntField("IntField",i1,GUILayout.Width(100));
d1=EditorGUILayout.DoubleField("DoubleField",d1,GUILayout.Width(100));
            
//bounds类型输入框 反正就两个三维向量
boundsv=EditorGUILayout.BoundsField("BoundsField",boundsv);
boundintv=EditorGUILayout.BoundsIntField("BoundsIntField",boundintv);
            
//层级和标签\物体选择,就是unity中的各种layer  tag gameboject
EditorGUILayout.LayerField("LayerField", 1);
EditorGUILayout.TagField("TagField", "一个tag");
EditorGUILayout.ObjectField("ObjectField",GameObject.Find("Cube"), typeof(GameObject),true);
            
//显示序列化属性字段 比如之前的自定义特性里的
EditorGUILayout.PropertyField(p)
            
EditorGUILayout.RectField(new Rect());
EditorGUILayout.RectIntField(new RectInt());
            
//delay和一般的区别是只有按下回车或者失去焦点时,才会返回值,就是说你再输入时不会返回
//Note that the return value will not change until the user has pressed enter or focus is moved away from the text field.
delayint=EditorGUILayout.DelayedIntField("DelayedInt",delayint);
delayfloat=EditorGUILayout.DelayedFloatField("DelayedFloat",delayfloat);
delaydouble=EditorGUILayout.DelayedDoubleField("DelayedDouble",delaydouble);
delaystr=EditorGUILayout.DelayedTextField("DelayedTextField",delaystr);
        
colorv=EditorGUILayout.ColorField("颜色框", colorv);
acurev=EditorGUILayout.CurveField("曲线", acurev);
 
//还有这几个向量
//EditorGUILayout.Vector2Field();
//EditorGUILayout.Vector2IntField();
//EditorGUILayout.Vector3Field();
//EditorGUILayout.Vector3IntField();
//EditorGUILayout.Vector4Field()

knob=EditorGUILayout.Knob(new Vector2(100, 100), knob, 1, 10, "斤", Color.black, Color.blue, true);

EditorGUILayout.HelpBox("helpBox,这里写一些提示、警告、错误", MessageType.None);

//不知道干嘛的,字面意思是分离
EditorGUILayout.Separator();
        
EditorGUILayout.Space();
        
EditorGUILayout.LabelField("labelField");
EditorGUILayout.PrefixLabel("PrefixLabel");
//点击变蓝的label 可以被选择和复制
EditorGUILayout.SelectableLabel("SelectableLabel");

//用于解决EditorGUILayout和EditorGUI(或者GUI)混着用的情况
//这样确保自动布局不会乱,不会叠在一起
Rect a=EditorGUILayout.GetControlRect();
EditorGUI.LabelField(a,"一个label");

if (EditorGUILayout.BeginFadeGroup(Value))
{
//...ps:这个应该用于开关的特效,因为Value的值不是0或1时,会让下面所有的控件都无法交互
}
EditorGUILayout.EndFadeGroup();

#region 折叠类
 
//折叠的相关控件 返回bool类型表示开、关
foldout = EditorGUILayout.Foldout(foldout, "折叠Label");
if (foldout)
{
   val1=EditorGUILayout.IntField("111", val1);
   val2=EditorGUILayout.IntField("222", val2);
}
 
foldout2=EditorGUILayout.InspectorTitlebar(foldout2,GameObject.Find("Cube"));
if (foldout2)
{
   val3=EditorGUILayout.IntField("333", val3);
   val4=EditorGUILayout.IntField("444", val4);
}
 
#endregion

==============================

Unity Editor 记录

EditorApplication.SaveCurrentSceneIfUserWantsTo();
//打开是否保存场景的对话框
EditorApplication.NewScene();
//创建新场景,方法以废弃=> EditorSceneManager.NewScene(NewSceneSetup.DefaultGameObjects);

public class GizmoExample : MonoBehaviour {
 //和Start()等一样自动调用
 private void OnDrawGizmos() {GUI...} //常态时显示Gizmos
 private void OnDrawGizmosSelected(){GUI...}//被选中时显示的Gizmos,不会覆盖前者,两者同时渲染
}

[DrawGizmo(
    GizmoType.InSelectionHierarchy//自己被选中或者父级被选中时调用
    |GizmoType.NotInSelectionHierarchy//自己和父级都没被选中时调用
    |GizmoType.Selected//自己被选中时调用
    |GizmoType.NonSelected//自己没被选中时调用
    |GizmoType.Active//激活状态时调用
    |GizmoType.Pickable//可以通过点击Gizmos选择物体
    )]
    private static void Gizmos4ExampleNoSelected(GizmoExample target, GizmoType type)
    {
    ...等同于OnDrawGizmos
    }
 
    [DrawGizmo(GizmoType.InSelectionHierarchy|GizmoType.Pickable)]
    private static void Gizmos4ExampleOnSelected(GizmoExample target, GizmoType type)
    {
        ...等同于OnDrawGizmosSelected
    }

Gizmos.color;
//Gizmos的颜色,最好在换色前保存原有原色,Gizmos写完后reset原来的颜色,避免影响别的Gizmos 
//如下所示-------------------
Color oldCol=Gizmos.color;
Gizmos.color=Color.red;
...end
Gizmos.color=oldCol
//--------------------------
 
 
Gizmos.matrix;
//Gizmos的4x4矩阵 应该存了Gizmos参考的位移 旋转 大小 默认应该是原点 0旋转 1缩放(后两个猜的) 
//比如为了方便设置,你想设Gizmos作用物体的Transform作为参考的原点 同样要记得reset
//如下设置-------------------
Matrix4x4 oldMatrix = Gizmos.matrix;
Gizmos.matrix = transform.localToWorldMatrix;
...end
Gizmos.matrix = oldMatrix;
//--------------------------
 
 
Gizmos.Draw(Wire)Cube/Line/ray/Mesh/Sphere/Frustrum();
//具体可以绘制的各种Gizmos 实体/线框 的方块,线、射线、自定义mesh,球 、视锥体(camera的那个)

[CustomEditor(typeof(targetType))]
 public class targetTypeEditor : Editor {
 
    private void OnEnable () {//启用时调用...}
 
    private void OnDisable () {//关闭时调用...}
 
    private void OnDestroy () {//摧毁时调用...}
 
    public override void OnInspectorGUI () {
        //...绘制Inspector面板
        DrawDefaultInspector();//默认显示
        targetType t=(targetTyp)target;//target来自Editor基类,表示目标实例
    }
 
}

//打开一个Unity对话框,
bool a=EditorUtility.DisplayDialog("对话框标题","对话框内容","Yes","No");
 
 
bool oldEnable=GUI.enabled;
GUI.enabled=flase;
//...enabled设为false,下面的GUI控件就无法交互和使用
GUI.enabled=oldEnable;
 
//GUI有变化就调用SetDirty,把Target标记为dirty,告知unity保存数据
if(GUI.changed) {
 EditorUtility.SetDirty(_myTarget);
}

 [CustomPropertyDrawer (typeof(TypeName))]
 public class TypeDrawer : PropertyDrawer {//这里也可以选择继承DecoratorDrawer,装饰类型的编辑
 
     public override void OnGUI (Rect position, SerializedProperty
        property, GUIContent label) {
        //这里只能用GUI和EditorGUI,不能用GUILayout和EditorGUILayout
 
     }
 }

private SerializedObject _mySerializedObject;
private SerializedProperty _serializedTotalTime;
 
private void InitSerialized () {
      //用target生成一个SerializedObject对象
     _mySerializedObject = new SerializedObject (target);
      //用FindProperty ("name");找到你要用的值或属性 SerializedProperty可以保留特性相关信息
     _serializedTotalTime = _mySerializedObject.FindProperty ("_totalTime");
}
 
private void OnInspectorGUI() {
 ...
 //这样就会显示指定特性所实现的GUI
 EditorGUILayout.PropertyField (_serializedTotalTime);
 ...
}

Unity内置编辑器特性
 
----Property Drawers---------------------
 
[Range (0, 1)]
public float floatRange = 0.5f;
 
[Multiline (2)]
public string stringMultiline = "This text is using a multiline
property drawer";
 
[TextArea (2, 4)]
public string stringTextArea = "This text \nis using \na text area \
nproperty \ndrawer";
 
[ContextMenu ("Do Something")]//..点Inspector右边的齿轮会多这个选项
 public void DoSomething() {
 Debug.Log ("DoSomething was called...");
 } 
 
[ContextMenuItem("Reset this value", "Reset")]//给指定的控件提供一个方法 和上面类似
public int intReset = 100;
public void Reset() {intReset = 0;}
 
----Property Drawers---------------------
 
 
----Decorator Drawers---------------------
 
[Header("This is a group of variables")]
public int varA = 10;
 
public int varC = 10;
[Space(40)]//上下隔空格
public int varD = 20;
 
[Tooltip("This is a tooltip")]//鼠标移在上面的提示框
public int varE = 30;
 
----Decorator Drawers---------------------

public class MyWindow : EditorWindow {
 public static MyWindow instance;
 
 //打开窗口的静态方法
 public static void OpenMyWindow () {
     //EditorWindow.GetWindow()创建一个窗口
     instance = (PaletteWindow) EditorWindow.GetWindow(typeof(PaletteWindow));
     instance.titleContent = new GUIContent("Palette");
 }
 private void OnEnable() {}
 
 private void OnDisable() {}
 
 private void OnDestroy() {}
 
 private void OnGUI() {}
 
 private void Update () {}
 
}

[MenuItem ("Tools/Level Creator/Show Palette _#p")]
private static void ShowPalette () {
 PaletteWindow.ShowPalette ();
}

搜寻指定资源,第一个参数是筛选的字符串,具体规则看下面,第二个是要搜索资源的文件路径,可以同时搜寻多个路径中的文件,返回值是符合要求的guid(全局唯一标识符 global unique identifier)

string[] guids = AssetDatabase.FindAssets ("t:Prefab", new string[] {path});

//得到guid后在找资源路径,再通过路径找到具体的资源

assetPath = AssetDatabase.GUIDToAssetPath (guids [i]);

asset = AssetDatabase.LoadAssetAtPath (assetPath, typeof(GameObject)) as GameObject;

FindAssets的筛选规则:可以用三种筛选方式,且可以混合用

1、通过资源名称查找,全名或部分名称度可以 比如 找一个叫“test”的资源,可以写"Test",也可以写"Te"
2、通过资源的标签(label)查找,查找的标签名前加"l:"来区分

3、通过资源的类型查找,查找的类型名前加"t:"来区分 

比如"player l:img t:Texure2D"表示搜寻Texture2D类型 label为img 名称里带有player的资源

注意:查找不分大小写,多个条件用空格分开

AssetPreview.GetAssetPreview(Object asset)得到资源的预览图片,返回Texture2D

[CustomEditor(typeof(targetType))]
 public class targetTypeEditor : Editor {
    ...
    public override void OnSceneGUI () {
            
            //因为是在OnSceneGUI上调用,所以最好用这个包裹GUI
            //The start of a GUILayout sequence of function calls. As expected the EndGUI is used to close these.
            Handles.BeginGUI();
 
            //确定绘制的范围,不然默认整个屏幕了
            GUILayout.BeginArea(new Rect(10,10,360,30));
 
            //GUIlayout代码...
 
            GUILayout.EndArea();
 
            Handles.EndGUI();
 
            Repaint();//重新绘制,在GUI变化是可以尝试调用来更新显示
    }
}

除了一些属性字段 Handles类里还有有unity提供的各种Handle(?操控杆)

像Handles.PositionHandle就是我们移动物体时跟坐标轴一样的三个箭头 

 通过SceneView.currentDrawingSceneView 可以得到SceneView的相关信息

比如SceneView.currentDrawingSceneView.in2DMode = true;就是开启

另外SceneView.currentDrawingSceneView.camera可以得到渲染sceneView的相机,要注意这个相机不是场景中放置的相机

 通过Tools.current 得到的相关信息 

HandleUtility.AddDefaultControl(GUIUtility.GetControlID(FocusType.Passive));将在SceneView中输入的控制权从Unity默认处理转到自己处理,避免点击其他地方导致物体失去焦点,GUI消失

Event.current 里可以得到各种键盘鼠标的输入事件

Event.current.mousePosition得到鼠标位置是基于左上的 如果想要得到基于左下,可以这样转化一下Vector2 mousePos = Event.current.mousePosition;
mousePos=new Vector2(mousePos.x,camera.pixelHeight-mousePos.y);

通过InstantiatePrefab生成对象,可以让对象是预制体(物体引用的prefab改变,物体自身也会改变)

GameObject go = PrefabUtility.InstantiatePrefab(_selectedPieces.gameObject)as GameObject;

为InspectorScript生成默认的GUI  懒得为某个引用的类特意绘制自定义的GUI,可以用这个方法

Editor.CreateEditor(InspectorScript).OnInspectorGUI();

hiding flags

每个Object都有,有下面这些选择

• None: 默认

• HideInHierarchy: 物体在Hierarchy面板隐藏

• HideInInspector: 物体在Inspector面板隐藏

• NotEditable: 物体在inspector无法编辑(变灰,不可交互)

• DontSaveInEditor: It is not saved to the scene in the editor.

• DontSaveInBuild: It is not saved when a player is built.

• DontUnloadUnusedAsset: It is not unloaded by Resources. UnloadUnusedAssets.

• DontSave: It is not saved to the scene. It will not be destroyed when a new scene is loaded. It is a shortcut for HideFlags.DontSaveInBuild | HideFlags. DontSaveInEditor | HideFlags.DontUnloadUnusedAsset.

• HideAndDontSave: It is a combination of not shown in the hierarchy, not saved to scenes, and not unloaded by the object; it will not be unloaded by Resources.UnloadUnusedAssets.

[Serializable]
public class LevelSeting:ScriptableObject
{
    public float gravity;
    public AudioClip bgm;
    public Sprite background;
}
 
public static T CreateAsset<T>(string path) where T:ScriptableObject
{
    //创建实例
    T asset = ScriptableObject.CreateInstance<T>();
    //保存至Asset
    AssetDatabase.CreateAsset(asset,path);
    AssetDatabase.Refresh();
    AssetDatabase.SaveAssets();
    return asset;
}
 
[MenuItem("Tools/Level Creator/NewLevelSeting")]
static void CreateLevelSeting()
{
    //打开一个选择存储路径的窗口
    string path = EditorUtility.SaveFilePanelInProject("NewLevelSetingAsset", "LevelSetings", "asset",
"Define the name for the LevelSettings asset");
 
    if (path!=null)
    {
        EditorUtils.CreateAsset<LevelSeting>(path);
    }
}

public class TexturePipeline:AssetPostprocessor
    {
        //Texture资源导入时调用
        private void OnPreprocessTexture()
        {
            //如果资源导入到该文件夹下,就会返回true
            if (assetPath.StartsWith("Assets/Art/Bg"))
            {
                ImportBackground();
            }
        }
    
        //Texture资源导入完成后调用
        private void OnPostprocessTexture(Texture2D texture)
        {
        
        }
    
        void ImportBackground()
        {
            Debug.Log("Loading Background...");
            TextureImporter importer = assetImporter as TextureImporter;
            //这里就是对导入的图片的一些格式信息进行修改
            importer.textureType = TextureImporterType.Sprite;
            TextureImporterSettings importSeting=new TextureImporterSettings();
            importer.ReadTextureSettings(importSeting);
            importSeting.spriteAlignment = (int)SpriteAlignment.BottomLeft;
            importSeting.mipmapEnabled = false;
            importer.SetTextureSettings(importSeting);
        }
    }

===============================

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值