Unity自定義mesh以及編譯器

Star

自定義編輯器簡易教程 an introduction to custom editors

原文地址 http://catlikecoding.com/unity/tutorials/star/編程

http://blog.csdn.net/lilanfei/article/details/7680802數組

簡介 Introduction

這個教程將讓你學會如何建立一個星型控件以及如何製做這個控件的自定義編輯器。你將學會:app

  • 動態的創建Mesh。
  • 使用一個嵌套類。
  • 創建一個自定義編輯器。
  • 使用SerializedObject
  • 支持所見即所得。
  • 對Undo、Redo、Reset和prefab提供支持。
  • 支持多對象編輯。
  • 支持場景視圖內編輯。

咱們假設你已經學會了Unity C#的基礎編程知識,以及Unity 編輯器的基礎知識。若是你已經完成了相關的學習,Let's Go!編輯器

創建Star類 Creating the star

咱們創建一個全新的Unity工程,而後創建一個新的C#腳本,將它命名爲Star。咱們將用這個腳本,創建一個由三角面拼接成的星,這裏須要一個Mesh。ide

什麼是Mesh?

3D模型是由多邊形拼接而成,一個複雜的多邊形,其實是由多個三角面拼接而成。因此一個3D模型的表面是由多個彼此相連的三角面構成。三維空間中,構成這些三角面的點以及三角形的邊的集合就是Mesh。學習

using UnityEngine;

public class Star : MonoBehaviour { private Mesh mesh; }

任何對於Mesh的使用,都必須搭配一個MeshFilter組件,而MeshFilter又被用於MeshRenderer組件。只有這樣,才能被Unity繪製。因此,這些組件都必須被加載到GameObject對象上,咱們的Star對象也必須這麼作。ui

固然,咱們能夠手動添加這些組件,但默認的自動添加是一個更好的辦法。因此咱們須要添加一個RequireComponent類做爲Star對象的一個特性。this

什麼是類的特性?

特性像對類附加一個標籤,用來告訴編譯器這個類須要如何處理。是除了類聲明的代碼以外,對類作的附加說明。另外,特性不止針對類,對方法和屬性一樣適用。idea

typeof有什麼用?

typeof是一種運算符,可以得到任何類的類型描述數據,數據裏最經常使用的就是類的名字。那爲何不直接在代碼裏寫類的名字就好呢?由於代碼中全部的賦值和運算都須要變量,直接使用類的名字會致使編譯錯誤。spa

using UnityEngine;

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Star : MonoBehaviour {

	private Mesh mesh;

}

如今,咱們創建一個新的空GameObject,將它命名爲My First Star,而後拖拽咱們的腳本Star到My First Star上。你能夠看到,My First Star擁有了兩個組件,MeshRenderer和Star。

拖動一個,獲得三個

下一個步驟是創建一個Mesh。咱們須要在Unity的Start事件裏來作這些事,Start事件將在程序啓動的時候發生。咱們還須要在MeshFilter中給這個新的Mesh起一個名字。

using UnityEngine;

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Star : MonoBehaviour {

	private Mesh mesh;

	void Start () {
		GetComponent<MeshFilter>().mesh = mesh = new Mesh(); mesh.name = "Star Mesh"; } }
without mesh with mesh
無編輯器模式與實時預覽模式

固然,如今咱們在預覽模式下還看不到任何東西,由於Mesh仍是空的。因此讓咱們開始編輯頂點數組吧,咱們的Star類須要一個用來設置頂點數量的屬性,以及這些定點與中心的相對距離。

第一個頂點是Star的中心點,其他的頂點將順時針排列。咱們將使用四元數來計算這些點的排列。由於咱們假設俯視Z軸,因此,輪轉的角度是負數,不然,將使這些點作逆時針排列。咱們不須要設置第一個點,由於vector默認會被設置成0, Mesh中使用本地座標系。

using UnityEngine;

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Star : MonoBehaviour {

	public Vector3 point = Vector3.up;
	public int numberOfPoints = 10; private Mesh mesh; private Vector3[] vertices; void Start () { GetComponent<MeshFilter>().mesh = mesh = new Mesh(); mesh.name = "Star Mesh"; vertices = new Vector3[numberOfPoints + 1]; float angle = -360f / numberOfPoints; for(int v = 1; v < vertices.Length; v++){ vertices[v] = Quaternion.Euler(0f, 0f, angle * (v - 1)) * point; } mesh.vertices = vertices; } }
新的編輯器屬性

三角面會被保存成頂點數組,每一個面三個頂點。由於咱們使用三角形來描述多邊形,每一個三角形都起始於相同的中點,而且與其餘的三角形相連。最後一個三角形與第一個三角形相連。例如,若是有四個三角形,那麼頂點數組以下{0, 1, 2, 0, 2, 3, 0, 3, 4, 0, 4, 1}。

using UnityEngine;

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Star : MonoBehaviour {

	public Vector3 point = Vector3.up;
	public int numberOfPoints = 10;

	private Mesh mesh;
	private Vector3[] vertices;
	private int[] triangles;

	void Start () {
		GetComponent<MeshFilter>().mesh = mesh = new Mesh();
		mesh.name = "Star Mesh";

		vertices = new Vector3[numberOfPoints + 1];
		triangles = new int[numberOfPoints * 3]; float angle = -360f / numberOfPoints; for(int v = 1, t = 1; v < vertices.Length; v++, t += 3){ vertices[v] = Quaternion.Euler(0f, 0f, angle * (v - 1)) * point; triangles[t] = v; triangles[t + 1] = v + 1; } triangles[triangles.Length - 1] = 1; mesh.vertices = vertices; mesh.triangles = triangles; } }
game view scene view
一個簡單的星星

如今,咱們的星星看起來還只是一個簡單的多邊形。Unity也提示說丟失材質座標,由於默認的Shader須要這些座標。咱們不會使用一個紋理來描繪全部的星星,讓咱們經過創建咱們本身的Shader來消除這個警告,這個Shader將只使用頂點着色。

咱們創建一個新的Shader將它命名爲Star,而後寫入如下代碼。

什麼是CGPROGRAM?

Basically, data flows from the Unity engine into the graphics card, where it's processed per vertex. Then interpolated data flows from the vertices down to the individual pixels. In this case, we pass position and color data all the way down. The only additional thing we do is convert vertex positions from world space to screen space.
The statements above the CGPROGRAM switch off default lighting and depth buffer writing. Culling is switched off so we can see the triangles from both sides, not just the front. "Blend SrcAlpha OneMinusSrcAlpha" is default alpha blending, allowing for transparency.

爲何不使用fixed-function shader?

fixed-function shader已經屬於過期的技術了。 CGPROGRAM 在將數據轉化成屏幕像素方面擁有更強大的功能。

Shader "Star"{
	SubShader{
		Tags{ "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent"}
		Blend SrcAlpha OneMinusSrcAlpha
		Cull Off
		Lighting Off
		ZWrite Off
		Pass{
			CGPROGRAM
				#pragma vertex vert
				#pragma fragment frag

				struct data {
					float4 vertex : POSITION;
					fixed4 color: COLOR;
				};

				data vert (data v) {
					v.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
					return v;
				}

				fixed4 frag(data f) : COLOR {
					return f.color;
				}
			ENDCG
		}
	}
}

如今咱們創建一個新的材質球,命名爲Star,將Shader設置爲咱們剛剛編寫的Star,而且將這個材質球賦予My First Star。

game view inspector
添加材質球以後

頂點着色默認是白色,因此咱們的多邊形如今變成了白色。咱們想要一個更漂亮的星星。因此咱們來爲每一個點定義一種顏色。

咱們再添加一個frequency屬性,這樣咱們就能讓程序自動重複點的序列,而不用咱們逐個定義所有的點。這個選項取代了numberOfPoints。

咱們在最後須要確認frequency屬性是否正確,而且星星至少擁有一個點。若是沒有,咱們的代碼就可能出錯。

Why check both for null and the length?

When freshly created, our star component won't have an array yet. It's also technically possible for scripts to explicitly set our array to null later on. We need to watch out for that, to prevent errors. Only if the array does exists do we go ahead and check its length as well.

using UnityEngine;

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Star : MonoBehaviour {

	public Vector3[] points;
	public int frequency = 1; private Mesh mesh; private Vector3[] vertices; private int[] triangles; void Start () { GetComponent<MeshFilter>().mesh = mesh = new Mesh(); mesh.name = "Star Mesh"; if(frequency < 1){ frequency = 1; } if(points == null || points.Length == 0){ points = new Vector3[]{ Vector3.up}; } int numberOfPoints = frequency * points.Length; vertices = new Vector3[numberOfPoints + 1]; triangles = new int[numberOfPoints * 3]; float angle = -360f / numberOfPoints; for(int iF = 0, v = 1, t = 1; iF < frequency; iF++){ for(int iP = 0; iP < points.Length; iP += 1, v += 1, t += 3){ vertices[v] = Quaternion.Euler(0f, 0f, angle * (v - 1)) * points[iP]; triangles[t] = v; triangles[t + 1] = v + 1; } } triangles[triangles.Length - 1] = 1; mesh.vertices = vertices; mesh.triangles = triangles; } }
scene view inspector
配置好的點

咱們須要些顏色!若是把所有的頂點都指定相同的顏色就很簡單,但這樣太無聊了。咱們來試試給每一個頂點分配一個顏色。咱們須要一個數組來保存這些顏色數據,並且必須保持顏色和頂點的數量一致。這有點小麻煩,咱們乾脆換成另一種方式,在Star類中創建一個新的類,這個類能夠保存一個頂點的顏色和位置。而後咱們能夠用這個類的數組來代替vector數組。

這類叫Point,若是在Star類以外使用,就是Star.Point。在Star裏面Point就能夠了。爲了讓Unity可以將Point序列化,咱們爲Point添加System.Serializable特性。

爲何不用結構體?

Because Star.Point is so lightweight and its data is always needed all at once, it would make sense to use a struct type and avoid the overhead that objects add. However, Unity does not support serialization of custom struct types. So you're stuck using classes to bundle data you want to store.
If you're really concerned about the object overhead and possible null errors, you can always store the offset and color data in two separate arrays. However, then you would need to make sure that these arrays always stay synchronized. While that is definitely doable, the class approach is simpler. That's why I use it in this tutorial.

using System;
using UnityEngine;

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Star : MonoBehaviour {

	[Serializable] public class Point { public Color color; public Vector3 offset; } public Point[] points; public int frequency = 1; private Mesh mesh; private Vector3[] vertices; private Color[] colors; private int[] triangles; void Start () { GetComponent<MeshFilter>().mesh = mesh = new Mesh(); mesh.name = "Star Mesh"; if(frequency < 1){ frequency = 1; } if(points == null || points.Length == 0){ points = new Point[]{ new Point()}; } int numberOfPoints = frequency * points.Length; vertices = new Vector3[numberOfPoints + 1]; colors = new Color[numberOfPoints + 1]; triangles = new int[numberOfPoints * 3]; float angle = -360f / numberOfPoints; for(int iF = 0, v = 1, t = 1; iF < frequency; iF++){ for(int iP = 0; iP < points.Length; iP += 1, v += 1, t += 3){ vertices[v] = Quaternion.Euler(0f, 0f, angle * (v - 1)) * points[iP].offset; colors[v] = points[iP].color; triangles[t] = v; triangles[t + 1] = v + 1; } } triangles[triangles.Length - 1] = 1; mesh.vertices = vertices; mesh.colors = colors; mesh.triangles = triangles; } }
game view inspector
有了顏色以後

最後是關於中心點的。如今,咱們尚未給它設置顏色,因此它一直保持着透明。讓咱們來爲它添加一個顏色屬性,最終,這個星星看上去變漂亮了。

using System;
using UnityEngine;

[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Star : MonoBehaviour {

	[Serializable]
	public class Point {
		public Color color;
		public Vector3 offset;
	}

	public Point[] points;
	public int frequency = 1;
	public Color centerColor;

	private Mesh mesh;
	private Vector3[] vertices;
	private Color[] colors;
	private int[] triangles;

	void Start () {
		GetComponent<MeshFilter>().mesh = mesh = new Mesh();
		mesh.name = "Star Mesh";

		if(frequency < 1){
			frequency = 1;
		}
		if(points == null || points.Length == 0){
			points = new Point[]{ new Point()};
		}

		int numberOfPoints = frequency * points.Length;
		vertices = new Vector3[numberOfPoints + 1];
		colors = new Color[numberOfPoints + 1];
		triangles = new int[numberOfPoints * 3];
		float angle = -360f / numberOfPoints;
		colors[0] = centerColor; for(int iF = 0, v = 1, t = 1; iF < frequency; iF++){ for(int iP = 0; iP < points.Length; iP += 1, v += 1, t += 3){ vertices[v] = Quaternion.Euler(0f, 0f, angle * (v - 1)) * points[iP].offset; colors[v] = points[iP].color; triangles[t] = v; triangles[t + 1] = v + 1; } } triangles[triangles.Length - 1] = 1; mesh.vertices = vertices; mesh.colors = colors; mesh.triangles = triangles; } }
game view scene view inspector
漂亮的星星,麻煩的編輯器

創建編輯器 Creating the Inspector

如今Star看起來不錯,但設計起來有些麻煩。默認的編輯器有點蛋疼,讓咱們本身作一個!

全部編輯器類的代碼,都須要放在Editor文件夾下,只有這樣Unity才能正確的識別這代碼。Editor的名字對就行,放在哪倒無所謂。咱們如今把Editor創建在根目錄也就是Assets下。而後再建一個StarInspector類的代碼文件,放在Editor裏面。

編輯器的類型?

須要瞭解的是,編輯器面板不僅有一個類型。咱們這個例子裏面使用的是屬性面板——Inspector,其他還有 EditorWindow——編輯對話框,能夠實現一個徹底自定義的彈出式對話框,還有ScriptableWizard——嚮導對話框,以及編輯器菜單。



目錄結構

由於咱們的類是一個編輯器類,它須要繼承Editor類而不是MonoBehaviour。咱們還須要添加一個屬性來告訴Unity,這個類是爲Star類定義編輯器的。

using UnityEditor;
using UnityEngine; [CustomEditor(typeof(Star))] public class StarInspector : Editor {}

到目前爲止,咱們沒有改變Star的編輯器。咱們須要替換默認的編輯器。咱們能夠經過重載Editor 類的OnInspectorGUI事件來實現。

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(Star))]
public class StarInspector : Editor {

	public override void OnInspectorGUI () {}
}
一個空的編輯器

默認的編輯器捏?沒了?

由於咱們沒有在OnInspectorGUI事件裏寫任何代碼,因此一切都是空白的。DrawDefaultInspector方法能夠用來繪製默認的編輯器界面,但咱們原本就不想要這個,仍是試試別的吧。

咱們首先要確認是哪一個Star對象被選中,應該在編輯器中被顯示。咱們可使用target屬性來表示這個對象,target屬性是Editor的一個屬性,咱們繼承了Editor,因此也繼承了這個屬性,能夠直接使用它,很是方便。雖然這不是必須的,咱們能夠用 SerializedObject來包裝target,這麼作會很方便,由於會使對不少編輯器的操做支持變得簡單,好比undo。

咱們使用了SerializedObject,能夠經過SerializedProperty對象來提取它的數據。咱們要在OnEnable事件裏初始化全部的star類的變量。這個事件會在一個添加Star組件的GameObject被選中時發生。

What's a SerializedObject?

SerializedObject is a class that acts as a wrapper or proxy for Unity objects. You can use it to extract data from the object even if you don't have a clue what's inside it. This is how the Unity inspector can show default inspectors for anything you create yourself. As a bonus, you get undo support for free.

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(Star))]
public class StarInspector : Editor {

	private SerializedObject star;
	private SerializedProperty points, frequency, centerColor; void OnEnable () { star = new SerializedObject(target); points = star.FindProperty("points"); frequency = star.FindProperty("frequency"); centerColor = star.FindProperty("centerColor"); } public override void OnInspectorGUI () {} } 

每一次編輯器更新的時候,咱們都須要肯定SerializedObject被實時更新了。這就是咱們要在OnInspectorGUI事件裏作的第一件事。以後咱們能夠簡單的調用EditorGUILayout.PropertyField來顯示咱們的屬性,顯示points及其內部的全部元素。以後咱們結束全部屬性修改並應用到選定的組件。

What's EditorGUILayout?

EditorGUILayout is a utility class for displaying stuff in the Unity editor. It contains methods for drawing all kinds of things, in this case we're simply using the default method for drawing a SerializedProperty.
There's also an EditorGUI utility class which does that same thing, but requires you to perform your own GUI layout.

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(Star))]
public class StarInspector : Editor {

	private SerializedObject star;
	private SerializedProperty
		points,
		frequency,
		centerColor;

	void OnEnable () { … }

	public override void OnInspectorGUI () {
		star.Update();

		EditorGUILayout.PropertyField(points, true); EditorGUILayout.PropertyField(frequency); EditorGUILayout.PropertyField(centerColor); star.ApplyModifiedProperties(); } }
一個被重建的編輯器

如今的編輯器和默認的差很少,咱們能夠作的更好。咱們須要從新整理一下points的顯示格式,讓每一個點的顏色和位移信息合併爲一組顯示。

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(Star))]
public class StarInspector : Editor {

	private SerializedObject star;
	private SerializedProperty
		points,
		frequency,
		centerColor;

	void OnEnable () { … }

	public override void OnInspectorGUI () {
		star.Update();

		for(int i = 0; i < points.arraySize; i++){
			EditorGUILayout.BeginHorizontal(); SerializedProperty point = points.GetArrayElementAtIndex(i); EditorGUILayout.PropertyField(point.FindPropertyRelative("offset")); EditorGUILayout.PropertyField(point.FindPropertyRelative("color")); EditorGUILayout.EndHorizontal(); } EditorGUILayout.PropertyField(frequency); EditorGUILayout.PropertyField(centerColor); star.ApplyModifiedProperties(); } }
排版不良的編輯器

咱們須要修正這個排版。讓咱們去掉顏色和位置的標籤,設置顏色條的最大長度爲50像素。咱們經過EditorGUILayout.PropertyField方法的額外參數可以實現。由於咱們對全部的對象都使用相同的配置,因此咱們使用靜態變量來保存這些設置。

而後再經過GUILayout.Label方法來給全部的points添加一個統一的標籤。

What's a GUIContent?

GUIContent is a wrapper object for text, textures, and tooltips that you typically use as labels.

Why use GUILayout instead of EditorGUILayout?

You use the same Unity GUI system for editors that you can use for your games. GUILayout provided basic functionality like labels and buttons, while EditorGUILayout provides extra editor-specific stuff like input fields.

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(Star))]
public class StarInspector : Editor {

	private static GUIContent pointContent = GUIContent.none;
	private static GUILayoutOption colorWidth = GUILayout.MaxWidth(50f); private SerializedObject star; private SerializedProperty points, frequency, centerColor; void OnEnable () { … } public override void OnInspectorGUI () { star.Update(); GUILayout.Label("Points"); for(int i = 0; i < points.arraySize; i++){ EditorGUILayout.BeginHorizontal(); SerializedProperty point = points.GetArrayElementAtIndex(i); EditorGUILayout.PropertyField(point.FindPropertyRelative("offset"), pointContent); EditorGUILayout.PropertyField(point.FindPropertyRelative("color"), pointContent, colorWidth); EditorGUILayout.EndHorizontal(); } EditorGUILayout.PropertyField(frequency); EditorGUILayout.PropertyField(centerColor); star.ApplyModifiedProperties(); } }
