- Home /
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,
http://docs.unity3d.com/Documentation/ScriptReference/PropertyDrawer.html
PropertyDrawers let you define what a type looks like in the inspector; if you create a wrapper "DelegateField" class and give it a custom drawer you won't have to write an editor for each class. (I don't have any words of wisdom on drawers other than I couldn't make them generic enough for me so i ended up not using them at all x))
There was a great in depth answer here a second ago.. Why is it gone?
Answer by sotirosn · Jun 16, 2013 at 11:46 AM
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.
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 ("\treturn type:" + returnType.ToString());
Debug.Log ("\tparam count:" + paramTypes.Length);
foreach(System.Type paramType in paramTypes)
Debug.Log("\t\t" + 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[i] = 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<Collider> { }
[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 { }
you can also set an action in script like this,
GetComponent<Trigger>().enters.action =(collider)=> {
Debug.Log ("enters " + collider);
};
hi, i have tried to simply put your examples in a unity project and I get compilation errors. For example, in declaring
public System.Action action;
it complains that T is not defined, probably a directive is missing.
could you simply post the cs files? this would be very useful to understand better how to deal with delegates in custom editor. best, Joan
Sorry the template declaration '<T>' was being interpreted as markup or something, but its fixed now. Let me know if you still get compile errors.
Answer by stepan-stulov · Aug 03, 2015 at 11:32 AM
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):
https://www.youtube.com/watch?v=tZENtGYccGI
public OrcWasCreatedEvent OrcWasCreated; // public class OrcWasCreatedEvent : UnityEvent<Orc>
public UnityEvent AllOrcsWereDestroyed;
// later
OrcWasCreated.Invoke(myOrc);
AllOrcsWereDestroyed.Invoke();
Answer by vexe · May 19, 2014 at 09:19 AM
See this answer to see how you could basically serialize delegates. See uFAction for a full already-made solution for serializable and inspectable delegates.
Your answer
Follow this Question
Related Questions
Error when trying to Serialize a field that is in a class 0 Answers
Serializable attribute defaulting on play 1 Answer
Help with editor serialization 1 Answer
Serializable class with interactivity 0 Answers
Public fields not showing in inspector 2 Answers