Created
January 19, 2025 08:11
-
-
Save adammyhre/e5318c8c9811264f0cabdd793b796529 to your computer and use it in GitHub Desktop.
Serialized Callback System
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using UnityEngine; | |
public enum ValueType { Int, Float, Bool, String, Vector3 } | |
[Serializable] | |
public struct AnyValue { | |
public ValueType type; | |
// Storage for different types of values | |
public bool boolValue; | |
public int intValue; | |
public float floatValue; | |
public string stringValue; | |
public Vector3 vector3Value; | |
// Implicit conversion operators to convert AnyValue to different types | |
public static implicit operator bool(AnyValue value) => value.ConvertValue<bool>(); | |
public static implicit operator int(AnyValue value) => value.ConvertValue<int>(); | |
public static implicit operator float(AnyValue value) => value.ConvertValue<float>(); | |
public static implicit operator string(AnyValue value) => value.ConvertValue<string>(); | |
public static implicit operator Vector3(AnyValue value) => value.ConvertValue<Vector3>(); | |
public T ConvertValue<T>() { | |
if (typeof(T) == typeof(object)) return CastToObject<T>(); | |
return type switch { | |
ValueType.Int => AsInt<T>(intValue), | |
ValueType.Float => AsFloat<T>(floatValue), | |
ValueType.Bool => AsBool<T>(boolValue), | |
ValueType.String => (T) (object) stringValue, | |
ValueType.Vector3 => AsVector3<T>(vector3Value), | |
_ => throw new InvalidCastException($"Cannot convert AnyValue of type {type} to {typeof(T).Name}") | |
}; | |
} | |
// Helper methods for safe type conversions of the value types without the cost of boxing | |
T AsBool<T>(bool value) => typeof(T) == typeof(bool) && value is T correctType ? correctType : default; | |
T AsInt<T>(int value) => typeof(T) == typeof(int) && value is T correctType ? correctType : default; | |
T AsFloat<T>(float value) => typeof(T) == typeof(float) && value is T correctType ? correctType : default; | |
T AsVector3<T>(Vector3 value) => typeof(T) == typeof(Vector3) && value is T correctType ? correctType : default; | |
public static Type TypeOf(ValueType valueType) { | |
return valueType switch { | |
ValueType.Bool => typeof(bool), | |
ValueType.Int => typeof(int), | |
ValueType.Float => typeof(float), | |
ValueType.String => typeof(string), | |
ValueType.Vector3 => typeof(Vector3), | |
_ => throw new NotSupportedException($"Unsupported ValueType: {valueType}") | |
}; | |
} | |
public static ValueType ValueTypeOf(Type type) { | |
return type switch { | |
_ when type == typeof(bool) => ValueType.Bool, | |
_ when type == typeof(int) => ValueType.Int, | |
_ when type == typeof(float) => ValueType.Float, | |
_ when type == typeof(string) => ValueType.String, | |
_ when type == typeof(Vector3) => ValueType.Vector3, | |
_ => throw new NotSupportedException($"Unsupported type: {type}") | |
}; | |
} | |
T CastToObject<T>() { | |
return type switch { | |
ValueType.Int => (T) (object) intValue, | |
ValueType.Float => (T) (object) floatValue, | |
ValueType.Bool => (T) (object) boolValue, | |
ValueType.String => (T) (object) stringValue, | |
ValueType.Vector3 => (T) (object) vector3Value, | |
_ => throw new InvalidCastException($"Cannot convert AnyValue of type {type} to {typeof(T).Name}") | |
}; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Linq; | |
using System.Linq.Expressions; | |
using System.Reflection; | |
using UnityEngine; | |
using Object = UnityEngine.Object; | |
[Serializable] | |
public class SerializedCallback<TReturn> : ISerializationCallbackReceiver { | |
[SerializeField] Object targetObject; | |
[SerializeField] string methodName; | |
[SerializeField] AnyValue[] parameters; | |
[NonSerialized] Delegate cachedDelegate; | |
[NonSerialized] bool isDelegateRebuilt; | |
public TReturn Invoke() { | |
return Invoke(parameters); | |
} | |
public TReturn Invoke(params AnyValue[] args) { | |
if (!isDelegateRebuilt) BuildDelegate(); | |
if (cachedDelegate != null) { | |
var result = cachedDelegate.DynamicInvoke(ConvertParameters(args)); | |
return (TReturn)Convert.ChangeType(result, typeof(TReturn)); | |
} | |
Debug.LogWarning($"Unable to invoke method {methodName} on {targetObject}"); | |
return default; | |
} | |
object[] ConvertParameters(AnyValue[] args) { | |
if (args == null || args.Length == 0) return Array.Empty<object>(); | |
var convertedParams = new object[args.Length]; | |
for (int i = 0; i < args.Length; i++) { | |
convertedParams[i] = args[i].ConvertValue<object>(); | |
} | |
return convertedParams; | |
} | |
void BuildDelegate() { | |
cachedDelegate = null; | |
if (targetObject == null || string.IsNullOrEmpty(methodName)) { | |
Debug.LogWarning("Target object or method name is null, cannot rebuild delegate."); | |
return; | |
} | |
Type targetType = targetObject.GetType(); | |
MethodInfo methodInfo = targetType.GetMethod(methodName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); | |
if (methodInfo == null) { | |
Debug.LogWarning($"Method {methodName} not found on {targetObject}"); | |
return; | |
} | |
Type[] parameterTypes = methodInfo.GetParameters().Select(p => p.ParameterType).ToArray(); | |
if (parameters.Length != parameterTypes.Length) { | |
Debug.LogWarning($"Parameter mismatch for method {methodName}"); | |
return; | |
} | |
Type delegateType = Expression.GetDelegateType(parameterTypes.Append(methodInfo.ReturnType).ToArray()); | |
cachedDelegate = methodInfo.CreateDelegate(delegateType, targetObject); | |
isDelegateRebuilt = true; | |
} | |
public void OnBeforeSerialize() { | |
// noop | |
} | |
public void OnAfterDeserialize() { | |
isDelegateRebuilt = false; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Linq; | |
using System.Reflection; | |
using UnityEditor; | |
using UnityEditor.UIElements; | |
using UnityEngine; | |
using UnityEngine.UIElements; | |
using Object = UnityEngine.Object; | |
[CustomPropertyDrawer(typeof(SerializedCallback<>), true)] | |
public class SerializedCallbackDrawerUI : PropertyDrawer { | |
public override VisualElement CreatePropertyGUI(SerializedProperty property) { | |
VisualElement root = new (); | |
SerializedProperty targetProp = property.FindPropertyRelative("targetObject"); | |
ObjectField targetField = new ("Target") { | |
objectType = typeof(Object), | |
bindingPath = targetProp.propertyPath | |
}; | |
root.Add(targetField); | |
SerializedProperty methodProp = property.FindPropertyRelative("methodName"); | |
Button methodField = new () { | |
text = string.IsNullOrEmpty(methodProp.stringValue) ? "Select Method" : methodProp.stringValue | |
}; | |
root.Add(methodField); | |
methodField.clicked += () => ShowMethodDropdown(targetProp.objectReferenceValue, methodProp, property, methodField, root); | |
SerializedProperty parametersProp = property.FindPropertyRelative("parameters"); | |
VisualElement parametersContainer = new (); | |
root.Add(parametersContainer); | |
UpdateParameters(parametersProp, parametersContainer); | |
property.serializedObject.ApplyModifiedProperties(); | |
return root; | |
} | |
void ShowMethodDropdown(Object target, SerializedProperty methodProp, SerializedProperty property, Button methodButton, VisualElement root) { | |
if (target == null) return; | |
GenericMenu menu = new (); | |
Type targetType = target.GetType(); | |
Type callbackType = fieldInfo.FieldType; | |
Type genericType = callbackType.GetGenericArguments()[0]; | |
if (callbackType.IsGenericType) { | |
var methods = targetType.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) | |
.Where(m => m.ReturnType == genericType) | |
.ToArray(); | |
foreach (MethodInfo method in methods) { | |
menu.AddItem( | |
new GUIContent(method.Name), | |
false, | |
() => { | |
methodProp.stringValue = method.Name; | |
methodButton.text = method.Name; | |
SerializedProperty parametersProp = property.FindPropertyRelative("parameters"); | |
var parameters = method.GetParameters(); | |
parametersProp.arraySize = parameters.Length; | |
for (int i = 0; i < parameters.Length; i++) { | |
SerializedProperty paramProp = parametersProp.GetArrayElementAtIndex(i); | |
SerializedProperty typeProp = paramProp.FindPropertyRelative("type"); | |
typeProp.enumValueIndex = (int) AnyValue.ValueTypeOf(parameters[i].ParameterType); | |
} | |
property.serializedObject.ApplyModifiedProperties(); | |
VisualElement parametersContainer = root.Children().Last(); | |
parametersContainer.Clear(); | |
UpdateParameters(parametersProp, parametersContainer); | |
} | |
); | |
if (!methods.Any()) { | |
menu.AddDisabledItem(new GUIContent("No methods found")); | |
} | |
menu.ShowAsContext(); | |
} | |
} | |
} | |
void UpdateParameters(SerializedProperty parametersProp, VisualElement container) { | |
if (!parametersProp.isArray) return; | |
for (int i = 0; i < parametersProp.arraySize; i++) { | |
SerializedProperty parameter = parametersProp.GetArrayElementAtIndex(i); | |
SerializedProperty typeProp = parameter.FindPropertyRelative("type"); | |
ValueType paramType = (ValueType) typeProp.enumValueIndex; | |
VisualElement field; | |
switch (paramType) { | |
case ValueType.Int: | |
SerializedProperty intProp = parameter.FindPropertyRelative("intValue"); | |
IntegerField intField = new ($"Parameter {i + 1} (Int)"); | |
intField.value = intProp.intValue; | |
intField.RegisterValueChangedCallback( | |
evt => { | |
intProp.intValue = evt.newValue; | |
parametersProp.serializedObject.ApplyModifiedProperties(); | |
} | |
); | |
field = intField; | |
break; | |
case ValueType.Float: | |
SerializedProperty floatProp = parameter.FindPropertyRelative("floatValue"); | |
FloatField floatField = new ($"Parameter {i + 1} (Float)"); | |
floatField.value = floatProp.floatValue; | |
floatField.RegisterValueChangedCallback( | |
evt => { | |
floatProp.floatValue = evt.newValue; | |
parametersProp.serializedObject.ApplyModifiedProperties(); | |
} | |
); | |
field = floatField; | |
break; | |
case ValueType.String: | |
SerializedProperty stringProp = parameter.FindPropertyRelative("stringValue"); | |
TextField stringField = new ($"Parameter {i + 1} (String)"); | |
stringField.value = stringProp.stringValue; | |
stringField.RegisterValueChangedCallback( | |
evt => { | |
stringProp.stringValue = evt.newValue; | |
parametersProp.serializedObject.ApplyModifiedProperties(); | |
} | |
); | |
field = stringField; | |
break; | |
case ValueType.Bool: | |
SerializedProperty boolProp = parameter.FindPropertyRelative("boolValue"); | |
Toggle boolField = new ($"Parameter {i + 1} (Bool)"); | |
boolField.value = boolProp.boolValue; | |
boolField.RegisterValueChangedCallback( | |
evt => { | |
boolProp.boolValue = evt.newValue; | |
parametersProp.serializedObject.ApplyModifiedProperties(); | |
} | |
); | |
field = boolField; | |
break; | |
case ValueType.Vector3: | |
SerializedProperty vector3Prop = parameter.FindPropertyRelative("vector3Value"); | |
Vector3Field vector3Field = new ($"Parameter {i + 1} (Vector3)"); | |
vector3Field.value = vector3Prop.vector3Value; | |
vector3Field.RegisterValueChangedCallback( | |
evt => { | |
vector3Prop.vector3Value = evt.newValue; | |
parametersProp.serializedObject.ApplyModifiedProperties(); | |
} | |
); | |
field = vector3Field; | |
break; | |
default: | |
field = new Label($"Parameter {i + 1}: Unsupported Type"); | |
break; | |
} | |
container.Add(field); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment