Further extending the Unity 3D editor

In our last entry, The Horseman revealed to you an incantation to transmute Unity’s Editor itself. That entry merely skims the surface of what a practitioner of the arts may accomplish through Unity’s editor scripting capabilities. In today’s lesson, we build on top of the previous layers of sorcery such that we can overcome a pernicious problem with getters and setters in C# scripts. As before, project source for this tutorial is available.

Those of you who have spent even a small amount of time adding scripts to Unity’s GameObjects stand a fair chance of having encountered a magnificent property of the editor. A public instance variable declared as a member of a class will be exposed in the inspector for that game object, so if we were to give the PunyPlane a property called health, that property would be editable within the editor on an instance-level basis.

public float health = 1000;

Yes, I hear your questions even before you ask: “But Mr. Horseman, what about encapsulation? What if I want to hide health behind a getter / setter pair? It would be great if the PunyPlane could notify listeners of a change in health, or possibly even alter its own size such that it shrinks as its health decreases.”

Well, let’s try it out!

	protected float healthMax = 1000;
	protected float health = 1000;
 
	public float HealthMax{
		get{return healthMax;}
		set{healthMax = value;}
	}
 
	public float Health{
		get{return health;}
		set{
			health = value;
			ResizeBasedOnHealth();
		}
	}
 
 
	protected void ResizeBasedOnHealth(){
		// As health approaches 0, scale should approach 0.5, 
		// to make the object appear half its normal size
		// when health is depleted.
 
		float newScale = 0.5f;
 
		if(health > 0){
			newScale += (health / healthMax) * 0.5f;
		}
 
		gameObject.transform.localScale = new Vector3(newScale,newScale,newScale);
	}

Huh.

It appears that you can’t encapsulate a variable if you want it to be editable in the Unity Editor. Of course, looks can be deceiving and two can play at deception in this grand illusion. There are in fact several ways to get past this difficulty, and the one you choose depends on what’s most important for your purposes.

The most simple of these solutions is to set up your function to implement the PunyPlane.OnDrawGizmosSelected function, which is called approximately once every 10ms while an object is selected in the Editor.

 
// leave health exposed as public...
public float health = 1000;
 
void OnDrawGizmosSelected() {
     // and when in Editor mode, call ResizeBasedOnHealth on every update
     if(Application.isPlaying == false){
         ResizeBasedOnHealth();
     }
}

This simple solution allows GameObjects to call functions that act on the publicly exposed values in your game. Here, you can set the health of any PunyPlane in the editor and watch as its scale changes on the fly while you edit the Health field.

But Mr. Horseman, what if I want ResizeBasedOnHealth to be called at runtime whenever health is changed? This only works in the editor!

It is at this point that we dig a bit deeper, and uncover the Editor class. You will create a new C# script located in “Assets/Editor” and rename it as PunyPlaneEditor. We’ll start off with this code inside:

using UnityEngine;
using UnityEditor;
using System.Collections;
 
// Remember to declare the Type of editor this should be!
[CustomEditor (typeof(PunyPlane))]
public class PunyPlaneEditor : Editor {
 
	/* * 
	 * This function is called repeatedly as the Inspector GUI is refreshed.
	 * */	
	public override void OnInspectorGUI(){
 
		// Our "target" is the particular PunyPlane
		// instance that is currently selected, and
		// whose properties this inspector panel
		// reveals.
		PunyPlane pp = target as PunyPlane;
 
		GUILayout.Label("PunyPlane Custom Controls");
		pp.Health = EditorGUILayout.FloatField("Health",pp.Health);
 
	}	
 
}

Now, young seeker of arcane wisdom, behold what you should see :

Our custom Editor script has usurped control over Unity’s default implementation and displays for us a field containing a floating point value that we have labeled “Health”, which gets and sets from PunyPlane.Health, the getter / setter pair. The drawback to this method is that it takes work to make your custom inspector layouts look aesthetically pleasing, and if you have a large number of editable fields this can become an onerous task. This should only be undertaken in the event that you have a side effect that must be triggered both at runtime and in the editor, and you don’t want to incur the overhead of performing the operation in the normal void Update() method belonging to all MonoBehaviour objects. The other limitation of the OnInspectorGUI method is that Screen.width and Screen.height will return the pixel dimensions of the inspector panel, not the game screen or the scene. If you need the pixel dimensions of the Scene editor, you can instead perform those actions in Editor.OnSceneGUI() and also from within the OnShowGizmosSelected handler. On the other hand, if you need the precise pixel dimensions of the Game window those are not available at all from within the editor scripts… but there is a way yet to reach them, in the event that you need those dimensions at runtime and you cannot, or do not wish to hard code them. I’ll save that bit of prestidigitation for another time.

There are yet other ways to interact with and change the Editor’s behavior. Virtually anything you see happen in the editor can be harnessed for your own purposes.

Download the source for this tutorial.

Tags: , ,

  1. No comments yet.