Star
自定義編輯器簡易教程 an introduction to custom editors
原文地址 http://catlikecoding.com/unity/tutorials/star/
http://blog.csdn.net/lilanfei/article/details/7680802

簡介 Introduction
這個教程將讓你學會如何創建一個星型控件以及如何制作這個控件的自定義編輯器。你將學會:
- 動態的建立Mesh。
- 使用一個嵌套類。
- 建立一個自定義編輯器。
- 使用
SerializedObject
。 - 支持所見即所得。
- 對Undo、Redo、Reset和prefab提供支持。
- 支持多對象編輯。
- 支持場景視圖內編輯。
我們假設你已經學會了Unity C#的基礎編程知識,以及Unity 編輯器的基礎知識。如果你已經完成了相關的學習,Let's Go!
建立Star類 Creating the star
我們建立一個全新的Unity工程,然后建立一個新的C#腳本,將它命名為Star。我們將用這個腳本,建立一個由三角面拼接成的星,這里需要一個Mesh。
什么是Mesh?
3D模型是由多邊形拼接而成,一個復雜的多邊形,實際上是由多個三角面拼接而成。所以一個3D模型的表面是由多個彼此相連的三角面構成。三維空間中,構成這些三角面的點以及三角形的邊的集合就是Mesh。
using UnityEngine;
public class Star : MonoBehaviour { private Mesh mesh; }
任何對於Mesh的使用,都必須搭配一個MeshFilter組件,而MeshFilter又被用於MeshRenderer組件。只有這樣,才能被Unity繪制。所以,這些組件都必須被加載到GameObject對象上,我們的Star對象也必須這么做。
當然,我們可以手動添加這些組件,但默認的自動添加是一個更好的辦法。所以我們需要添加一個RequireComponent類作為Star對象的一個特性。
什么是類的特性?
特性像對類附加一個標簽,用來告訴編譯器這個類需要如何處理。是除了類聲明的代碼之外,對類做的附加說明。另外,特性不止針對類,對方法和屬性同樣適用。
typeof有什么用?
typeof是一種運算符,能夠獲得任何類的類型描述數據,數據里最常用的就是類的名字。那為什么不直接在代碼里寫類的名字就好呢?因為代碼中所有的賦值和運算都需要變量,直接使用類的名字會導致編譯錯誤。
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"; } }


當然,現在我們在預覽模式下還看不到任何東西,因為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; } }


現在,我們的星星看起來還只是一個簡單的多邊形。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。


頂點着色默認是白色,所以我們的多邊形現在變成了白色。我們想要一個更漂亮的星星。所以我們來為每個點定義一種顏色。
我們再添加一個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; } }


我們需要些顏色!如果把全部的頂點都指定相同的顏色就很簡單,但這樣太無聊了。我們來試試給每個頂點分配一個顏色。我們需要一個數組來保存這些顏色數據,而且必須保持顏色和頂點的數量一致。這有點小麻煩,我們干脆換成另外一種方式,在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; } }


最后是關於中心點的。現在,我們還沒有給它設置顏色,所以它一直保持着透明。讓我們來為它添加一個顏色屬性,最終,這個星星看上去變漂亮了。
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; } }



建立編輯器 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生成它的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();
} } } }

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(); } } } } }


在場景中編輯 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();
}
}
}
}