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

Unity限制UnityEvent单监听器或指定调用内部方法的实现方案

问题描述

我正在开发一款游戏通用资源,用来避免重复实现游戏里的常规功能。在ColorFollower脚本中定义了包含Custom1到Custom10的CustomComponentType枚举,ColorManager脚本持有颜色属性,并声明了public UnityEvent<ColorFollower>[] CustomFunctions;数组。需求是根据选中的枚举值,调用用户自定义脚本里的对应方法。

目前用UnityEvent数组做临时方案,通过枚举转索引调用对应的UnityEvent,但每个UnityEvent允许添加多个方法监听器,不符合我仅需单个方法引用的需求。需要解决以下任一问题:

  • 限制数组中每个UnityEvent仅能添加一个方法引用,并移除Inspector面板中每个元素的“+”“-”按钮;
  • 实现直接调用UnityEvent中的指定单个方法。

现有代码示例

ColorFollower脚本枚举代码

public enum CustomComponentType {
    Custom1,
    Custom2,
    Custom3,
    Custom4,
    Custom5,
    Custom6,
    Custom7,
    Custom8,
    Custom9,
    Custom10
}

public CustomComponentType customComponentType = CustomComponentType.Custom1;

当前调用逻辑代码

if (colorManager.CustomFunctions.Length > (int) customComponentType) {
    UnityEvent<ColorFollower> customFunction = colorManager.CustomFunctions[(int) customComponentType];
    customFunction?.Invoke((ColorFollower) this);
}

解决方案

方案1:自定义单方法事件类型并定制Inspector

通过创建自定义事件类+自定义PropertyDrawer,强制每个事件只能绑定一个方法,并隐藏Inspector中的加减按钮。

步骤1:创建自定义单参数事件类

using UnityEngine.Events;

[System.Serializable]
public class SingleUnityEvent_ColorFollower : UnityEvent<ColorFollower> { }

步骤2:修改ColorManager中的数组声明

把原有的UnityEvent数组替换成自定义事件类型:

public SingleUnityEvent_ColorFollower[] CustomFunctions;

步骤3:编写自定义PropertyDrawer(需放在Editor文件夹下)

这个Drawer会强制事件只能保留一个方法绑定,并且直接绘制单方法选择框,完全隐藏加减按钮:

using UnityEditor;
using UnityEngine;
using UnityEngine.Events;

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

        // 获取事件的持久化调用列表
        SerializedProperty persistentCalls = property.FindPropertyRelative("m_PersistentCalls");
        SerializedProperty callsList = persistentCalls.FindPropertyRelative("m_Calls");

        // 确保列表有且仅有一个调用项
        if (callsList.arraySize == 0) callsList.arraySize = 1;
        else if (callsList.arraySize > 1) callsList.arraySize = 1;

        // 获取唯一的调用项,绘制目标对象和方法选择框
        SerializedProperty call = callsList.GetArrayElementAtIndex(0);
        Rect methodRect = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label);
        
        EditorGUI.PropertyField(methodRect, call.FindPropertyRelative("m_Target"), GUIContent.none);
        EditorGUI.PropertyField(methodRect, call.FindPropertyRelative("m_MethodName"), GUIContent.none);

        EditorGUI.EndProperty();
    }
}

方案2:直接调用UnityEvent中的单个方法

如果不想修改Inspector,可通过代码直接提取UnityEvent中的第一个方法并调用,分为两种情况:

情况1:仅调用Inspector中绑定的持久化方法

if (colorManager.CustomFunctions.Length > (int)customComponentType)
{
    UnityEvent<ColorFollower> customFunction = colorManager.CustomFunctions[(int)customComponentType];
    if (customFunction != null)
    {
        var persistentCalls = customFunction.GetPersistentCalls();
        if (persistentCalls.Count > 0)
        {
            var firstCall = persistentCalls[0];
            if (firstCall.target != null && !string.IsNullOrEmpty(firstCall.methodName))
            {
                // 通过反射调用第一个绑定的方法
                firstCall.target.GetType().InvokeMethod(firstCall.methodName, new object[] { this });
            }
        }
    }
}

情况2:调用所有注册方法(包括代码动态AddListener的方法)

此方法依赖Unity私有字段,版本更新可能失效,需谨慎使用:

using System.Reflection;

// ...

if (colorManager.CustomFunctions.Length > (int)customComponentType)
{
    UnityEvent<ColorFollower> customFunction = colorManager.CustomFunctions[(int)customComponentType];
    if (customFunction != null)
    {
        // 反射获取UnityEvent内部的调用列表
        FieldInfo callsField = typeof(UnityEventBase).GetField("m_Calls", BindingFlags.Instance | BindingFlags.NonPublic);
        if (callsField != null)
        {
            var callsList = callsField.GetValue(customFunction) as System.Collections.IList;
            if (callsList != null && callsList.Count > 0)
            {
                var firstCall = callsList[0];
                // 反射获取调用项的目标对象和方法
                FieldInfo targetField = firstCall.GetType().GetField("m_Target", BindingFlags.Instance | BindingFlags.NonPublic);
                FieldInfo methodField = firstCall.GetType().GetField("m_Method", BindingFlags.Instance | BindingFlags.NonPublic);
                
                if (targetField != null && methodField != null)
                {
                    Object target = targetField.GetValue(firstCall) as Object;
                    MethodInfo method = methodField.GetValue(firstCall) as MethodInfo;
                    if (target != null && method != null)
                    {
                        method.Invoke(target, new object[] { this });
                    }
                }
            }
        }
    }
}

内容的提问来源于stack exchange,提问作者Jaimin Vashi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 08:08:17