一個緊湊的編輯器

最終,看上去好多了!如今,若是咱們能方便的添加和刪除points就更好了。讓咱們試試添加這些按鈕吧。

咱們爲每一個point添加兩個按鈕,一個是「+」用來插入point,一個是"-"用來刪除point。咱們再添加一些說明使用戶可以瞭解這些按鈕的用途。咱們還須要控制按鈕寬度,將樣式設置成mini buttons,由於這些按鈕要小一些。

How does GUILayout.Button work?

The method GUILayout.Button both shows a button and returns whether it was clicked. So you typically call it inside an if statement and perform the necessary work in the corresponding code block.
What actually happens is that your own GUI method, in this case OnInspectorGUI, gets called far more often than just once. It gets called when performing layout, when repainting, and whenever a significant GUI event happens, which is quite often. Only when a mouse click event comes along that is consumed by the button, will it return true.
To get an idea, put Debug.Log(Event.current); at the start of your OnInspectorGUI method and fool around a bit.
Usually you need not worry about this, but be aware of it when performing heavy work like generating textures. You don't want to do that dozens of times per second if you don't need to.

What are the contents of a new item?

If you insert a new array element via a SerializedProperty, the new element will be a duplicate of the element just above it. If there's no other element, it gets default values.

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(Star))]
public class StarInspector : Editor {

	private static GUIContent
		insertContent = new GUIContent("+", "duplicate this point"),
		deleteContent = new GUIContent("-", "delete this point"), pointContent = GUIContent.none; private static GUILayoutOption buttonWidth = GUILayout.MaxWidth(20f), colorWidth = GUILayout.MaxWidth(50f); private SerializedObject star; private SerializedProperty points, frequency, centerColor; void OnEnable () { … } public override void OnInspectorGUI () { star.Update(); GUILayout.Label("Points"); for(int i = 0; i < points.arraySize; i++){ EditorGUILayout.BeginHorizontal(); SerializedProperty point = points.GetArrayElementAtIndex(i); EditorGUILayout.PropertyField(point.FindPropertyRelative("offset"), pointContent); EditorGUILayout.PropertyField(point.FindPropertyRelative("color"), pointContent, colorWidth); if(GUILayout.Button(insertContent, EditorStyles.miniButtonLeft, buttonWidth)){ points.InsertArrayElementAtIndex(i); } if(GUILayout.Button(deleteContent, EditorStyles.miniButtonRight, buttonWidth)){ points.DeleteArrayElementAtIndex(i); } EditorGUILayout.EndHorizontal(); } EditorGUILayout.PropertyField(frequency); EditorGUILayout.PropertyField(centerColor); star.ApplyModifiedProperties(); } }
添加了傳送按鈕和提示

看上去不錯,可是怎麼移動points?若是咱們可以直接拖動這些點來排列它們,那就太棒了。雖然這確定是整齊的,讓咱們用一個簡單辦法來解決它。

咱們能夠給每一個point添加一個傳送按鈕。點一下,你就激活了這個point的顯示。點另外一個,就會跳轉到另外一個point,同時移動視角到當前point。

這種方式須要咱們來記錄哪一個point是當前的焦點。咱們可使用point的索引值來記錄焦點,用-1表示焦點爲空。咱們將改變按鈕的提示信息,信息將根據按鈕的狀態而定,並添加一個標籤來告訴用戶該作什麼。

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(Star))]
public class StarInspector : Editor {

	private static GUIContent
		insertContent = new GUIContent("+", "duplicate this point"),
		deleteContent = new GUIContent("-", "delete this point"),
		pointContent = GUIContent.none,
		teleportContent = new GUIContent("T"); private static GUILayoutOption buttonWidth = GUILayout.MaxWidth(20f), colorWidth = GUILayout.MaxWidth(50f); private SerializedObject star; private SerializedProperty points, frequency, centerColor; private int teleportingElement; void OnEnable () { star = new SerializedObject(target); points = star.FindProperty("points"); frequency = star.FindProperty("frequency"); centerColor = star.FindProperty("centerColor"); teleportingElement = -1; teleportContent.tooltip = "start teleporting this point"; } public override void OnInspectorGUI () { star.Update(); GUILayout.Label("Points"); for(int i = 0; i < points.arraySize; i++){ EditorGUILayout.BeginHorizontal(); SerializedProperty point = points.GetArrayElementAtIndex(i); EditorGUILayout.PropertyField(point.FindPropertyRelative("offset"), pointContent); EditorGUILayout.PropertyField(point.FindPropertyRelative("color"), pointContent, colorWidth); if(GUILayout.Button(teleportContent, EditorStyles.miniButtonLeft, buttonWidth)){ if(teleportingElement >= 0){ points.MoveArrayElement(teleportingElement, i); teleportingElement = -1; teleportContent.tooltip = "start teleporting this point"; } else{ teleportingElement = i; teleportContent.tooltip = "teleport here"; } } if(GUILayout.Button(insertContent, EditorStyles.miniButtonMid, buttonWidth)){ points.InsertArrayElementAtIndex(i); } if(GUILayout.Button(deleteContent, EditorStyles.miniButtonRight, buttonWidth)){ points.DeleteArrayElementAtIndex(i); } EditorGUILayout.EndHorizontal(); } if(teleportingElement >= 0){ GUILayout.Label("teleporting point " + teleportingElement); } EditorGUILayout.PropertyField(frequency); EditorGUILayout.PropertyField(centerColor); star.ApplyModifiedProperties(); } }
有傳送按鈕的編輯器

所見即所得 WYSIWYG

咱們的編輯器對point已經很友好了,但咱們還不能實時看到咱們編輯過程當中的結果。是時候改變這一切了!

第一件事是讓Unity瞭解,咱們的組件須要在編輯模式下被激活。咱們經過ExecuteInEditMode類來聲明這一屬性。此後,star在編輯中的任何顯示,都會調用Start方法。

由於咱們創建了一個Mesh在Start方法中,它將在編輯模式下被建立。正如咱們把Mesh存放在MeshFilter中,它將被保存於場景中。咱們不但願這樣,由於咱們須要動態的建立Mesh。咱們能夠設置HideFlags來阻止Unity保存Mesh。因而,咱們還須要確認Mesh被清理時,編輯器已經再也不須要它。OnDisable事件會在每個組件實效時被調用,它能夠幫咱們處理這些事情。咱們須要在OnDisable中清理MeshFilter來阻止它發出缺乏Mesh的警告。

