Star
an introduction to custom editors

Introduction
- dynamically generate a mesh
- use a nested class
- create a custom editor
- use
SerializedObject - support WYSIWYG editing
- react to undo, redo, reset, and prefab modifications
- support multi-object editing
- support editing in the scene view
You're assumed to know your way around Unity's editor and know the basics of Unity C# scripting. If you've completed some of the other tutorials then you're good to go.
Creating the star
Mesh.
using UnityEngine;
public class Star : MonoBehaviour {
private Mesh mesh;
}
MeshFilter component, which in turn
is used by a MeshRenderer component. Only then will the mesh be drawn by Unity.
So it is required that both these components are attached to the game object that our star component
is also attached to.
Of course we can manually add these components, but it's a better idea to do this automatically. So we'll
add a RequireComponent class attribute to our class.
using UnityEngine;
[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class Star : MonoBehaviour {
private Mesh mesh;
}
Start Unity event method for now, so it
happens as soon as we enter play mode. We also assign the mesh to the MeshFilter in one go
and give it a descriptive name.
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";
}
}
The first vertex of our triangle fan sits at the center of the star, with all other vertices placed around it clockwise. We'll use a quaternion to rotate the points. The rotation angle is negative because we assume that we're looking down the Z axis, which makes positive rotation around Z go counterclockwise. We don't need to set the first vertex because vectors are set to zero by default.
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;
}
}
Let's create a new shader asset and name it Star, then put the following code in it.
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
}
}
}
Let's also add a frequency option so we can automatically repeat point sequences instead of having
to configure every single point of the star. This option replaces numberOfPoints.
Finally, we include a check to make sure that the frequency is positive and that there's at least one point. If we won't we would get in trouble with the array.
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 to hold
both the color and offset value of a single point. Then we use an array of that instead of our vector array.
By defining the Point class inside Star, it will be know to the outside
world as Star.Point, while inside we can suffice with just Point. By
adding the System.Serializable attribute to the class, Unity will be able to save its data.
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;
}
}
The final part is the center of the star. Right now, we're not setting its color, so it will always be fully transparent. So let's add an option to configure that and tweak the star so it finally looks neat.
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
All scripts that deal with editor stuff should sit inside a folder named Editor, otherwise it won't work right in Unity. It doesn't matter where these folders are located, so we'll just place one in the project root. Inside it, create a new C# script named StarInspector.
Editor instead of MonoBehaviour.
We also need to add a class attribute to it which tells Unity that it is a custom editor for Star components.
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(Star))]
public class StarInspector : Editor {}
OnInspectorGUI method of the Editor class.
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(Star))]
public class StarInspector : Editor {
public override void OnInspectorGUI () {}
}
Because we don't do anything yet in OnInspectorGUI, there's nothing to show. We
could get the old inspector contents back by calling the DrawDefaultInspector method,
but that's exactly what we want to get rid of, so let's not.
The first thing we need to do is figure out which star is selected whenever our inspector is shown.
We can use target for that, which is a variable of Editor that we inherited.
While we could use this target directly, we'll wrap it inside a SerializedObject. While this is
not strictly necessary, it is very convenient because it makes a lot of editor stuff easier, like undo support.
When using a SerializedObject, you can access its contents by extracting SerializedProperty
instances from it. We'll do this for all three star variables and initialize everything in the editor's OnEnable Unity event method.
This event happens whenever we select a game object that has a Star component.
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
is up to date. So that's the first thing we do in OnInspectorGUI. After that, we can
show our properties with a simple call to EditorGUILayout.PropertyField, instructing
points to also show its individual array elements.
And after that, we end with applying all property modifications to the selected component.
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();
}
}
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();
}
}
EditorGUILayout.PropertyField method. Because we'll always be using the same
configuration we store these settings in static variables.
Let's also use the GUILayout.Label method to add a label above all the points.
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();
}
}
We'll add two button to each point, one labeled "+" to insert, and one labeled "-" to delete. We'll include tooltips in case the user is unsure what the buttons do. We also limit the width of the buttons and style them as mini buttons, because they should be small.
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();
}
}
We're going to add a teleport button to each point. Press one, you activate the teleporter for that point. Press another one, the active point teleports there, shoving the other points aside.
This approach requires us to track which point is our current teleport candidate, if any. We'll use
the point index for that and reserve -1 for when the teleporter isn't active. We'll
change the teleport button's tooltip depeding on this state and also add a label that tells the user
what to do.
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
The first thing we need to do is tell Unity that our component should be active in edit mode. We indicate this
by adding the ExecuteInEditMode class attribute. From now on, our Start method
will be called whenever a star manifests in the editor.
Because we create a mesh in Start, it will be created in edit mode. As we assign it to a
MeshFilter, it will persist and be saved in the scene. We don't want this to happen, because
we generate the mesh dynamically. We can prevent Unity from saving the
mesh by settings the appropriate HideFlags. However, now we also need to make sure that the mesh
is cleaned up whenever it is no longer needed in the editor. The best place to do this is inside the OnDisable Unity event method,
which is called whenever the component becomes disabled. We also clean the MeshFilter to prevent it from reporting a missing 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 component or the entire object,
the star mesh will disappear. However, it won't reappear if we turn it back on. That's because
Start only gets called the first time the component is activated. The solution is to
move our initialization to the OnEnable Unity event method.
While we're at it, let's go a step further and move the code to its own method so we can intialize the mesh whenever we want to. We'll also add a few checks that prevent recreation of the mesh and the arrays if that's not needed.
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 () { … }
}
The SerializedObject.ApplyModifiedProperties method returns whether any modifications were
actually made. If so, we simply call the UpdateStar method of the target. We need
to explicitly cast it to Star because an editor can work with all kind of objects, so it
uses the generic Object type.
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();
}
}
}
Unfortunately, there's no easy universal guaranteed way to detect undo events in Unity, but we can get pretty close. In our case, we can suffice by checking whether a "ValidateCommand" event happened that refers to an undo action. As this event must relate to the currently selected object, we just assume it was our component that got modified.
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();
}
}
}
You can detect a component reset by adding a Reset method. This is a Unity event method that is only
used inside the editor. Whenever this event happens, all we need to do is update our star.
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();
}
}
Now it doesn't make much sense to use prefabs for our star, because each star generates its own little mesh. If you wanted to use lots of similar stars, it would be a better idea to just create a star model in a 3D editor and import the mesh. That way all the stars can share the same mesh. But suppose we do want to use a prefab, just to instantiate similar stars that we might later tweak individually.
Fortunately, you can simply drag a star from the hierarchy into the project view and you get a prefab
of it. Updates even propagate to the prefab instances, because every prebab modification triggers
OnDisable and OnEnable, which we already react to. Reverting an instance to
its prefab state also works as it should.
The only thing that isn't completely all right is that the prefab's MeshFilter shows
a type mismatch for its mesh value. This is because the prefab is an asset, while the generated mesh
isn't. This appears harmless, but let's get rid of it anyway.
UpdateStar method
anymore. Unfortunately, this also means that they won't show a preview
anymore either. We can use the PrefabUtility.GetPrefabType method to detect whether our inspector
target is a prefab. If so, we simply won't update it.
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();
}
}
}
}
SerializedObject with all targets, instead of a single one. We also need
to make sure we update all targets when we detect a change.
That would be enough to get multi-object editing to work, except that it will go wrong if some of the selected stars don't have the same number of points. That's because Unity's GUI will try to read the values of points that don't exist. We can prevent this by fetching each point's offset and checking whether it exists. If not, we stop. So we only show as much points as the star with the least amount of points has.
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
OnSceneGUI Unity event method to our inspector, we can.
This method will be called once per selected object, during which that object will be assigned to the target
variable. We shouldn't use our SerializedObject here. In fact, it's best to think of this
method as being completely separate from the rest of our editor.
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 () {}
}
We'll use the Handles.FreeMoveHandle method to draw our handles, which has a couple of parameters.
First, it needs the position – in world space – for the handle. Then it needs the rotation of
the handle, which we'll just leave unrotated. Next it wants the size of the handle, we'll use a small value
here that looks good. Then comes a vector used for the snapping size (hold Control or Command to snap), which we configure as (0.1, 0.1 0.1). The last
parameter is used to define the shape of the handle.
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);
}
}
}
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();
}
}
}
}
SerializedObject here, but
fortunately the handles can take care of the undo stuff for us. All we need to do is tell them which
object is being edited and how the undo step should be named. We can do that with the
Undo.SetSnapshotTarget method.
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();
}
}
}
}
Downloads
- star.unitypackage
- The finished project.