Can you assign serializable delegates in the inspector?

Hopefully I am missing really simple but I am trying to do this.

[Serializable]
public delegate IEnumerator Routine();

class Example : MonoBehaviour {
  public Routine start;

  public IEnumerator StartOne() {} 
  public IEnumerator StartTwo() {}

  IEnumerator Start() {
    yield return StartCoroutine(start());
  }
}

And in the inspector I would like there to be a ‘start’ field with a drop down of [“StartOne”, “StartTwo”]. I can do this if I make a custom editor for my class Example and use reflection to scoop up the matching methods, but I don’t want a custom inspector for everyone that stores a Routine publicly. i.e:

[CustomEditor(typeof(Example))]
class ExampleEditor : Unity.Editor {
  override OnInpsectorGUI() {
      // collect method names = Example.GetMethods() :filter signatures
      // select index from popup(methods)
      // example.start = methods[selected index];

      // or
       // select example.index from popup(methods)
      // and just set the field on Awake from the index
  }
}

actually I tried this and it works but it breaks as soon as I upgrade my delegate to a generic

[Serializable]
public delegate IEnumerator Routine<T>(T parameters); // compiles but stops the inspector form saving the below..

public class Example {
    public Routine<int> start;

    IEnumerator Start() {
        yield return StartCoroutine(start(5));
    }

    public IEnumerator MaybeStartThis(int value) {
      ...
    }
}

The editor finds the methods and assigns them but they are cleared as soon as I hit play? I don’t know. I could save each method name as an index and assign it at runtime with reflection but this sounds cumbersome. Another problem is that I seem to have to make a custom editor for every singe class that has a delegate field!

If anyone can help me I would be much obliged.

Thx,

Unity has serialisable delegates from the box.