using System;
using UnityEngine;

[ExecuteInEditMode, RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Star : MonoBehaviour {

	[Serializable]
	public class Point { … }

	public Point[] points;
	public int frequency = 1;
	public Color centerColor;

	private Mesh mesh;
	private Vector3[] vertices;
	private Color[] colors;
	private int[] triangles;

	void Start () {
		GetComponent<MeshFilter>().mesh = mesh = new Mesh();
		mesh.name = "Star Mesh";
		mesh.hideFlags = HideFlags.HideAndDontSave; if(frequency < 1){ frequency = 1; } if(points == null || points.Length == 0){ points = new Point[]{ new Point()}; } int numberOfPoints = frequency * points.Length; vertices = new Vector3[numberOfPoints + 1]; colors = new Color[numberOfPoints + 1]; triangles = new int[numberOfPoints * 3]; float angle = -360f / numberOfPoints; colors[0] = centerColor; for(int iF = 0, v = 1, t = 1; iF < frequency; iF++){ for(int iP = 0; iP < points.Length; iP += 1, v += 1, t += 3){ vertices[v] = Quaternion.Euler(0f, 0f, angle * (v - 1)) * points[iP].offset; colors[v] = points[iP].color; triangles[t] = v; triangles[t + 1] = v + 1; } } triangles[triangles.Length - 1] = 1; mesh.vertices = vertices; mesh.colors = colors; mesh.triangles = triangles; } void OnDisable () { if(Application.isEditor){ GetComponent<MeshFilter>().mesh = null; DestroyImmediate(mesh); } } } 
編輯模式下的星星

咱們的星星已經顯示在了編輯模式中!當咱們在一個對象上關閉Star組件,星星的Mesh將被消除。當咱們啓用Star組件,它將再也不恢復。由於Start方法僅在組件第一次激活時被調用。解決的辦法是將咱們的初始化代碼移動到OnEnable事件中去。

作好以後,咱們進一步重構代碼,讓咱們能隨時初始化Mesh。爲了在不須要的時候不進行初始化,咱們還須要添加少許的檢查。

using System;
using UnityEngine;

[ExecuteInEditMode, RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Star : MonoBehaviour {

	[Serializable]
	public class Point { … }

	public Point[] points;
	public int frequency = 1;
	public Color centerColor;

	private Mesh mesh;
	private Vector3[] vertices;
	private Color[] colors;
	private int[] triangles;

	public void UpdateStar () { if(mesh == null){ GetComponent<MeshFilter>().mesh = mesh = new Mesh(); mesh.name = "Star Mesh"; mesh.hideFlags = HideFlags.HideAndDontSave; } if(frequency < 1){ frequency = 1; } if(points.Length == 0){ points = new Point[]{ new Point()}; } int numberOfPoints = frequency * points.Length; if(vertices == null || vertices.Length != numberOfPoints + 1){ vertices = new Vector3[numberOfPoints + 1]; colors = new Color[numberOfPoints + 1]; triangles = new int[numberOfPoints * 3]; } float angle = -360f / numberOfPoints; colors[0] = centerColor; for(int iF = 0, v = 1, t = 1; iF < frequency; iF++){ for(int iP = 0; iP < points.Length; iP += 1, v += 1, t += 3){ vertices[v] = Quaternion.Euler(0f, 0f, angle * (v - 1)) * points[iP].offset; colors[v] = points[iP].color; triangles[t] = v; triangles[t + 1] = v + 1; } } triangles[triangles.Length - 1] = 1; mesh.vertices = vertices; mesh.colors = colors; mesh.triangles = triangles; } void OnEnable () { UpdateStar (); } void OnDisable () { … } }

如今,組件再被啓動時,星星再也不出現。不幸的是,它再也不相應修改。幸虧,這很容易解決。

SerializedObject.ApplyModifiedProperties方法能夠返回任何修改的實際狀況。這樣,咱們就能很簡單的調用target的UpdateStar方法。咱們須要顯式轉換target的類型,由於編輯器須要爲全部類型提供支持,因此target的類型被定義成了Object。

譯者注,有一種方法能夠簡單的解決這個問題,寫一個基類以下 

public  class InspectorBase<T> : Editor where T : UnityEngine.Object
{
		protected T Target { get { return  (T)target; } }
}                

 

而後所有的編輯器類都繼承這個基類以下 

[CustomEditor(typeof(Star))]
public  class StarEditor : InspectorBase< Star >
{
		......
}            

 

這樣在之後的代碼裏,target會自動成爲你想要的類型。

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(Star))]
public class StarInspector : Editor {

	private static GUIContent
		insertContent = new GUIContent("+", "duplicate this point"),
		deleteContent = new GUIContent("-", "delete this point"),
		pointContent = GUIContent.none,
		teleportContent = new GUIContent("T");

	private static GUILayoutOption
		buttonWidth = GUILayout.MaxWidth(20f),
		colorWidth = GUILayout.MaxWidth(50f);

	private SerializedObject star;
	private SerializedProperty
		points,
		frequency,
		centerColor;

	private int teleportingElement;

	void OnEnable () { … }

	public override void OnInspectorGUI () {
		star.Update();

		GUILayout.Label("Points");
		for(int i = 0; i < points.arraySize; i++){
			EditorGUILayout.BeginHorizontal();
			SerializedProperty point = points.GetArrayElementAtIndex(i);
			EditorGUILayout.PropertyField(point.FindPropertyRelative("offset"), pointContent);
			EditorGUILayout.PropertyField(point.FindPropertyRelative("color"), pointContent, colorWidth);

			if(GUILayout.Button(teleportContent, EditorStyles.miniButtonLeft, buttonWidth)){
				if(teleportingElement >= 0){
					points.MoveArrayElement(teleportingElement, i);
					teleportingElement = -1;
					teleportContent.tooltip = "start teleporting this point";
				}
				else{
					teleportingElement = i;
					teleportContent.tooltip = "teleport here";
				}
			}
			if(GUILayout.Button(insertContent, EditorStyles.miniButtonMid, buttonWidth)){
				points.InsertArrayElementAtIndex(i);
			}
			if(GUILayout.Button(deleteContent, EditorStyles.miniButtonRight, buttonWidth)){
				points.DeleteArrayElementAtIndex(i);
			}

			EditorGUILayout.EndHorizontal();
		}
		if(teleportingElement >= 0){
			GUILayout.Label("teleporting point " + teleportingElement);
		}

		EditorGUILayout.PropertyField(frequency);
		EditorGUILayout.PropertyField(centerColor);

		if(star.ApplyModifiedProperties()){ ((Star)target).UpdateStar(); } } }
