Old Star

an introduction to custom editors in Unity 3

Introduction

This is an old tutorial about editor scripting, for those who still use Unity 3. If you're using Unity 4, do the other editor tutorials instead.

In this tutorial you will create a simple star component and write your own custom editor for it. You will learn to

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

We start by creating a new empty project and adding a new C# script named Star. We'll use this script to create a triangle fan to produce a starlike effect, which requires a Mesh.
using UnityEngine;

public class Star : MonoBehaviour {

	private Mesh mesh;

}
For this mesh to be of any use, it must be assigned to a 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;

}
Now we create a new empty game object, name it My First Star, and drag our script onto it. You will see that the object has also gained the other two components.
drag one, get three
The next step is to create a mesh. We'll do this in the 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";
	}
}
without mesh with mesh
without mesh in editor mode vs with mesh in play mode
Of course we won't see anything yet when entering play mode, because the mesh remains empty. So let's add an array of vertices, an option to control how many points our star has, and where these points should be placed relative to the center of the star.

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;
	}
}
new inspector properties
Triangles are stored as an array of vertex indices, three per triangle. Because we're using a triangle fan approach, every triangle starts at the first vertex and connects with the previous and next triangle. The last triangle wraps back to the first one. Fox example, if we had four triangles, the vertex indices would be {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
one ugly star
Right now our star looks like a very ugly polygon. Unity also complains about missing texture coordinates because the default shader expects them. Because we won't be using a texture at all, let's get rid of that warning by creating our own shader that only uses vertex colors.

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
		}
	}
}
Now we create a new material named – you guessed it – Star, set its shader to the one we just created, and drag it onto My First Star.
game view inspector
now with material
Vertex colors are white by default, so now our polygon turns up white. However, we want a multicolored star with points at varying distances from the center. So instead of configuring a single point, let's configure an array of points instead.

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;
	}
}
scene view inspector
now with pointy points
It's time to add some color! The easy way would be to give the same color to all points, but that would be boring. Instead, we allow each point to have its own color. We could add an array to hold the point colors, but we must make sure that it will always contain as many elements as the points array. However, we choose a different approach. We will create a new class inside 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;
	}
}
game view inspector
now with colored points
If you see nothing after this step, try adjusting the alpha component of the point colors.

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;
	}
}
game view scene view inspector
neat star, terrible inspector

Creating the Inspector

Now the star looks nice, but it was a pain to design. The inspector is simply aweful. So let's make our own!

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.

project with inspector script
Because our class is a custom editor, it needs to inherit from 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 {}
So far, no change to our star's inspector. We need to replace the default inspector code with our own. We do this by overriding the OnInspectorGUI method of the Editor class.
using UnityEditor;
using UnityEngine;

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

	public override void OnInspectorGUI () {}
}
a rather empty inspector
Hey, what happened to our inspector? It's gone!

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 () {}
}
Each time the inspector gets updated, we need to make sure that the 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();
	}
}
a reconstructed inspector
Now we more or less have the same inspector we started with, but we can do way better than that by drawing the individual points manually. We'll iterate over all the points and place their colors and offsets next to each other.
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();
	}
}
inspector with bad layout
We definitely need to fix this layout. Let's get rid of the labels for color and offset and set the maximum width for the color bar to 50 pixels. We can do this by providing extra information to the 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();
	}
}
a compact inspector
Finally, something that looks decent! Now it would be really sweet if we could insert and delete points at any place in the array. So let's add buttons to support exactly that.

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();
	}
}
inspector with buttons and tooltips
This looks good, but what about moving points? It would be awesome if we could just drag the points to reorder them. While that would certainly be neat, let's settle for a simpler approach.

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();
	}
}
cool inspectors have a teleporter

WYSIWYG

While our inspector is pretty good at this point, it's a bummer we can't actually see the star while we're editing it. It's about time we change that!

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 in edit mode
Our star just showed up in editor mode! And if we switch off the 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 () { … }
}
Now the star does reappear when its component becomes enabled again. Unfortunately, it doesn't respond to modifications yet. Fortunately, this is easy to solve.

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();
		}
	}
}
the joys of editing
Now the mesh does get updated immediately. This makes editing a whole lot easier! Alas, it does not respond to undo!

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();
		}
	}
}
Finally, sweet editing! Anything else? Well, ever reset a component? At the top right of each component's inspector sits a gear icon with an option to reset that component. Sure enough, our mesh does not get updated when you reset the star component.

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();
	}
}
Ok, now resetting works too. Are we done? Well, what about prefabs?

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.

