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

如何实现Unity/C#中全局可访问自动注册的自定义事件函数

Unity实现零手动绑定自定义事件的可行方案

Unity原生的Start()、Update()这类生命周期事件没有用什么特殊黑魔法,核心逻辑就是提前扫描所有脚本的对应方法做缓存,到触发节点直接批量调用,照着这个逻辑实现就能做到和原生事件一致的使用体验:脚本里只要定义对应签名的方法就自动注册,不需要手动写绑定代码。

核心实现步骤

1. 约定自定义事件的固定方法签名

先统一你要的事件方法格式,比如你示例里的全局事件:

// 任意脚本中只要声明完全同签名的方法,就会被自动识别为事件监听者
public void OnEvent(string[] args)
{
    // 自定义业务逻辑
}

要求所有监听方法的方法名、参数列表、返回值完全一致,不要写重载,避免扫描时匹配错误。

2. 实现全局事件缓存管理器

核心逻辑是游戏启动时一次性扫描所有符合规则的脚本和实例,把方法信息缓存下来,触发事件时直接调用缓存的方法,避免每次触发都走反射带来性能损耗。
参考实现代码:

using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
using UnityEngine.SceneManagement;

public static class GlobalEventHub
{
    // 缓存容器:键为事件方法名,值为(脚本实例、方法元信息)的列表
    private static readonly Dictionary<string, List<(MonoBehaviour instance, MethodInfo method)>> _eventCache 
        = new Dictionary<string, List<(MonoBehaviour, MethodInfo)>();

    // 游戏启动后自动执行扫描,不需要外部手动调用
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
    private static void Init()
    {
        _eventCache.Clear();
        // 首次启动扫描当前场景所有实例
        CollectAllListeners();
        // 后续加载新场景时自动扫描新场景里的监听者
        SceneManager.sceneLoaded += (_, _) => CollectAllListeners();
    }

    private static void CollectAllListeners()
    {
        // 先清理缓存里已经被销毁的空实例,避免内存泄漏
        foreach (var listenerList in _eventCache.Values)
        {
            listenerList.RemoveAll(item => item.instance == null);
        }

        // 查找场景中所有存在的MonoBehaviour实例(包括未激活的)
        MonoBehaviour[] allActiveMonos = FindObjectsOfType<MonoBehaviour>(true);
        const string targetEventName = "OnEvent";
        Type[] targetParams = new Type[] { typeof(string[]) };

        foreach (var mono in allActiveMonos)
        {
            Type scriptType = mono.GetType();
            // 匹配对应签名的public实例方法
            MethodInfo targetMethod = scriptType.GetMethod(
                targetEventName, 
                BindingFlags.Public | BindingFlags.Instance,
                null, targetParams, null);

            // 方法不存在或者返回值不是void就跳过
            if (targetMethod == null || targetMethod.ReturnType != typeof(void)) continue;
            
            // 已经缓存过的实例不要重复加
            if (!_eventCache.TryGetValue(targetEventName, out var listenerList))
            {
                listenerList = new List<(MonoBehaviour, MethodInfo)>();
                _eventCache[targetEventName] = listenerList;
            }
            if (!listenerList.Exists(item => item.instance == mono))
            {
                listenerList.Add((mono, targetMethod));
            }
        }
    }

    /// <summary>
    /// 全局触发OnEvent事件,任意逻辑处都可以直接调用
    /// </summary>
    public static void FireEvent(string[] args)
    {
        if (!_eventCache.TryGetValue("OnEvent", out var listenerList)) return;
        
        // 倒序遍历,方便直接移除销毁/禁用的实例
        for (int i = listenerList.Count - 1; i >= 0; i--)
        {
            var (instance, method) = listenerList[i];
            // 自动跳过已经销毁、或者被禁用的脚本/物体
            if (instance == null || !instance.isActiveAndEnabled)
            {
                listenerList.RemoveAt(i);
                continue;
            }
            // 调用监听方法
            method.Invoke(instance, new object[] { args });
        }
    }
}

3. 直接使用

不需要做任何额外注册操作,任意脚本里写同签名的OnEvent方法就能收到事件,触发的时候只要在任意逻辑处调用GlobalEventHub.FireEvent(参数)就行,和你调用Unity原生API的体验完全一致。

性能优化和注意事项

  • 扫描逻辑只在游戏启动、新场景加载时执行,不会每帧运行,常规项目规模下几乎没有性能开销;缓存了MethodInfo之后的方法调用,性能远高于SendMessage/BroadcastMessage,和Unity原生调用生命周期方法的差距在业务层完全感知不到。
  • 如果需要扩展更多自定义事件,比如OnPlayerDead(int damage)、OnGamePause()这类,只要在扫描逻辑里增加对应的方法名、参数签名判断,扩展缓存规则即可,不需要改整体框架。
  • 如果使用Unity 2022及以上版本,可以用C#源生成器(Source Generator)把运行时反射扫描的逻辑完全挪到编译期执行,直接生成静态注册代码,运行时零反射开销,性能和手动写绑定代码完全一致,这也是目前Unity新版本内置生命周期事件的优化方向。
  • 不要试图用SendMessage做这类全局事件,这个API每次调用都会临时做反射查找,没有缓存,事件调用频率稍高就会造成明显的帧率卡顿,只适合快速做原型验证。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.02 07:57:27