Ok, Unity has (semi-) serialisable delegates from the box. It’s not directly a serialisation of delegates, rather of observers, a notifier+observer pattern. Unity’s UnityEvent, that’s used for Inspector-driven UI event listening (when you hang a game object and its specific method to call on a button’s Click event e.g.) is serialised multicast delegates. It’s slow (there was an article somewhere comparing native C# delegates and Unity’s delegates and it was magnitude 3 difference, but I can’t seem to find it). But it works. You can also declare your own events/delegates as either public or [SerilizeField] private fields of type UnityEvent (or one of it’s generic variations, up to 4 arguments, but in this case you’d need to extend the generic class, as Unity can’t serialise generics) and they become inspector-injectable. These events have API for adding and removing listeners (and obviously invoking) from script. But script-added/removed listeners are non persistent. So happy mouse-programming:)

This feature enable developers to expose events for hooking up listeners to non-programmer folks. I’d say quiet a niche tool for event-driven visual level editing. I can imagine creating a set of actor game objects and a “protocol” of event cross-linking and vuala, a game designer can create little sandbox simulations with no coding help. Correct me if I’m wrong, this is how the GameMaker works in their visual scripting tool.

That having been said, I think you should keep programmer-for-programmer delegates in pure C# and unexposed for inspector.

The biggest problem of these serialised events is that they remain completely unadvertised outside of new UI system by Unity, although they’re completely UI agnostic.

Here is a little video, where a guy uses these events (without the UI system):

public OrcWasCreatedEvent OrcWasCreated; // public class OrcWasCreatedEvent : UnityEvent public UnityEvent AllOrcsWereDestroyed;
// later

OrcWasCreated.Invoke(myOrc);
AllOrcsWereDestroyed.Invoke();

2 days of searching and this is my best solution so far. It has some kinks like having to make an editor file for each delegate signature (see Editor/TriggerActionDrawer.cs), and it does not automatically repopulate candidate matches after compiling (must manually refresh), and you have to manually initialize fields on Awake(), but other than that it works great.

[12052-screen+shot+2013-06-16+at+5.36.58+am.png*|12052]

Action.cs:

[System.Serializable]
public class Action<T> {
	// public settings
	public Object target;
	public string method;
	
	// inspector cache
	public string[] candidates = {};
	public int index;
	
	// invocation
	public System.Action<T> action;
	
	public void Awake() {
		action = System.Action<T>.CreateDelegate(typeof(System.Action<T>), target, target.GetType().GetMethod(method)) as System.Action<T>;
	}
}

public class ActionAttribute : PropertyAttribute {
	public System.Type returnType;
	public System.Type[] paramTypes;
	public ActionAttribute(System.Type returnType = null, params System.Type[] paramTypes) {
		this.returnType = returnType != null ? returnType : typeof(void);
		this.paramTypes = paramTypes != null ? paramTypes : new System.Type[0];
	}
	
	public System.Delegate method;
}

Editor/ActionDrawer.cs:

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

[CustomPropertyDrawer(typeof(Action))]
public class ActionDrawer : PropertyDrawer {
	const float rows = 3;
	
	public override void OnGUI (Rect pos, SerializedProperty properties, GUIContent label) {
		//Debug.Log(((attribute as ActionAttribute).paramTypes.Length).ToString());
		
		SerializedProperty targetProperty = properties.FindPropertyRelative("target");
		SerializedProperty methodNameProperty = properties.FindPropertyRelative("method"); 
		SerializedProperty candidateNamesProperty = properties.FindPropertyRelative("candidates");
		SerializedProperty indexProperty = properties.FindPropertyRelative("index");
		
		// pass through label
		EditorGUIUtility.LookLikeControls();
		EditorGUI.LabelField(
			new Rect (pos.x, pos.y, pos.width/2, pos.height/rows),
			label
		);
		
		// target + method section
		EditorGUI.indentLevel++;
		EditorGUI.BeginChangeCheck(); // if target changes we need to repopulate the candidate method lists
		
		// select target
		EditorGUI.PropertyField(
			new Rect (pos.x, pos.y += pos.height/rows, pos.width, pos.height/rows),
			targetProperty
		);
		if(targetProperty.objectReferenceValue == null) {
			return; // null objects have no methods - don't continue
		} 
		
		// polulate method candidate names
		string[] methodCandidateNames;
		if(EditorGUI.EndChangeCheck()) {
			// lets do some reflection work -> search, filter, collect candidate methods..
			methodCandidateNames = RepopulateCandidateList(targetProperty, candidateNamesProperty, indexProperty);
		}
		else {
			methodCandidateNames = new string [candidateNamesProperty.arraySize]; 
			
			int i = 0;
			foreach(SerializedProperty element in candidateNamesProperty) {
				methodCandidateNames[i++] = element.stringValue;
			}
		}
		
		// place holder when no candidates are available
		if(methodCandidateNames.Length == 0) {
			EditorGUI.LabelField (
				new Rect (pos.x, pos.y += pos.height/rows, pos.width, pos.height/rows),
				"Method",
				"none"
			);	
			return; // no names no game
		}
		
		// select method from candidates
		indexProperty.intValue = EditorGUI.Popup (
			new Rect (pos.x, pos.y += pos.height/rows, pos.width, pos.height/rows),
			"Method (" + targetProperty.objectReferenceValue.GetType().ToString() + ")",
			indexProperty.intValue,
			methodCandidateNames
		);
		
		methodNameProperty.stringValue = methodCandidateNames[indexProperty.intValue];
		EditorGUI.indentLevel--;
	}	

	public string[] RepopulateCandidateList(
			SerializedProperty targetProperty, 
			SerializedProperty candidateNamesProperty,
			SerializedProperty indexProperty
	) {
		System.Type type = targetProperty.objectReferenceValue.GetType();
		System.Type[] paramTypes = this.paramTypes;
		IList<MemberInfo> candidateList = new List<MemberInfo>();
		string[] candidateNames;
		int i = 0; 
		
		Debug.Log ("Candidate Criteria:");
		Debug.Log ("	return type:" + returnType.ToString());
		Debug.Log ("	param count:" + paramTypes.Length);	
		foreach(System.Type paramType in paramTypes)
			Debug.Log("		" + paramType.ToString());
		
		type.FindMembers(
			MemberTypes.Method,
			BindingFlags.Instance | BindingFlags.Public,
			(member, criteria) => {
				Debug.Log("matching " + member.Name);
				MethodInfo method;
				if((method = type.GetMethod(member.Name, paramTypes)) != null && method.ReturnType == returnType) {
					candidateList.Add(method);
					return true;
				}
				return false;
			},
			null
		);
		
		// clear/resize/initialize storage containers
		candidateNamesProperty.ClearArray();
		candidateNamesProperty.arraySize = candidateList.Count;
		candidateNames = new string[candidateList.Count];
		
		// assign storage containers
		i = 0;
		foreach(SerializedProperty element in candidateNamesProperty) {
			element.stringValue = candidateNames *= candidateList[i++].Name;*
  •  }*
    
  •  // reset popup index*
    
  •  indexProperty.intValue = 0;*
    
  •  return candidateNames;*
    
  • }*

  • public System.Type returnType {*

  •  get { return attribute != null ? (attribute as ActionAttribute).returnType : typeof(void); }*
    
  • }*

  • public System.Type paramTypes {*

  •  get {* 
    
  •  	return (attribute != null && (attribute as ActionAttribute).paramTypes != null) ? (attribute as ActionAttribute).paramTypes : new System.Type[0];*
    
  •  }*
    
  • }*

  • public System.Delegate method {*

  •  get { return attribute != null ? (attribute as ActionAttribute).method : null; }*
    
  •  set { (attribute as ActionAttribute).method = value; }*
    
  • }*

  • public override float GetPropertyHeight (SerializedProperty property, GUIContent label) {*
    _ return base.GetPropertyHeight (property, label) * rows;_

  • }*
    }
    Trigger.cs:

// example consumer
using UnityEngine;

public class Trigger : MonoBehaviour {

  • // must derive a new class because templates and inheritance don’t seem to trigger the custom drawer*

  • [System.Serializable]*

  • public class Action : Action { }*

  • [Action(typeof(void), typeof(Collider))]*

  • public Action enters, stays, exits;*

  • public void OnTriggerEnter(Collider collider) {*

  •  Debug.Log ("here");*
    
  •  enters.action(collider);*
    
  • }*

  • public void OnTriggerStays(Collider collider) {*

  •  stays.action(collider);*
    
  • }*

  • public void OnTriggerExit(Collider collider) {*

  •  exits.action(collider);*
    
  • }*

  • public void Awake() {*

  •  enters.Awake();*
    
  •  stays.Awake();*
    
  •  exits.Awake();*
    
  • }*
    }
    Example.cs:
    // example provider
    using UnityEngine;

public class Example : MonoBehaviour {

  • public void Hello(Collider collider) {*

  •  Debug.Log ("Hello " + collider + "!");*
    
  • }*

  • public void Goodbye(Collider collider) {*

  •  Debug.Log ("bye " + collider);*
    
  • }*
    }
    Editor/TriggerActionDrawer.cs:
    using UnityEditor;

[CustomPropertyDrawer(typeof(Trigger.Action))]
public class TriggerActionDrawer : ActionDrawer { }
*

See this answer to see how you could basically serialize delegates. See uFAction for a full already-made solution for serializable and inspectable delegates.