本篇繼續上一篇的內容來介紹第三種事件訂閱的方式:C# 介面。並在最後比較這三種訂閱方式。

C# 介面

使用 C# 介面讓物件提供事件的處理函式,並把物件註冊到事件系統上以訂閱事件。在 Unity 的 EventSystems 中就是使用 C# 介面讓物件訂閱操作事件。

public class UIPuzzleImage : MonoBehaviour,
    IBeginDragHandler, IDragHandler, IEndDragHandler
{
    public void OnBeginDrag(PointerEventData data)
    {
        Debug.Log("OnBeginDrag");
    }

    public void OnDrag(PointerEventData data)
    {
        Debug.Log("OnDrag");
    }

    public void OnEndDrag(PointerEventData data)
    {
        Debug.Log("OnEndDrag");
    }
}

實作事件系統

使用 C# 介面做為事件訂閱的方式,還需要自行管理訂閱的物件,以觸發事件。這邊使用 HashSet 來儲存訂閱的物件,好處是就算物件重複訂閱,在 HashSet 中也不會出現兩個物件。而且如果有需要,還可以先檢查該物件有沒有先訂閱了,而輸出警告訊息甚至是 exception。

public interface ILevelEventHandler
{
    void OnLevelStart();
}

public class LevelManager : MonoBehaviour
{
    public static LevelManager Instance;

    private readonly HashSet<ILevelEventHandler> _levelEventHandlers = new ();

    private void Awake()
    {
        Instance = this;
    }

    public void SubscribeLevelEvent(ILevelEventHandler handler)
    {
        // 也可不檢查
        if (_levelEventHandlers.Contains(handler))
        {
            Debug.Warning($"{handler} 已經被註冊過了");
            return;
        }

        _levelEventHandlers.Add(handler);
    }

    public void UnsubscribeLevelEvent(ILevelEventHandler handler)
    {
        _levelEventHandlers.Remove(handler);
    }

    private void LevelStart()
    {
        foreach (var handler in _levelEventHandlers)
            handler.OnLevelStart();
    }
}

而用 C# 介面訂閱事件的優點是不能用匿名函式訂閱事件,一定要是物件中明確實作的函式。

public class Enemy : MonoBehaviour, ILevelEventHandler
{
    private void Start()
    {
        LevelManager.Instance.SubscribeLevelEvent(this);
    }

    private void OnDestory()
    {
        LevelManager.Instance.UnsubscribeLevelEvent(this);
    }

    // 明確介面實作,讓事件函式只能透過 ILevelEventHandler 介面呼叫
    void ILevelEventHandler.OnLevelStart()
    {
        Debug.Log("OnLevelStart");
    }
}

C# 介面的問題

讓訂閱物件提供過多的 public 函式

會發現使用介面可能會讓物件提供過多的 public 函式,但是可以透過明確介面實作,讓實作的介面函式,只能透過介面型別呼叫。要注意在明確介面實作中,實作的函式是沒有 public 的。

public class UIPuzzleImage : MonoBehaviour,
    IBeginDragHandler, IDragHandler, IEndDragHandler
{
    void IBeginDragHandler.OnBeginDrag(PointerEventData data)
    {
        Debug.Log("OnBeginDrag");
    }

    void IDragHandler.OnDrag(PointerEventData data)
    {
        Debug.Log("OnDrag");
    }

    void IEndDragHandler.OnEndDrag(PointerEventData data)
    {
        Debug.Log("OnEndDrag");
    }
}

public class SomeClass : MonoBehaviour
{
    [SerializeField]
    private UIPuzzleImage _puzzleImage;

    private void SomeFunc()
    {
        // Error
        //_puzzleImage.OnDrag(null);
    }
}

事件觸發測試

以下在 Unity 中,以生成 1000 個物件測試觸發事件時的秏時。測試方式為一次觸發事件 10 次,取平均運行時間。

private StopWatch _stopWatch = new ();

public void InvokeEvent()
{
    var ticks = 0L;
    for (var i = 0; i < 10; ++i)
    {
        _stopWatch.Restart();
        // 如果是 C# 介面則用 foreach 去逐個觸發事件
        _onEvent.Invoke();
        _stopWatch.Stop();
        ticks += _stopWatch.ElapsedTicks;
    }

    Debug.Log($"{ticks / 10f}");
}

而首次執行中,因為 JIT 編譯器要轉換 IL 成可執行的機器碼,所以會比較花時間,因此分別列出:

  [在編輯器]
首次執行
[在編輯器]
首次之後
[執行檔]
首次執行
[執行檔]
首次之後
UnityEvent 257.7 ticks 175.5 ticks 248.4 ticks 153.4 ticks
C# event 127.3 ticks 31.2 ticks 194.2 ticks 28.7 ticks
C# 介面 150.9 ticks 35.1 ticks 260 ticks 32.9 ticks

可以發現除了在執行檔中,第一次執行時,C# 介面會比較慢之外,執行的速度由快到慢為:C# event、C# 介面、UnityEvent

總結

整理這三種訂閱事件方式的問題比較表:

  過多
public 函式
被外部
觸發事件
重複訂閱 註冊
匿名函式
處理速度
UnityEvent public
C# event 不會 不可
C# 介面 會,可避免 public 不可 不可 次快

Unity 事件訂閱方式有各自的好處,但相對的也有各自的問題。如果沒注意事件訂閱的管理的話,只要遊戲規模一大,就很容易出問題,而且還很難找出原因。而在這三種方法中,我會推薦使用 C# 介面做為事件訂閱的方法,因為相比其它兩種方式比較不容易出錯。而且追蹤有訂閱的物件也方便,只要找出有實作該介面的類別就可以了。