prefab with type mismatch after modification
To stop prefabs from generating their own mesh, we shouldn't call their 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();
			}
		}
	}
}
prefab without mesh and preview
Ok, now we're finished, right? Not if we want to support editing more than one star at the same time, because right now we don't. Just select multiple stars and try it.
no multi-object editing support yet
So let's support multi-object editing. First, we have to add a class attribute to indicate that our editor supports it. Then we need to initialize our 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();
				}
			}
		}
	}
}
scene view inspector
multi-object editing

Editing in the Scene View

Now we do have a nice inspector indeed, but wouldn't it be cool if we could edit the points directly in the scene view? By adding the 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 () {}
}
Let's put a small square handle at the top of the star's points. We only do this the first time a point appears, not for all their repetitions due to frequency. Placing these points works just like generating the star's mesh, except that we're working in world space here, not local space, so we need to apply the star's transform.

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);
		}
	}
}
extra points for the scene view
We now have nice handles that don't do much yet. You can click one and it becomes yellow, that's it. What we need to do is compare the position we put into the handle with the position that the handle returns. If they differ, the user dragged the handle and we should modify the star. We shouldn't forget to convert the new position back to the star's local space before assigning it to the point's offset and updating the star.
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();
			}
		}
	}
}
point dragging in the scene view
Yes, it works! Wait, it doesn't support undo! We can't rely on 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();
			}
		}
	}
}
And with that, we're done! Have fun designing stars!

Downloads

star.unitypackage
The finished project.

Questions & Answers

What's a Mesh?
Conceptually, a Mesh is a construct used by the graphics hardware to draw complex stuff. It contains a collection of points in 3D space plus a set of triangles – the most basic 2D shapes – defined by these points. The triangles constitute the surface of whatever the mesh respresents. Often, you won't realize that you're looking at a bunch of triangles instead of a real object.
What's a class attribute?
A class attribute is like a tag you can attach to classes. You can use them to influence how Unity deals with these classes. In this case, we instruct Unity to make sure that the required components are present for any game object which our component gets added to.

Classes aren't the only things you can attach attributes to. It also works for methods, variables, and so on.

What does "typeof" do?
The typeof operator is used to get the type object of something, usually a class. You cannot use it with variables, only with explicit type names.

Why not just write down the class name? Because that results in a compiler error! The extra step is needed because you're converting a type into a variable.

What's with the weird "for" loop?
You can put multiple statements in the iterator declaration and increment parts of a "for" construct. The only weird thing is that you have to separate the statements with a comma in both places.

for(int iA = 0, iB = 0; iA < 10; iA++, iB++) { DoStuff(iA, iB); }

is the same as

int iA = 0, iB = 0; while(iA < 10) { DoStuff(iA, iB); iA++; iB++ }

What does the CGPROGRAM do?
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.

Why no fixed function shader?
Fixed function shaders belong to the past. Also, the CGPROGRAM makes it more obvious how data from Unity is transformed into screen pixels.
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.
Why no use a struct?
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.

What does "override" do?
Overriding is a mechanic which you use to replace a method from the class you are extending. You can only override methods that have been marked as virtual. In this case, it allows Unity to call any editor's OnInspectorGUI method without having to know it's exact type.

So why don't Unity event methods like Start and Update require the override keyword? Because Unity uses a different approach to make its event methods work.

So why isn't OnInspectorGUI a Unity event method? I don't know. Either something trumped consistency or it's simply legacy code.

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.
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.

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.
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.
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.

What does "break" do?
You can use the break keyword to jump – break – out of a loop. It's useful if there are complex criteria that warrant an early termination of the loop.

for(int i = 0; i < 100; i++) { if(i == 5) { break; } DoStuff(); }

is the same as

for(int i = 0; i < 5; i++) { DoStuff(); }

What does "foreach" do?
foreach is a convenient alternative for a for loop. Because it has some overhead compared to a regular for loop, I never use it in game code. But I don't have such reservations for using it in editor code if I don't need the iterator integer.

foreach(Star s in targets) { s.UpdateStar(); }

is the same as

for(int i = 0; i < targets.Length; i++) { Star s = (Star)targets[i]; s.UpdateStar(); }

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.
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.
How do we conver 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.
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.