編輯中的星星

如今,Mesh沒有當即更新。這讓編輯輕鬆許多!惋惜的是,它尚未支持Undo!

不幸的是,在Unity中沒有一種簡單的方法來支持Undo事件,但咱們能夠作到接近支持。在咱們的案例中,咱們能夠檢查ValidateCommand事件是否發生,來判斷Undo操做。當前被選中的對象這個事件的目標,咱們假設它被修改過。

What's a ValidateCommand?

ValidateCommand is a type of GUI event, which indicates that some special action happened, like undo or redo. So why isn't it called something like ExecuteCommand? Actually, that command type exists as well. While they have a slightly different meaning, in practice you use them for the exact same purpose. Unfortunately, depening on exactly where you're checking and how you're constructing your GUI, either one or the other event happens, but not both. Why this is so, I do not know.
So to be perfectly safe, you have to check for both command types. In this case, however, you can suffice with checking ValidateCommand.

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(Star))]
public class StarInspector : Editor {

	private static GUIContent
		insertContent = new GUIContent("+", "duplicate this point"),
		deleteContent = new GUIContent("-", "delete this point"),
		pointContent = GUIContent.none,
		teleportContent = new GUIContent("T");

	private static GUILayoutOption
		buttonWidth = GUILayout.MaxWidth(20f),
		colorWidth = GUILayout.MaxWidth(50f);

	private SerializedObject star;
	private SerializedProperty
		points,
		frequency,
		centerColor;

	private int teleportingElement;

	void OnEnable () { … }

	public override void OnInspectorGUI () {
		star.Update();

		GUILayout.Label("Points");
		for(int i = 0; i < points.arraySize; i++){
			EditorGUILayout.BeginHorizontal();
			SerializedProperty point = points.GetArrayElementAtIndex(i);
			EditorGUILayout.PropertyField(point.FindPropertyRelative("offset"), pointContent);
			EditorGUILayout.PropertyField(point.FindPropertyRelative("color"), pointContent, colorWidth);

			if(GUILayout.Button(teleportContent, EditorStyles.miniButtonLeft, buttonWidth)){
				if(teleportingElement >= 0){
					points.MoveArrayElement(teleportingElement, i);
					teleportingElement = -1;
					teleportContent.tooltip = "start teleporting this point";
				}
				else{
					teleportingElement = i;
					teleportContent.tooltip = "teleport here";
				}
			}
			if(GUILayout.Button(insertContent, EditorStyles.miniButtonMid, buttonWidth)){
				points.InsertArrayElementAtIndex(i);
			}
			if(GUILayout.Button(deleteContent, EditorStyles.miniButtonRight, buttonWidth)){
				points.DeleteArrayElementAtIndex(i);
			}

			EditorGUILayout.EndHorizontal();
		}
		if(teleportingElement >= 0){
			GUILayout.Label("teleporting point " + teleportingElement);
		}

		EditorGUILayout.PropertyField(frequency);
		EditorGUILayout.PropertyField(centerColor);

		if(
			star.ApplyModifiedProperties() ||
			(Event.current.type == EventType.ValidateCommand && Event.current.commandName == "UndoRedoPerformed") ){ ((Star)target).UpdateStar(); } } }

最後,一個舒服的編輯過程!還有什麼須要作嗎?在編輯器的右上角有一個齒輪圖標可以重置組件。當咱們重置Star組件的時候咱們的Mesh沒有及時更新。

你能夠定義Reset方法來監聽一個組件的重置。這事Unity爲Editor及其子類提供的一個方法。當這個事件發生,咱們只要及時更新咱們的星星就能夠了。

using System;
using UnityEngine;

[ExecuteInEditMode, RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Star : MonoBehaviour {

	[Serializable]
	public class Point { … }

	public Point[] points;
	public int frequency = 1;
	public Color centerColor;

	private Mesh mesh;
	private Vector3[] vertices;
	private Color[] colors;
	private int[] triangles;

	public void UpdateStar () { … }

	void OnEnable () { … }

	void OnDisable () { … }

	void Reset () {
		UpdateStar(); } }

OK咱們開始寫Reset。咱們要作什麼?咱們來試試prefabs?

如今使用prefabs對於咱們star並無太多意義,由於每個star都擁有本身的獨立的Mesh。若是你想使用不少個同樣的star,那在創建一個3D模型而且導入Mesh是一個好主意。這樣全部的star就共享了同一個Mesh。但假設咱們使用prefab,就能夠實例化多個一樣的star而後咱們還可以調整它們。

你只要簡單的拖拽一個star從層級視圖到項目視圖,就能創建一個prefab。對prefab的更新可以影響所有的prefab實例,由於每一個prefab的修改都會觸發OnDisable和OnEnable。將一個實例回覆成prefab一樣的狀態它依然可以工做。

惟一咱們沒有徹底作好的事情是prefab的MeshFilter會顯示它的Mesh類型不匹配。這事由於prefab是一個實際的資源,而動態生成的Mesh不是。這不影響功能,但仍是讓咱們解決它吧。

一個修改後提示類型不匹配的prefab

爲了中止prefab生成它的Mesh,咱們不能再調用UpdateStar方法。不幸的是,這表明咱們將不能再看到預覽了。咱們能夠用PrefabUtility.GetPrefabType方法來檢測編輯窗口當前的對象是否是prefab。若是是,咱們簡單的不更新它就好了。

using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(Star))]
public class StarInspector : Editor {

	private static GUIContent
		insertContent = new GUIContent("+", "duplicate this point"),
		deleteContent = new GUIContent("-", "delete this point"),
		pointContent = GUIContent.none,
		teleportContent = new GUIContent("T");

	private static GUILayoutOption
		buttonWidth = GUILayout.MaxWidth(20f),
		colorWidth = GUILayout.MaxWidth(50f);

	private SerializedObject star;
	private SerializedProperty
		points,
		frequency,
		centerColor;

	private int teleportingElement;

	void OnEnable () { … }

