インスペクタ上でのメソッド指定
EventTriggerとかButtonのOnClick()でメソッド指定することがあるかと思います。今回はあれを作りました。
これやるのはUnityEvent使えばできます。あくまでこの記事は車輪の再発明です。
EventTriggerもそうなんですが実はこれ制限がきつくて引数が1個以下かつ返り値は受け取れません。SendMessage使ってるから。
結果
コード
クラスは選択メソッド定義と呼び出しとエディタ拡張の3つ使います。
まず選択メソッド定義。SendMessage飛ばすMonoBehaviourとメソッド名と引数関連のメンバー持ってる。
using System;
using UnityEngine;
using UnityEngine.EventSystems;
[System.Serializable]
public class SelectMethod : ISerializationCallbackReceiver
{
public MonoBehaviour TargetObject;
public string CallbackName;
public System.Object MethodArgument;
[SerializeField, HideInInspector] string ArgSerialized;
[SerializeField, HideInInspector] string ArgType;
public void OnBeforeSerialize()
{
if (MethodArgument != null)
{
ArgSerialized = MethodArgument.ToString();
ArgType = MethodArgument.GetType().ToString();
}
}
public void OnAfterDeserialize()
{
if (!string.IsNullOrEmpty(ArgSerialized) && !string.IsNullOrEmpty(ArgType))
{
DeserializeFromString();
}
}
//実行時MethodArgumentをデシリアライズ
public void Execute()
{
if (!string.IsNullOrEmpty(ArgSerialized) && !string.IsNullOrEmpty(ArgType))
{
DeserializeFromString();
}
if (MethodArgument != null)
{
TargetObject.SendMessage(CallbackName, MethodArgument);
}
else
{
TargetObject.SendMessage(CallbackName);
}
}
//デシリアライズ処理
void DeserializeFromString()
{
if (string.IsNullOrEmpty(ArgSerialized) || string.IsNullOrEmpty(ArgType))
{
return;
}
//System.TypeとUnityEngine.Typeのデシリアライズ
var type = System.Type.GetType(ArgType);
if (type == null)
type = System.Reflection.Assembly.Load("UnityEngine.dll").GetType(ArgType);
if (type == typeof(string))
MethodArgument = ArgSerialized;
if (type == typeof(bool))
MethodArgument = bool.Parse(ArgSerialized);
else if (type == typeof(int))
MethodArgument = int.Parse(ArgSerialized);
else if (type == typeof(float))
MethodArgument = float.Parse(ArgSerialized);
else if (type == typeof(double))
MethodArgument = double.Parse(ArgSerialized);
else if (type == typeof(Vector2))
MethodArgument = Vector2FromString(ArgSerialized);
else if (type == typeof(Vector3))
MethodArgument = Vector3FromString(ArgSerialized);
else if (type == typeof(Vector4))
MethodArgument = Vector4FromString(ArgSerialized);
}
Vector2 Vector2FromString(string s)
{
string[] sArray = s.Substring(1, s.Length - 2).Split(',');
return new Vector2(float.Parse(sArray[0]), float.Parse(sArray[1]));
}
Vector3 Vector3FromString(string s)
{
string[] sArray = s.Substring(1, s.Length - 2).Split(',');
return new Vector3(float.Parse(sArray[0]), float.Parse(sArray[1]), float.Parse(sArray[2]));
}
Vector4 Vector4FromString(string s)
{
string[] sArray = s.Substring(1, s.Length - 2).Split(',');
return new Vector4(float.Parse(sArray[0]), float.Parse(sArray[1]), float.Parse(sArray[2]), float.Parse(sArray[3]));
}
}
呼び出し側。SelectMethodのList持ってればなんでもいい。
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
public class Test : MonoBehaviour
{
public List<SelectMethod> MethodList = new List<SelectMethod>();
void Start()
{
//呼び出し これは好きなタイミングで呼ぶ
MethodList.ForEach(item => item.Execute());
}
//テスト用メソッド群
public void Test(string t)
{
Debug.Log("Test Log : " + t);
}
public void BoolTest(bool b)
{
Debug.Log("Test Log : " + b);
}
public void VectorTest(Vector3 v)
{
Debug.Log("Test Log : " + v);
}
Test用のEditor拡張。自分で使うときはTestにキャストしてる部分を適当に入れ替えてください。
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
[CanEditMultipleObjects]
[CustomEditor(typeof(Test))]
public class TestEditor : Editor
{
private ReorderableList RL;
private void OnEnable()
{
//ReorderableList作成
var ListProp = serializedObject.FindProperty("MethodList");
var test = (TestEditor)target;
RL = new ReorderableList(test.MethodList, typeof(SelectMethod));
RL.elementHeight = 40;
RL.drawHeaderCallback = (rect) =>
{
EditorGUI.LabelField(rect, "OnTest()");
};
RL.drawElementCallback = (rect, index, isActive, isFocused) =>
{
serializedObject.Update();
var element = ListProp.GetArrayElementAtIndex(index);
var m = test.MethodList[index];
if (m != null)
{
var MonoRect = new Rect(rect)
{
height = EditorGUIUtility.singleLineHeight
};
var MethodRect = new Rect(rect)
{
height = EditorGUIUtility.singleLineHeight,
width = rect.width / 3,
y = MonoRect.y + EditorGUIUtility.singleLineHeight + 2
};
var ArgumentRect = new Rect(rect)
{
height = EditorGUIUtility.singleLineHeight,
width = (rect.width * 2 / 3) - 4,
x = MethodRect.x + MethodRect.width + 2,
y = MonoRect.y + EditorGUIUtility.singleLineHeight + 2
};
EditorGUI.PropertyField(MonoRect, element.FindPropertyRelative("TargetObject"));
var methods = CollectMethods(m);
if (methods != null)
{
//メソッド名のインデックスを取得(見つからなかったらNoneである0にする)
int NameIndex = methods.FindIndex(item => item == m.CallbackName);
NameIndex = NameIndex != -1 ? NameIndex : 0;
EditorGUI.LabelField(MethodRect, "Methods");
m.CallbackName = methods[EditorGUI.Popup(MethodRect, NameIndex, methods.ToArray())];
}
if (m.TargetObject)
{
var arg = m.TargetObject.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public)
.Where(x => x.Name == m.CallbackName);
if (arg.Count() != 0)
{
var param = arg.First();
if (param.GetParameters().Length != 0)
{
param.GetParameters();
//フィールド用の描画及びMethodArgumentの初期化をベタ書き
if (param.GetParameters()[0].ParameterType == typeof(string))
{
if (m.MethodArgument == null)
m.MethodArgument = "";
m.MethodArgument = EditorGUI.TextField(ArgumentRect, (string)m.MethodArgument);
}
else if (param.GetParameters()[0].ParameterType == typeof(bool))
{
if (m.MethodArgument == null)
m.MethodArgument = false;
m.MethodArgument = EditorGUI.Toggle(ArgumentRect, (bool)m.MethodArgument);
}
else if (param.GetParameters()[0].ParameterType == typeof(int))
{
if (m.MethodArgument == null)
m.MethodArgument = (int)0;
m.MethodArgument = EditorGUI.IntField(ArgumentRect, (int)m.MethodArgument);
}
else if (param.GetParameters()[0].ParameterType == typeof(float))
{
if (m.MethodArgument == null)
m.MethodArgument = 0f;
m.MethodArgument = EditorGUI.FloatField(ArgumentRect, (float)m.MethodArgument);
}
else if (param.GetParameters()[0].ParameterType == typeof(double))
{
if (m.MethodArgument == null)
m.MethodArgument = 0.0;
m.MethodArgument = EditorGUI.DoubleField(ArgumentRect, (double)m.MethodArgument);
}
else if (param.GetParameters()[0].ParameterType == typeof(Vector2))
{
if (m.MethodArgument == null)
m.MethodArgument = new Vector2();
m.MethodArgument = EditorGUI.Vector2Field(ArgumentRect, "", (Vector2)m.MethodArgument);
}
else if (param.GetParameters()[0].ParameterType == typeof(Vector3))
{
if (m.MethodArgument == null)
m.MethodArgument = new Vector3();
m.MethodArgument = EditorGUI.Vector3Field(ArgumentRect, "", (Vector3)m.MethodArgument);
}
else if (param.GetParameters()[0].ParameterType == typeof(Vector4))
{
if (m.MethodArgument == null)
m.MethodArgument = new Vector4();
m.MethodArgument = EditorGUI.Vector4Field(ArgumentRect, "", (Vector4)m.MethodArgument);
}
}
}
}
serializedObject.ApplyModifiedProperties();
}
EditorGUI.PropertyField(rect, element);
};
}
public override void OnInspectorGUI()
{
DrawDefaultInspector();
serializedObject.Update();
RL.DoLayoutList();
serializedObject.ApplyModifiedProperties();
}
//メソッドの取得(SendMessageを使うので引数が1以下かつpublicに限定)
List<string> CollectMethods(SelectMethod m)
{
if (m == null || !m.TargetObject)
{
return null;
}
List<string> result = new List<string>();
result.Add("None");
var methods = m.TargetObject.GetType().GetMethods(BindingFlags.Instance | BindingFlags.Public)
.Where(x => x.DeclaringType == m.TargetObject.GetType())
.Where(x => x.GetParameters().Length <= 1)
.Select(x => x.Name)
.ToArray();
result.AddRange(methods);
return result;
}
}
課題
本家の引数指定と比べて使える引数が圧倒的に少ない。
System.Objectは何でも入るがUnityのシリアライズ対象にならないため自前でデシリアライズしているが全部ベタ書きするのはちょっと...
またUnityEngine.Objectは入力フィールド描画時にPropertyFieldを使うがSystem.ObjectはUnityのシリアライズ対象にならないため使えない。
この辺解決する方法シリアライズに詳しい方教えてください...