RSS
 

Building a reusable event trigger volume for unity3d prototyping.

24 Jul

Even straight out of the box, unity is pretty great at cutting the amount of work between you and prototyping an idea. But quite often I’ll find myself writing tons of little scripts to handle really basic stuff. That’s fine for me (albeit a bit distracting), but for people who are less confident (or just less happy) with scripting it’s a lot of work between them and what they want. Primary offender… “Do X when Y enters trigger collider Z”.

So I decided to make a reusable one…

Part 1 – The Behaviour

using UnityEngine;
using System.Collections.Generic;

[AddComponentMenu("Prototyping/Triggers/EventTrigger")]
public class EventTrigger : MonoBehaviour
{
	public string				tagMask;
	public LayerMask 			layerMask = -1;
	public GameObject 			target;
	public string				firstIn;
	public string				lastOut;
	public string				enter;
	public string				exit;

	private List<GameObject> 	contains = new List<GameObject>();

	public void OnTriggerEnter(Collider other)
	{
		if(layerMask.value != -1 && ((layerMask.value & other.gameObject.layer)!=0))
			return;

		if(tagMask.Length > 0 && tagMask!=other.tag)
			return;

		contains.Add(other.gameObject);

		if(target!=null && enter.Length > 0)
			target.SendMessage(enter);

		if(target!=null && contains.Count == 1 && firstIn.Length > 0)
			target.SendMessage(firstIn);
	}

	public void OnTriggerExit(Collider other)
	{
		if(!contains.Remove(other.gameObject))
			return;

		if(target!=null && exit.Length > 0)
			target.SendMessage(exit);

		if(target!=null && contains.Count == 0 && lastOut.Length > 0)
			target.SendMessage(lastOut);
	}
}

The behaviour is pretty simple. It just implements OnTriggerEnter and OnTriggerExit and (with some filtering), calls functions by name (using SendMessage) on an object that’s specified by the user.

The trigger keeps a list of which objects are in the trigger, since I’m interested in sending events when the trigger goes to/from being empty, but it has another event hook for each time an object enters and leaves.

Part 2 – The Editor

using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;

[CustomEditor(typeof(EventTrigger))]
public class EventTriggerEditor : Editor
{
	private bool showDoc = false;
	private bool showAdv = false;

	private static string[] FindEventStrings(GameObject evtSource)
	{
		List<string> eventStrings = new List<string>();
		string[] eventNameExclusions = new string[] {"Update", "FixedUpdate",  "LateUpdate", "Start", "Awake", "OnEnable", "OnDisable"};

		MonoBehaviour[] behaviours = evtSource.GetComponents<MonoBehaviour>();
		foreach(MonoBehaviour behavior in behaviours)
		{
			System.Type behaviorType = behavior.GetType();
			MethodInfo[] methods = behaviorType.GetMethods();
			foreach(MethodInfo method in methods)
			{
				// events are currently restrected to public, zero parameter functions, that aren't constructors
				if(method.GetParameters().Length != 0 || method.IsConstructor || !method.IsPublic)
					continue;

				// return type void only
				if(method.ReturnType != typeof(void))
					continue;

				// skip all base class methods from monodevelop, component and UnityEngine.Object
				if(method.DeclaringType.IsAssignableFrom(typeof(MonoBehaviour)))
				 	continue;

				// don't allow unity callbacks to be used as events (Update, Awake, etc)
				if(System.Array.IndexOf(eventNameExclusions, method.Name)!=-1)
					continue;

				// don't have duplicates in the list
				if(eventStrings.Contains(method.Name))
					continue;

				eventStrings.Add(method.Name);
			}
		}
		eventStrings.Add("--NONE--");
		return eventStrings.ToArray();
	}

	public override void OnInspectorGUI()
	{
		OnInspectorGUI_Documentation ();
		OnInspectorGUI_Settings();
		OnInspectorGUI_Advanced ();
	}

