Posts Tagged ‘Extending Unity’

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: , ,

No Comments


Extending the Unity 3D Editor

The Horseman has found that one of the most interesting aspects of Unity 3D is that one is able to extend the authoring tool, such that one can add new functionality, menus, and behaviors to optimize workflow.

Those of you who remember a certain entry about using JSFL to automate build processes in the Flash environment may already have an idea of what I’m talking about, but extending the Unity editor is actually much easier because the same code used to write and script object behavior at runtime in the player can also be used inside the editor itself. This would be as though you were able to write extentions for the Flash authoring tool using ActionScript 3, instead of JSFL. That would be simply huge.

Fortunately, getting started with extending the Unity Editor is relatively straightforward, but unfortunately there is scant documentation on what is possible, and the samples on Unity’s website currently feature only Unity’s JavaScript and not C#. Fortunately for you, the Horseman can shed at least a little light on the subject, so follow along with this tutorial. For those who want the final source project right away, you can download the source here. I am currently using Version 3.3.0f4 of Unity on Windows, and have also tested this on Mac OSX.

First, open the Unity editor and in your “Project” panel create the following folder structure:

 
- Assets
  + Editor
 
- Scenes
 
- Scripts

Create a folder called Assets, with a subfolder called Editor. This is where our editor scripts must live in order for Unity to use them. Scenes is where we will save our Unity Scene, and Scripts is where we will keep our GameObject classes.

First, we need to create our Scene. Simply select File -> Save Scene As… and save the current scene with the name “MyScene”. You will now see “MyScene” in the project panel, so move it into the Scenes folder now.

Next, we’re going to start by creating a GameObject script. For the sake of argument, let’s say we want to write a C# script that, when called, will instantiate a GameObject that represents a simple four-vertex plane. We’ll call it “PunyPlane” for now, so in your “Scripts” folder, right-click and choose Create -> C Sharp Script. Rename this script to be named “PunyPlane”, then replace the code inside with the following, and save:

 
/* *
 * 
 *  This class creates a small, 4 vertex plane.  
 *  It's provided purely for instructional purposes,
 *  as an example of how one can instantiate custom 
 *  GameObjects with graphical representations.
 * 
 *  It is not necessary to understand how this code
 *  works, but feel free to study it if you wish
 *  to get a small taste of how polygons and planes
 *  are created procedurally in Unity.
 * 
 * */
 
 
 
using UnityEngine;
using System.Collections;
 
public class PunyPlane : MonoBehaviour {
 
	public static Material sharedMaterial;
 
	/* *
	 * Call Create without parameters to return a PunyPlane of 1 x 1 world units.
	 * */
	public static PunyPlane Create(){
		return Create(1.0f,1.0f);
	}
 
 
	/* *
	 * Call Create with a height and width expressed in world units.
	 * */
	public static PunyPlane Create(float width, float height){
 
		/* *
		 * We start by creating a GameObject to represent our plane,
		 * giving it the requisite components in order to accomplish
		 * this goal, a <MeshFilter> and a <MeshRenderer>.
		 * */
		GameObject go = new GameObject();
		go.name = "PunyPlane";
		MeshFilter mf = go.AddComponent<MeshFilter>();
		MeshRenderer mr = go.AddComponent<MeshRenderer>();
 
		/* *
		 * Now it's time to create our PunyPlane component.  
		 * */
		PunyPlane pp;		
		pp = go.AddComponent<PunyPlane>() as PunyPlane;
 
		if(mf.sharedMesh == null){
			mf.sharedMesh = new Mesh();
		}
 
		Mesh mesh = mf.sharedMesh;
 
 
		/*	
			Here is a diagram of our plane,
			with the verts labeled. 
		 0    3
		  ----
		  | /|
		  |/ |
		  ---- 
		 1    2		 
 
		 */
 
		Vector3 p0 = new Vector3(-width * 0.5f, -height * 0.5f,0);
		Vector3 p1 = new Vector3(-width * 0.5f, height * 0.5f,0);
		Vector3 p2 = new Vector3(width * 0.5f, height * 0.5f,0);
		Vector3 p3 = new Vector3(width * 0.5f, -height * 0.5f,0);
 
		/* *
		 * Make sure the Mesh is cleared of all old data, then 
		 * apply the new verts and triangles in order to form
		 * a square plane.
		 * */
		mesh.Clear();
		mesh.vertices = new Vector3[]{p0,p1,p2,p3};
		mesh.triangles = new int[]{
			0,1,3,
			3,1,2			
		};
 
		/* *
		 * And we'll want to set up the uv coordinates to match
		 * the verts listed above.  This is so the plane can wear
		 * a texture without it appearing mangled, flipped, or 
		 * inverted.
		 * */
		mesh.uv = new Vector2[]{
			new Vector2(0,0),
			new Vector2(0,1),
			new Vector2(1,1),
			new Vector2(1,0)
		};
		mesh.RecalculateNormals();
		mesh.RecalculateBounds();
		mesh.Optimize();
 
 
		/* *
		 * Using "Unlit/Texture" because of the assumption that 
		 * this will be a flat, 2D sprite and will not need to 
		 * be affected by things like lighting.  This will make
		 * the plane render more efficiently.
		 * */
		if(sharedMaterial == null)sharedMaterial = new Material(Shader.Find("Unlit/Texture"));
		mr.sharedMaterial = sharedMaterial;
		mr.sharedMaterial.shader = Shader.Find("Unlit/Texture");
 
		return pp;
	}
 
 
}

The above script allows us to dynamically generate a plane in 3D space by calling PunyPlane.Create(), for a 1×1 plane, or PunyPlane.Create(width,height) for a variable sized plane. Note that this code does not correct for negative width / height values.

To test this code, let’s give it somewhere to run. We’re going to create a “GameManager” object in our heirarchy, that will contain a “GameManager.cs” component that kicks off and runs our game logic. In this case, it won’t be very exciting logic. It will merely instantiate some of our PunyPlane objects. Go to the toolbar and select GameObject -> Create Empty. This will create a new, empty GameObject in your Heirarchy. Rename this GameObject as “GameManager”. You’ll also create another empty GameObject, and rename it as “World.”

Next, right-click your Scripts folder, and create a new C# script. Rename it as “GameManager”. Next, select the GameManager object in your heirarchy, and then drag the GameManager script over to the “Inspector” panel for the GameManager object (The “Inspector” is the panel where you can set the object’s Transform positions in world space.)

Now, we’re going to open the GameManager.cs script file and write the following code:

/* *
 * 
 * The GameManager kicks off all of our game logic.  In this case, there isn't much logic.
 * We simply instantiate a few PunyPlane objects and place them in the heirarchy.
 * Assuming that this code has been added as a component to one of the GameObjects in the
 * Heirarchy, this should take effect when you press "Play" or when you view the compiled
 * application in the Unity player.
 * 
 * */
 
using UnityEngine;
using System.Collections;
 
public class GameManager : MonoBehaviour {
 
 
 
	void Start () {
 
 
 
		// This creates a PunyPlane instance that lives on the 
		// top level of the heirarchy.
		PunyPlane punyPlane = PunyPlane.Create();
 
		// Let's add a 0 to this instance's name 
		// in order to differentiate it from its kin.
		punyPlane.name = punyPlane.name + "0";
 
 
		// Now let's make a new PunyPlane, giving it
		// an uneven rectangular shape, and give it
		// a distinctive name.
		punyPlane = PunyPlane.Create(3.0f, 0.5f);
		punyPlane.name = punyPlane.name + "1";		
 
		// Add the new PunyPlane to the World.  This removes it
		// from the top level of the heirarchy.  First, find the 
		// "World", then add the plane's transform as a child
		// to the world's transform.
		GameObject world = GameObject.Find("World");
		punyPlane.gameObject.transform.parent = world.transform;
 
 
		// Finally, we offset this new PunyPlane
		// just a bit, so we can see them both on the screen.
		punyPlane.transform.position = new Vector3(0,1.5f,0);
 
 
	}
 
 
}

Now save the script file. All this Manager script does is instantiate two PunyPlane object in our game, dynamically at runtime. Press the Play button to preview the game. If you have performed the steps correctly, you should see two planes appear in the Game window. Once you stop the game, these planes will disappear, as they only exist at runtime.

I already know what you’re thinking. But Mr. Horseman! What does any of this have to do with extending the Unity editor? All we’re doing is creating planes at runtime!

