You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity自定义SimpleCallback属性抽屉:List<object>参数类型丢失问题求助

解决Unity自定义Callback中参数Inspector绘制的类型丢失与修改追踪问题

问题背景

我为替代UnityEvent(对多参数方法、异步逻辑支持不足),基于ScriptableObject实现了SimpleCallback类,但编写自定义PropertyDrawer时遇到两个核心问题:

  1. 用List<object>存储方法参数时,通过SerializedProperty访问会丢失类型信息,无法正确绘制对应字段
  2. 手动将参数转为UnityEngine.Object生成SerializedObject后,无法追踪修改值(EditorGUI.PropertyField不返回值,依赖SerializedObject.ApplyModifiedProperties但无法关联回原List<object>)

需要找到能根据System.Type获取对应PropertyDrawer的方法,或更优实现方案。

可行解决方案

方案1:直接获取对应类型的PropertyDrawer(推荐)

UnityEditor内置EditorGUIUtility.GetDrawerForType方法,可直接获取指定System.Type对应的PropertyDrawer,复用Unity内置的字段绘制逻辑,同时手动处理值的更新与保存。

核心实现代码

using UnityEngine;
using UnityEditor;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Linq;

[CustomPropertyDrawer(typeof(SimpleCallback))]
public class SimpleCallbackDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUI.BeginProperty(position, label, property);

        // 绘制Target选择框
        Rect targetRect = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight);
        SerializedProperty targetProp = property.FindPropertyRelative("target");
        EditorGUI.PropertyField(targetRect, targetProp);

        // 绘制Method选择框(实际项目需替换为自定义方法选择UI)
        Rect methodRect = new Rect(position.x, position.y + EditorGUIUtility.singleLineHeight + 2, position.width, EditorGUIUtility.singleLineHeight);
        EditorGUI.LabelField(methodRect, "Method", property.FindPropertyRelative("method").stringValue);

        var callback = property.serializedObject.targetObject as SimpleCallback;
        if (callback.target == null || callback.method == null)
        {
            EditorGUI.EndProperty();
            return;
        }

        // 过滤可绘制的参数
        ParameterInfo[] parameters = callback.method.GetParameters();
        List<ParameterInfo> drawableParams = parameters.Where(p => IsDrawableType(p.ParameterType)).ToList();

        // 绘制参数字段
        Rect paramRect = new Rect(position.x, methodRect.y + EditorGUIUtility.singleLineHeight + 4, position.width, EditorGUIUtility.singleLineHeight);
        for (int i = 0; i < drawableParams.Count; i++)
        {
            ParameterInfo param = drawableParams[i];
            Type paramType = param.ParameterType;
            GUIContent paramLabel = new GUIContent($"{param.Name} ({paramType.Name})");

            // 确保参数列表长度匹配
            while (callback.arguments.Count <= i)
            {
                callback.arguments.Add(GetDefaultValue(paramType));
            }
            object currentValue = callback.arguments[i];

            // 获取对应类型的PropertyDrawer
            PropertyDrawer drawer = EditorGUIUtility.GetDrawerForType(paramType);
            if (drawer != null)
            {
                // 用临时对象包装值,生成SerializedProperty供Drawer使用
                TempWrapper tempWrapper = new TempWrapper { Value = currentValue };
                SerializedObject tempSO = new SerializedObject(tempWrapper);
                SerializedProperty tempProp = tempSO.FindProperty("Value");

                // 绘制字段并同步修改
                drawer.OnGUI(paramRect, tempProp, paramLabel);
                tempSO.ApplyModifiedProperties();
                if (!Equals(tempWrapper.Value, currentValue))
                {
                    callback.arguments[i] = tempWrapper.Value;
                    EditorUtility.SetDirty(callback);
                }
            }
            else
            {
                // 处理无Drawer的基础类型
                object newValue = DrawBasicTypeField(paramRect, paramLabel, currentValue, paramType);
                if (!Equals(newValue, currentValue))
                {
                    callback.arguments[i] = newValue;
                    EditorUtility.SetDirty(callback);
                }
            }

            paramRect.y += EditorGUIUtility.singleLineHeight + 2;
        }

        EditorGUI.EndProperty();
    }

    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        var callback = property.serializedObject.targetObject as SimpleCallback;
        if (callback.target == null || callback.method == null)
        {
            return EditorGUIUtility.singleLineHeight * 2 + 2;
        }

        int paramCount = callback.method.GetParameters().Count(p => IsDrawableType(p.ParameterType));
        return EditorGUIUtility.singleLineHeight * (2 + paramCount) + 2 * (paramCount + 1);
    }

    // 判断类型是否可绘制
    private bool IsDrawableType(Type type)
    {
        return type.IsValueType || type == typeof(string) || typeof(UnityEngine.Object).IsAssignableFrom(type);
    }

    // 获取类型默认值
    private object GetDefaultValue(Type type)
    {
        return type.IsValueType ? Activator.CreateInstance(type) : null;
    }

    // 绘制基础类型字段
    private object DrawBasicTypeField(Rect rect, GUIContent label, object value, Type type)
    {
        if (type == typeof(int)) return EditorGUI.IntField(rect, label, (int)value);
        if (type == typeof(float)) return EditorGUI.FloatField(rect, label, (float)value);
        if (type == typeof(bool)) return EditorGUI.Toggle(rect, label, (bool)value);
        if (type == typeof(string)) return EditorGUI.TextField(rect, label, (string)value);
        if (typeof(UnityEngine.Object).IsAssignableFrom(type))
        {
            return EditorGUI.ObjectField(rect, label, value as UnityEngine.Object, type, true);
        }
        return value;
    }

    // 临时包装类,用于生成SerializedProperty
    private class TempWrapper
    {
        public object Value;
    }
}

方案2:优化参数存储结构

放弃List<object>,改用可序列化的参数容器类,将每个参数的类型信息与值分开存储(比如用string存储类型全名,用JsonUtility序列化/反序列化值)。这种方式能完全利用Unity的序列化系统,但需要额外处理类型转换逻辑,适合对序列化兼容性要求高的场景。

方案3:手动处理常见类型绘制

如果支持的参数类型有限,可直接通过switch-case处理基础类型(int、float、bool、string、UnityEngine.Object等),自行绘制对应字段并更新值。这种方式实现简单,但扩展性较差。

关键注意事项

  • 用EditorUtility.SetDirty标记SimpleCallback对象为脏,确保修改能被Unity保存
  • 处理参数列表长度与方法参数数量的匹配逻辑,避免索引越界
  • 对于异步方法,可在SimpleCallback.Invoke中判断方法返回值是否为IEnumerator,通过MonoBehaviour.StartCoroutine执行协程

内容的提问来源于stack exchange,提问作者Nianyi Wang

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 21:40:47