[Unity] 事件訂閱與問題(下)- C# 介面
本篇繼續上一篇的內容來介紹第三種事件訂閱的方式: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# 介面做為事件訂閱的方法,因為相比其它兩種方式比較不容易出錯。而且追蹤有訂閱的物件也方便,只要找出有實作該介面的類別就可以了。