	public override void OnInspectorGUI () {
		star.Update();

		GUILayout.Label("Points");
		for(int i = 0; i < points.arraySize; i++){
			EditorGUILayout.BeginHorizontal();
			SerializedProperty point = points.GetArrayElementAtIndex(i);
			EditorGUILayout.PropertyField(point.FindPropertyRelative("offset"), pointContent);
			EditorGUILayout.PropertyField(point.FindPropertyRelative("color"), pointContent, colorWidth);

			if(GUILayout.Button(teleportContent, EditorStyles.miniButtonLeft, buttonWidth)){
				if(teleportingElement >= 0){
					points.MoveArrayElement(teleportingElement, i);
					teleportingElement = -1;
					teleportContent.tooltip = "start teleporting this point";
				}
				else{
					teleportingElement = i;
					teleportContent.tooltip = "teleport here";
				}
			}
			if(GUILayout.Button(insertContent, EditorStyles.miniButtonMid, buttonWidth)){
				points.InsertArrayElementAtIndex(i);
			}
			if(GUILayout.Button(deleteContent, EditorStyles.miniButtonRight, buttonWidth)){
				points.DeleteArrayElementAtIndex(i);
			}

			EditorGUILayout.EndHorizontal();
		}
		if(teleportingElement >= 0){
			GUILayout.Label("teleporting point " + teleportingElement);
		}

		EditorGUILayout.PropertyField(frequency);
		EditorGUILayout.PropertyField(centerColor);

		if(
			star.ApplyModifiedProperties() ||
			(Event.current.type == EventType.ValidateCommand &&
			Event.current.commandName == "UndoRedoPerformed")
		){
			if(PrefabUtility.GetPrefabType(target) != PrefabType.Prefab){
				((Star)target).UpdateStar();
			} } } }
prefab沒有了Mesh和預覽

OK,咱們完成了,真的?我沒尚未對同時存在多個對象的狀況進行支持。試試同時選擇多個star。

尚未提供多對象編輯

讓咱們嘗試多對象編輯功能吧。首先,咱們須要給類添加一個屬性讓編輯器提供相應的支持。而後咱們須要初始化全部target的SerializedObject,而再也不只是一個。咱們還須要把任何變化同步到所有的target上。

這樣就能在編輯器中支持多個對象了,但若是一些star的point個數不同,就會出錯。由於在Unity的編輯器嘗試讀取所有點的資料的時候,有些點會不存在。咱們能夠在得到每一個point的數據的時候檢查一下這個point是否存在,若是不存在,就中止取值。因此咱們只須要顯示一個star所擁有的數量的point就能夠了。

using UnityEditor;
using UnityEngine;

[CanEditMultipleObjects, CustomEditor(typeof(Star))]
public class StarInspector : Editor {

	private static GUIContent
		insertContent = new GUIContent("+", "duplicate this point"),
		deleteContent = new GUIContent("-", "delete this point"),
		pointContent = GUIContent.none,
		teleportContent = new GUIContent("T");

	private static GUILayoutOption
		buttonWidth = GUILayout.MaxWidth(20f),
		colorWidth = GUILayout.MaxWidth(50f);

	private SerializedObject star;
	private SerializedProperty
		points,
		frequency,
		centerColor;

	private int teleportingElement;

	void OnEnable () {
		star = new SerializedObject(targets); points = star.FindProperty("points"); frequency = star.FindProperty("frequency"); centerColor = star.FindProperty("centerColor"); teleportingElement = -1; teleportContent.tooltip = "start teleporting this point"; } public override void OnInspectorGUI () { star.Update(); GUILayout.Label("Points"); for(int i = 0; i < points.arraySize; i++){ SerializedProperty point = points.GetArrayElementAtIndex(i), offset = point.FindPropertyRelative("offset"); if(offset == null){ break; } EditorGUILayout.BeginHorizontal(); EditorGUILayout.PropertyField(offset, pointContent); EditorGUILayout.PropertyField(point.FindPropertyRelative("color"), pointContent, colorWidth); if(GUILayout.Button(teleportContent, EditorStyles.miniButtonLeft, buttonWidth)){ if(teleportingElement >= 0){ points.MoveArrayElement(teleportingElement, i); teleportingElement = -1; teleportContent.tooltip = "start teleporting this point"; } else{ teleportingElement = i; teleportContent.tooltip = "teleport here"; } } if(GUILayout.Button(insertContent, EditorStyles.miniButtonMid, buttonWidth)){ points.InsertArrayElementAtIndex(i); } if(GUILayout.Button(deleteContent, EditorStyles.miniButtonRight, buttonWidth)){ points.DeleteArrayElementAtIndex(i); } EditorGUILayout.EndHorizontal(); } if(teleportingElement >= 0){ GUILayout.Label("teleporting point " + teleportingElement); } EditorGUILayout.PropertyField(frequency); EditorGUILayout.PropertyField(centerColor); if( star.ApplyModifiedProperties() || (Event.current.type == EventType.ValidateCommand && Event.current.commandName == "UndoRedoPerformed") ){ foreach(Star s in targets){ if(PrefabUtility.GetPrefabType(s) != PrefabType.Prefab){ s.UpdateStar(); } } } } }
scene view inspector
多對象編輯

在場景中編輯 Editing in the Scene View

如今咱們擁有了一個很不錯的編輯器了,但若是咱們能直接在場景裏編輯這些point會不會更酷一些?用OnSceneGUI事件,咱們能夠作到。這個方法會在一個對象被選中即將賦予target時調用。咱們不能在這個事件中使用SerializedObject。事實上,你能夠認爲這個方法與咱們編輯器類中的其它部分是徹底分離的。

Why does OnSceneGUI mess with target?

Probably for backwards compatibility. Multi-object editing was introduced in Unity 3.5. Versions before that only had the target variable.

using UnityEditor;
using UnityEngine;

[CanEditMultipleObjects, CustomEditor(typeof(Star))]
public class StarInspector : Editor {

	private static GUIContent
		insertContent = new GUIContent("+", "duplicate this point"),
		deleteContent = new GUIContent("-", "delete this point"),
		pointContent = GUIContent.none,
		teleportContent = new GUIContent("T");

	private static GUILayoutOption
		buttonWidth = GUILayout.MaxWidth(20f),
		colorWidth = GUILayout.MaxWidth(50f);

	private SerializedObject star;
	private SerializedProperty
		points,
		frequency,
		centerColor;

	private int teleportingElement;

	void OnEnable () { … }

	public override void OnInspectorGUI () { … }

	void OnSceneGUI () {}
}

讓咱們設置一個方形的小手柄在star所有的point上面。咱們只要在這些point的第一個重複週期裏顯示手柄就能夠了,不須要把所有的重複週期都顯示出來。放置這些手柄就好象生成Mesh同樣,除了咱們使用的是世界座標系,不是本地座標系,因此咱們要用到star的transform。