	public void OnInspectorGUI_Settings()
	{
			EventTrigger 	trigger = (EventTrigger)target;
			GameObject 		evtSource = trigger.target;

			string[] strings = evtSource!=null ? FindEventStrings(evtSource):new string[] {"No object selected"};

			trigger.target = (GameObject)EditorGUILayout.ObjectField("Target Object", (Object)trigger.target, typeof(GameObject));

			trigger.tagMask = EditorGUILayout.TagField("Activator Tag", trigger.tagMask);
			if(trigger.tagMask == "Untagged")
				trigger.tagMask = "";

			trigger.enter 	= StringPopup("Enter Event", strings, trigger.enter);

			trigger.exit 	= StringPopup("Exit Event", strings, trigger.exit);

			trigger.firstIn = StringPopup("First In Event", strings, trigger.firstIn);

			trigger.lastOut = StringPopup("Last Out Event", strings, trigger.lastOut);
	}

	private void OnInspectorGUI_Documentation ()
	{
		showDoc = EditorGUILayout.Foldout(showDoc, "Documentation");
		if(showDoc)
		{
			EditorGUILayout.BeginHorizontal();
			EditorGUILayout.Space();
			EditorGUILayout.BeginVertical();

			DocumentationBox("Description", 	"An EventTrigger is used to call script events when an object enters or leaves a trigger collider.");
			DocumentationBox("Target Object", 	"The object that the events will be directed to. Events can only be selected from the script functions this object has defined");
			DocumentationBox("Activator Tag", 	"If set to Untagged (or blank), any object will fire the trigger. If a tag is set, then only objects with that tag will fire the trigger.");
			DocumentationBox("Enter Event", 	"The event fired each time an object enters the trigger.");
			DocumentationBox("Exit Event", 		"The event fired each time an object enters the trigger.");
			DocumentationBox("First In Event", 	"The event fired when an object enters the trigger, and the trigger previously was empty.");
			DocumentationBox("Last Out Event", 	"The event fired when the tigger goes from having objects inside to being empty.");

			EditorGUILayout.EndVertical();
			EditorGUILayout.EndHorizontal();
		}
	}

	public void OnInspectorGUI_Advanced ()
	{
		showAdv = EditorGUILayout.Foldout(showAdv, "Advanced");
		if(showAdv)
			DrawDefaultInspector();
	}

	public static void DocumentationBox (string label, string boxText)
	{
		EditorGUILayout.BeginHorizontal();

		EditorGUILayout.PrefixLabel(label, "box");
		GUILayout.Box(boxText, "box");

		EditorGUILayout.EndHorizontal();
	}

	private static string StringPopup (string label, string[] strings, string str)
	{
		int strId = System.Array.IndexOf(strings,str);

		if(strId==-1)
			strId = strings.Length-1;

		strId = EditorGUILayout.Popup(label, strId, strings, "popup");

		if(strId==-1 || strId==strings.Length-1)
			return "";

		return strings[strId];
	}
}

This part is a bit less simple. The flaw in the behaviour so far is that with the default inspector, the user has to remember and then manually enter the correct name of a function that the target object will respond to.

The default inspector for the trigger

There’s a lot of ways this can go wrong; the user can misspell the name of a method, not know what methods are available, etc.  The purpose of the editor should be to present the user with a quick selection of valid parameters only.

The custom trigger editor inspector

The custom editor replaces the text fields for the event strings with drop down menus of valid method names for the selected object. The function names are found by grabbing the target object’s behaviors and finding any and all suitable methods using c# reflection.

Finally we built some fold out documentation into the editor. Why? Because 2 years from now, even I might not remember exactly how this thing worked and it saves on time having to explain to others how to use the feature.

Part 3 – Further thoughts

The trigger is of course, far from perfect. The custom editor looks up c# reflection to find function names for the drop-down boxes which can’t inspect behaviors in the other languages that unity supports. The editor is incompatible with unity iphone (excluding the version 3.0 preview of course).  The code also hasn’t seen much use at all yet, so it wouldn’t surprise me if there were a few bugs I hadn’t run into yet.

Moving forward I’m undecided if it’s worth adding parameter passing into the events. Certainly it adds a lot of flexibility, though that comes at the cost of making this intentionally simple component into a complex one. Still, as a prototyping tool I’m quite happy with it already.

 
 

Leave a Reply

 

 
  1. laurent

    November 28, 2010 at 6:14 am

    There is no link to your custom inspector.

    Did you use reflection to find the list of methods?

     
  2. Cratesmith

    November 29, 2010 at 2:49 pm

    Yes, it uses reflection to find method names. Too much can go wrong if people need to write in method names themselves.

    The source code is on the page, just click “show source” in the second code snippet to see it.

     
 
Performance Optimization WordPress Plugins by W3 EDGE