Little do you realize dear reader, that is in fact the magic of what we’re about to do next! It’s nice to be able to generate dynamic planes at runtime, but wouldn’t it be even nicer if we could just treat this plane like a Unity primitive and instantiate it inside the Editor instead of just in the player at runtime? That way we don’t have to rely on the bloated 121 vert / 200 triangle plane that Unity supplies as a primitive. Well, get ready to blow your own mind. Inside the “Assets/Editor” folder (specifically inside Editor), right-click and create a new C# file. Rename it as “CustomToolBar”, then open the file in your text editor and replace it with the following code, then save:

/* *
 * CustomToolBar is used to add some new functionality to the Unity3D Editor.  
 * In this case we're going to add some menu items to the top level toolbar.
 * */
 
using UnityEngine;
using UnityEditor;
using System.Collections;
 
public class CustomToolbar : MonoBehaviour {
 
 
 
 
	/* *
	 * 
	 * The following two methods are added to the Unity default toolbar,
	 * as sub-menus under the GameObject dropdown.  It should appear at 
	 * the very bottom of the GameObject dropdown list.
	 * 
	 * GameObject ->
	 * 		Primitives (We made this one) ->
	 * 			Create XXX
	 * 
	 * */
	[MenuItem ("GameObject/Primitives/Create PunyPlane in Heirarchy")]
	public static void MakePunyPlane(){
 
		// This function simply creates a PunyPlane in the top level of the
		// Heirarchy.
		PunyPlane.Create();
 
	}
 
	[MenuItem ("GameObject/Primitives/Create PunyPlane As Child of Selection")]
	public static void MakePunyPlaneAsChildOfSelection(){
 
		// We start by creating a PunyPlane in the top level of the 
		// Heirarchy.
		PunyPlane pp = PunyPlane.Create();
 
		// And if we've got a GameObject currently selected in the 
		// heirarchy, we'll reparent the PunyPlane to our selected
		// object.
		if(Selection.activeGameObject){
			pp.gameObject.transform.parent = Selection.activeGameObject.transform;
		}
 
	}
 
 
	/* *
	 * And finally, we create a brand new menu item in the Unity editor.
	 * This will actually appear as a new top-level item called "Scriptocalypse"
	 * with a command "Say Hello".  The "Scriptocalypse" item should appear 
	 * between "Terrain" and "Window".
	 * 
	 *   Scriptocalypse ->
	 * 		Say Hello
	 * 
	 * */
	[MenuItem ("Scriptocalypse/Say Hello")]
	public static void SayHello(){
		Debug.Log("Hello from the Unity3D toolbar.");	
	}
 
 
 
 
}

As a reference, your project’s file and asset structure should look like this:

Assuming there are no errors in any of the above scripts, when you click back onto the Unity Editor you should immediately notice that the toolbar at the top has a new entry between “Terrain” and “Window” that was never there before. It should be called “Scriptocalypse”, and inside is a menu with a single option, “Say Hello”. Clicking this option sends a message to the Debugger, as you can see.

But Mr. Horseman, what about those functions that let you create planes in the editor? Where are they?

Aaah, notice in the code sample above that we have some braces that contain some interesting code? Look at the very first function and its brace block:

[MenuItem (“GameObject/Primitives/Create PunyPlane in Heirarchy”)]

That string represents the path to the menu item we’ve created, so as you can see we’ve simply added a new option to the pre-existing GameObject menu item. At the bottom, you should find “Primitives”, which has two functions. The first of them simply adds a new PunyPlane to the Heirarchy. The second will go a step further, and add the PunyPlane to whichever game object is currently selected in the Heirarchy. Go ahead, and try them out.

Do you see the magic here? The same code we use to create and manipulate behaviors at runtime is also the code we use to instantiate scripts in the editor! Such sorcery is simply not known in the lands of Flash, which makes it all the more exciting to actually see in action here.

You’ll notice that the PunyPlane instances we create actually have the PunyPlane component attached to the GameObject. This is by design. We currently have no special behaviors or public properties attached to the PunyPlane component, but we may soon in another posting. We’ll find that public properties are exposed in the inspector, and can be set both at runtime and in the editor… but what about public getter/setter pairs? Aaaah, in C# those are not exposable in the inspector… at least not without a bit of trickery! The Horseman will leave you pondering just what manner of trickery until then, but suffice it to say there are a few ways.

In the meantime, Download the Unity project and source files for this tutorial if you need any assistance.

Tags: , ,

5 Comments