咱們能夠經過Handles.FreeMoveHandle方法來繪製咱們的手柄。首先,須要一個世界座標系的位置,手柄的位置。其次,須要一個繪製手柄的角度,但咱們不須要旋轉。而後,還須要手柄的尺寸,咱們用一個很小的尺寸就夠了。咱們用一個vector來保存這個尺寸,能夠設置成(0.1, 0.1 0.1)。最後一個參數是定義手柄的形狀。

How do we convert to world space?

You convert a point from local to world space by appling all transformation matrices of its object hierarchy to it. Unity takes care of this when rendering the scene, but sometimes you need to do it yourself. You can use the Transform.TransformPoint method for this.

using UnityEditor;
using UnityEngine;

[CanEditMultipleObjects, CustomEditor(typeof(Star))]
public class StarInspector : Editor {

	private static Vector3 pointSnap = Vector3.one * 0.1f;

	private static GUIContent
		insertContent = new GUIContent("+", "duplicate this point"),
		deleteContent = new GUIContent("-", "delete this point"),
		pointContent = GUIContent.none,
		teleportContent = new GUIContent("T");

	private static GUILayoutOption
		buttonWidth = GUILayout.MaxWidth(20f),
		colorWidth = GUILayout.MaxWidth(50f);

	private SerializedObject star;
	private SerializedProperty
		points,
		frequency,
		centerColor;

	private int teleportingElement;

	void OnEnable () { … }

	public override void OnInspectorGUI () { … }

	void OnSceneGUI () {
		Star star = (Star)target; Transform starTransform = star.transform; float angle = -360f / (star.frequency * star.points.Length); for(int i = 0; i < star.points.Length; i++){ Quaternion rotation = Quaternion.Euler(0f, 0f, angle * i); Vector3 oldPoint = starTransform.TransformPoint(rotation * star.points[i].offset); Handles.FreeMoveHandle(oldPoint, Quaternion.identity, 0.04f, pointSnap, Handles.DotCap); } } }
在場景中添加了控制手柄

如今還有什麼能夠作到更好嗎?你能夠點擊一個手柄,讓它變成黃色。咱們須要比較一個手柄的初始化位置和返回位置。若是不一樣,說明用戶拖動了手柄,咱們須要將改變同步到star。star的Mesh使用本地座標系,在把座標改變保存以前,不要忘記轉換座標。

How do we convert to local space?

You have to perform the exact opposite steps for converting to world space, in reverse order. You can use the Transform.InverseTransformPoint method for this. Note that when going to world space we rotated in local space first, then transformed. So to convert back, we inverse transform first, then inverse rotate in local space.

using UnityEditor;
using UnityEngine;

[CanEditMultipleObjects, CustomEditor(typeof(Star))]
public class StarInspector : Editor {

	private static Vector3 pointSnap = Vector3.one * 0.1f;

	private static GUIContent
		insertContent = new GUIContent("+", "duplicate this point"),
		deleteContent = new GUIContent("-", "delete this point"),
		pointContent = GUIContent.none,
		teleportContent = new GUIContent("T");

	private static GUILayoutOption
		buttonWidth = GUILayout.MaxWidth(20f),
		colorWidth = GUILayout.MaxWidth(50f);

	private SerializedObject star;
	private SerializedProperty
		points,
		frequency,
		centerColor;

	private int teleportingElement;

	void OnEnable () { … }

	public override void OnInspectorGUI () { … }

	void OnSceneGUI () {
		Star star = (Star)target;
		Transform starTransform = star.transform;

		float angle = -360f / (star.frequency * star.points.Length);
		for(int i = 0; i < star.points.Length; i++){
			Quaternion rotation = Quaternion.Euler(0f, 0f, angle * i);
			Vector3
				oldPoint = starTransform.TransformPoint(rotation * star.points[i].offset),
				newPoint = Handles.FreeMoveHandle (oldPoint, Quaternion.identity, 0.04f, pointSnap, Handles.DotCap); if(oldPoint != newPoint){ star.points[i].offset = Quaternion.Inverse(rotation) * starTransform.InverseTransformPoint(newPoint); star.UpdateStar(); } } } }
咱們能夠在場景中編輯了

有用了!不過咱們還沒支持Undo!這裏咱們不能靠SerializedObject來解決問題,不過幸虧這些手柄能夠支持Undo。咱們只須要告訴編輯器哪一個對象被改變了,咱們還應該爲此次改變起一個名字。咱們能夠用Undo.SetSnapshotTarget來作這些事。

What's a snapshot?

If an undo step would be created for each GUI event, dragging a handle would result in an undo history filled with dozens of tiny modifications. Instead, the handles make a copy – a snapshot – of the object when movement begins and only register a single undo step with the copy when movement ends. SetSnapshotTarget tells the handles which object to use for this.
All Unity editor GUI elements essentialy do the same thing, whether it's for draggin handles, sliding numbers, typing text, or whatever.

using UnityEditor;
using UnityEngine;

[CanEditMultipleObjects, CustomEditor(typeof(Star))]
public class StarInspector : Editor {

	private static Vector3 pointSnap = Vector3.one * 0.1f;

	private static GUIContent
		insertContent = new GUIContent("+", "duplicate this point"),
		deleteContent = new GUIContent("-", "delete this point"),
		pointContent = GUIContent.none,
		teleportContent = new GUIContent("T");

	private static GUILayoutOption
		buttonWidth = GUILayout.MaxWidth(20f),
		colorWidth = GUILayout.MaxWidth(50f);

	private SerializedObject star;
	private SerializedProperty
		points,
		frequency,
		centerColor;

	private int teleportingElement;

	void OnEnable () { … }

	public override void OnInspectorGUI () { … }

	void OnSceneGUI () {
		Star star = (Star)target;
		Transform starTransform = star.transform;
		Undo.SetSnapshotTarget(star, "Move Star Point");

		float angle = -360f / (star.frequency * star.points.Length);
		for(int i = 0; i < star.points.Length; i++){
			Quaternion rotation = Quaternion.Euler(0f, 0f, angle * i);
			Vector3
				oldPoint = starTransform.TransformPoint(rotation * star.points[i].offset),
				newPoint = Handles.FreeMoveHandle
					(oldPoint, Quaternion.identity, 0.04f, pointSnap, Handles.DotCap);
			if(oldPoint != newPoint){
				star.points[i].offset = Quaternion.Inverse(rotation) *
					starTransform.InverseTransformPoint(newPoint);
				star.UpdateStar();
			}
		}
	}
}
相關文章
相關標籤/搜索