Skip to content

Instantly share code, notes, and snippets.

@adammyhre
Created January 19, 2025 08:11
Show Gist options
  • Save adammyhre/e5318c8c9811264f0cabdd793b796529 to your computer and use it in GitHub Desktop.
Save adammyhre/e5318c8c9811264f0cabdd793b796529 to your computer and use it in GitHub Desktop.
Serialized Callback System
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}")
};
}
}
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;
}
}
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