【Unity】ボタンのOnClickの引数にenumを使えるようにする(2020対応)

2022-01-08

インスペクタでボタンの OnClick イベントの引数に enum の値も指定できるように修正した。

動作環境: Unity 2021.1.6f1

概要

UnityのUIボタンのOnClickイベントの引数として enum の値を指定したいときがあるが標準だと int 型の1引数メソッドしか列挙されないため、 インスペクタから整数値で指定するしかない。 使えるようにするにはどうするか。

ググって出てきた ほっぽこ開発: UnityのButtonのOnClickに列挙型の引数を与える を見ると、 “Ability to add enum argument to button functions” への回答 でできるとのこと。 しかし試したところUnityのバージョンアップによりエラーが出てしまっていた:

NullReferenceException: Object reference not set to an instance of an object
UnityEventDrawer.GetDummyEvent (UnityEditor.SerializedProperty prop) (at Assets/EnumAction/Editor/UnityEventDrawer.cs:311)
UnityEventDrawer.OnGUI (UnityEngine.Rect position) (at Assets/EnumAction/Editor/UnityEventDrawer.cs:97)
UnityEventDrawer.OnGUI (UnityEngine.Rect position, UnityEditor.SerializedProperty property, UnityEngine.GUIContent label) (at Assets/EnumAction/Editor/UnityEventDrawer.cs:80)
UnityEditor.PropertyDrawer.OnGUISafe (UnityEngine.Rect position, UnityEditor.SerializedProperty property, UnityEngine.GUIContent label) (at <9b8dec54b88e4aa9b9661a30f7e3f61e>:0)
UnityEditor.PropertyHandler.OnGUI (UnityEngine.Rect position, UnityEditor.SerializedProperty property, UnityEngine.GUIContent label, System.Boolean includeChildren, UnityEngine.Rect visibleArea) (at <9b8dec54b88e4aa9b9661a30f7e3f61e>:0)
UnityEditor.PropertyHandler.OnGUI (UnityEngine.Rect position, UnityEditor.SerializedProperty property, UnityEngine.GUIContent label, System.Boolean includeChildren) (at <9b8dec54b88e4aa9b9661a30f7e3f61e>:0)
UnityEditor.PropertyHandler.OnGUILayout (UnityEditor.SerializedProperty property, UnityEngine.GUIContent label, System.Boolean includeChildren, UnityEngine.GUILayoutOption[] options) (at <9b8dec54b88e4aa9b9661a30f7e3f61e>:0)
UnityEditor.EditorGUILayout.PropertyField (UnityEditor.SerializedProperty property, UnityEngine.GUIContent label, System.Boolean includeChildren, UnityEngine.GUILayoutOption[] options) (at <9b8dec54b88e4aa9b9661a30f7e3f61e>:0)
UnityEditor.EditorGUILayout.PropertyField (UnityEditor.SerializedProperty property, UnityEngine.GUILayoutOption[] options) (at <9b8dec54b88e4aa9b9661a30f7e3f61e>:0)
UnityEditor.UI.ButtonEditor.OnInspectorGUI () (at Library/PackageCache/com.unity.ugui@1.0.0/Editor/UI/ButtonEditor.cs:27)
UnityEditor.UIElements.InspectorElement+<>c__DisplayClass59_0.<CreateIMGUIInspectorFromEditor>b__0 () (at <47d44085adbe43bd896d97295bdef4f5>:0)
UnityEngine.GUIUtility:ProcessEvent(Int32, IntPtr, Boolean&)

UnityEventDrawer.cs でのイベントに対するアクセス方法が変わってしまっているぽい。

FindMethod などでググっていると EasyEventEditorというソースがヒットしたのでそれを元に修正してみた。

差分

UnityEventDrawer.cs の差分は以下の通り (ソース全体はgistに置いた):

@@ -13,7 +13,18 @@
{
private Dictionary<string, State> m_States = new Dictionary<string, State>();
// Find internal methods with reflection
- private static MethodInfo findMethod = typeof(UnityEventBase).GetMethod("FindMethod", BindingFlags.NonPublic | BindingFlags.Instance, null, CallingConventions.Standard, new Type[] { typeof(string), typeof(object), typeof(PersistentListenerMode), typeof(Type) }, null);
+ private static MethodInfo findMethod = typeof(UnityEventBase).GetMethod(
+ "FindMethod", BindingFlags.NonPublic | BindingFlags.Instance, null, CallingConventions.Standard,
+ new Type[] {
+ typeof(string),
+#if UNITY_2020_1_OR_NEWER
+ typeof(Type),
+#else
+ typeof(object),
+#endif
+ typeof(PersistentListenerMode),
+ typeof(Type),
+ }, null);
private static MethodInfo temp = typeof(GUIContent).GetMethod("Temp", BindingFlags.NonPublic | BindingFlags.Static, null, CallingConventions.Standard, new Type[] { typeof(string) }, null);
private static PropertyInfo mixedValueContent = typeof(EditorGUI).GetProperty("mixedValueContent", BindingFlags.NonPublic | BindingFlags.Static);
private Styles m_Styles;
@@ -26,7 +37,14 @@

private static string GetEventParams(UnityEventBase evt)
{
- var method = (MethodInfo)findMethod.Invoke(evt, new object[] { "Invoke", evt, PersistentListenerMode.EventDefined, null });
+ var method = (MethodInfo)findMethod.Invoke(evt, new object[] {
+ "Invoke",
+#if UNITY_2020_1_OR_NEWER
+ evt.GetType(),
+#else
+ evt,
+#endif
+ PersistentListenerMode.EventDefined, null });
var stringBuilder = new StringBuilder();
stringBuilder.Append(" (");
var array = ((IEnumerable<ParameterInfo>)method.GetParameters()).Select(x => x.ParameterType).ToArray();
@@ -306,6 +324,67 @@
m_LastSelectedIndex = list.index;
}

+#if UNITY_2020_1_OR_NEWER
+ private static UnityEventBase GetDummyEventStep(string propertyPath, System.Type propertyType, BindingFlags bindingFlags)
+ {
+ UnityEventBase dummyEvent = null;
+
+ while (propertyPath.Length > 0)
+ {
+ if (propertyPath.StartsWith("."))
+ propertyPath = propertyPath.Substring(1);
+
+ string[] splitPath = propertyPath.Split(new char[] { '.' }, 2);
+
+ FieldInfo newField = propertyType.GetField(splitPath[0], bindingFlags);
+
+ if (newField == null)
+ break;
+
+ propertyType = newField.FieldType;
+ if (propertyType.IsArray)
+ {
+ propertyType = propertyType.GetElementType();
+ }
+ else if (propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof(List<>))
+ {
+ propertyType = propertyType.GetGenericArguments()[0];
+ }
+
+ if (splitPath.Length == 1)
+ break;
+
+ propertyPath = splitPath[1];
+ if (propertyPath.StartsWith("Array.data["))
+ propertyPath = propertyPath.Split(new char[] { ']' }, 2)[1];
+ }
+
+ if (propertyType.IsSubclassOf(typeof(UnityEventBase)))
+ dummyEvent = System.Activator.CreateInstance(propertyType) as UnityEventBase;
+
+ return dummyEvent;
+ }
+
+ private static UnityEventBase GetDummyEvent(SerializedProperty property)
+ {
+ UnityEngine.Object targetObject = property.serializedObject.targetObject;
+ if (targetObject == null)
+ return new UnityEvent();
+
+ UnityEventBase dummyEvent = null;
+ Type targetType = targetObject.GetType();
+ BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
+
+ do
+ {
+ dummyEvent = GetDummyEventStep(property.propertyPath, targetType, bindingFlags);
+ bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic;
+ targetType = targetType.BaseType;
+ } while (dummyEvent == null && targetType != null);
+
+ return dummyEvent ?? new UnityEvent();
+ }
+#else
private static UnityEventBase GetDummyEvent(SerializedProperty prop)
{
var type = Type.GetType(prop.FindPropertyRelative("m_TypeName").stringValue, false);
@@ -313,6 +392,7 @@
return new UnityEvent();
return Activator.CreateInstance(type) as UnityEventBase;
}
+#endif

private static IEnumerable<ValidMethodMap> CalculateMethodMap(UnityEngine.Object target, Type[] t, bool allowSubclasses)
{
@@ -365,7 +445,14 @@

private static MethodInfo GetMethod(UnityEventBase dummyEvent, string methodName, UnityEngine.Object uObject, PersistentListenerMode modeEnum, Type argumentType)
{
- return (MethodInfo)findMethod.Invoke(dummyEvent, new object[] { methodName, uObject, modeEnum, argumentType });
+ return (MethodInfo)findMethod.Invoke(dummyEvent, new object[] {
+ methodName,
+#if UNITY_2020_1_OR_NEWER
+ uObject?.GetType(),
+#else
+ uObject,
+#endif
+ modeEnum, argumentType });
}

private static GenericMenu BuildPopupList(UnityEngine.Object target, UnityEventBase dummyEvent, SerializedProperty listener)

所感

  • Unity2021.1.6f1でちょっと試したところではちゃんと動作した
    • 軽く使ってみる分には問題ないが、実際には enum の並びに変更があった場合に追従できない点は注意が必要だと思う
  • 受け取り側は結局 int なのでキャストする必要があって面倒
  • UnityEvent のインスペクタ表示を乗っ取ってしまっているため、 バージョンアップに弱くあまりよろしくはないとは思う…
  • Unity が公式サポートしてくれるのが望ましいのだが…

参考