如何避免Unity UI事件回调链?寻求非单例替代方案
解决Unity中避免回调链传递事件的方案(非单例版)
你现在的痛点很明确——不想让事件从C→B→A层层传递,也不想用单例EventManager,下面给你几个实用的非单例方案,都是Unity里常用的思路:
方案1:直接透传回调,跳过B的中转
核心思路是让A把自己的处理方法直接传递给C,而不是通过B来转发。具体可以让B在创建C的时候,把A的回调直接绑定到C的事件上,B本身不再持有事件。
修改后的代码示例:
class A : MonoBehaviour { void Start() { GameObject bGO = InstantiateGameObject(); B b = bGO.GetComponent<B>(); // 直接把A的处理方法传给B,让B转交给C b.SetButtonCallback(DoSmthinA); } private void DoSmthinA() { Debug.Log("A"); } } class B : MonoBehaviour { private C _cInstance; void Start() { GameObject cGO = InstantiateGameObject(); _cInstance = cGO.GetComponent<C>(); } // 新增方法,接收A的回调并绑定到C public void SetButtonCallback(System.Action callback) { if(_cInstance != null) { _cInstance.m_onButtonClicked += callback; } } } class C : MonoBehaviour { public UnityEngine.UI.Button m_button; public event System.Action m_onButtonClicked; void Start() { m_button.onClick.AddListener(OnButtonClicked); } private void OnButtonClicked() { Debug.Log("C"); m_onButtonClicked?.Invoke(); } }
这样B就只是个“传递者”,不再需要维护自己的事件,直接把A的回调绑定到C上,事件触发时C直接通知A,砍掉了中间的回调链。
方案2:用ScriptableObject实现无引用事件总线
这个方案是Unity里非常流行的非单例事件方案,用ScriptableObject作为事件的“载体”,C触发事件,A订阅事件,完全不需要A、B、C之间有直接引用。
步骤1:创建ScriptableObject事件类
[CreateAssetMenu(fileName = "ButtonClickedEvent", menuName = "Events/Button Clicked Event")] public class ButtonClickedEvent : ScriptableObject { private event System.Action _onTriggered; public void Subscribe(System.Action callback) { _onTriggered += callback; } public void Unsubscribe(System.Action callback) { _onTriggered -= callback; } public void Trigger() { _onTriggered?.Invoke(); } }
你可以在Project窗口右键创建这个SO实例,命名比如ButtonClickedEvent.asset。
步骤2:修改C和A的代码
class A : MonoBehaviour { public ButtonClickedEvent buttonClickedEvent; // 在Inspector里拖入刚才创建的SO实例 void OnEnable() { buttonClickedEvent.Subscribe(DoSmthinA); } void OnDisable() { buttonClickedEvent.Unsubscribe(DoSmthinA); } private void DoSmthinA() { Debug.Log("A"); } } class C : MonoBehaviour { public UnityEngine.UI.Button m_button; public ButtonClickedEvent buttonClickedEvent; // 同样拖入SO实例 void Start() { m_button.onClick.AddListener(OnButtonClicked); } private void OnButtonClicked() { Debug.Log("C"); buttonClickedEvent.Trigger(); } }
B的代码完全不需要修改,甚至B可以不知道这个事件的存在!因为A和C都是通过SO来通信,没有任何实例引用,完美解决回调链问题,而且不是单例——你可以创建多个SO实例,对应不同的按钮事件,非常灵活。
方案3:让A直接获取C的实例并订阅
如果B创建C后,能把C的实例暴露给A,那A可以直接订阅C的事件,跳过B。比如B里保存C的引用,提供一个公共方法让A获取:
class A : MonoBehaviour { void Start() { GameObject bGO = InstantiateGameObject(); B b = bGO.GetComponent<B>(); // 获取B创建的C实例,直接订阅 C c = b.GetCInstance(); if(c != null) { c.m_onButtonClicked += DoSmthinA; } } private void DoSmthinA() { Debug.Log("A"); } } class B : MonoBehaviour { private C _cInstance; void Start() { GameObject cGO = InstantiateGameObject(); _cInstance = cGO.GetComponent<C>(); // 这里可以取消B自己的订阅,除非B本身需要处理这个事件 // _cInstance.m_onButtonClicked += DoSmthinB; } // 提供公共方法返回C的实例 public C GetCInstance() { return _cInstance; } // 如果B不需要处理事件,可以删掉这个方法 private void DoSmthinB() { Debug.Log("B"); } }
这个方案最简单直接,但缺点是A需要知道B持有C的关系,耦合度比SO方案高一点,适合小型项目或者简单场景。
总结一下,最推荐的是ScriptableObject事件总线,既解耦又灵活,完全符合你的非单例需求;如果场景简单,直接透传回调或者直接获取C实例也能快速解决问题。
内容的提问来源于stack exchange,提问作者Lovecry
相关产品推荐
相关产品推荐

