快速摘要
Unity 裡沒有哪一種事件寫法能通吃所有情境。本文用同一組範例比較 delegate、Action、UnityAction、UnityEvent、EventHandler<TEventArgs>,並直接給出實務選型。
![]()
Unity 裡能用來傳遞事件的寫法很多,但實務上不需要把它們當成五個平行選項來背。一般程式邏輯大多用 Action 就夠了;要讓事件能在 Inspector 直接綁定,才輪到 UnityEvent;當事件資料開始變多、後面還可能繼續擴充時,再考慮 EventHandler<TEventArgs> 會比較穩。
真正麻煩的地方,在於我們常在還沒分清楚情境時,就先選了工具。結果專案一大,事件越掛越多,訂閱點越來越散,後面要讀、要改、要除錯都開始卡。本文用同一組範例把五種常見寫法擺在一起,先看差異,再談怎麼選。
先把選型原則講清楚
如果事件只是用來通知程式內部某件事發生了,例如血量改變、按鈕按下、狀態切換,Action 通常是最乾脆的選擇。它是標準 C# 委派,寫法短,也不會把程式邏輯綁在 Unity 特定 API 上。
UnityEvent 的位置很不一樣。它最有價值的地方,在於可以序列化到場景與 Inspector,讓設計師、企劃或我們自己在編輯器裡直接綁事件。這點是一般 Action 和 event 做不到的,Unity 官方文件也正是從這個角度在介紹它。
EventHandler<TEventArgs> 則是另一個方向。它的用途,是把事件資料整理成一個明確型別。當參數開始變多,或未來很可能還要再加欄位時,這種寫法通常比一路往 Action<int, GameObject, int> 疊參數更好讀。
五種寫法放在同一個範例裡
下面這份腳本故意把五種常見寫法寫在一起,參數都維持同一組:id、player、score。這樣看差異會比較清楚。
1using System;
2using UnityEngine;
3using UnityEngine.Events;
4
5public class DelegateExample : MonoBehaviour
6{
7 // A: 最基本的自訂委派
8 public delegate void GameEventDelegate(int id, GameObject player, int score);
9 public event GameEventDelegate OnGameEventA;
10
11 // B: C# Action
12 public event Action<int, GameObject, int> OnGameEventB;
13
14 // C: UnityAction
15 public event UnityAction<int, GameObject, int> OnGameEventC;
16
17 // D: UnityEvent
18 [Serializable]
19 public class GameUnityEvent : UnityEvent<int, GameObject, int> { }
20
21 public GameUnityEvent OnGameEventD;
22
23 // E: EventHandler<TEventArgs>
24 public class GameEventArgs : EventArgs
25 {
26 public int Id { get; }
27 public GameObject Player { get; }
28 public int Score { get; }
29
30 public GameEventArgs(int id, GameObject player, int score)
31 {
32 Id = id;
33 Player = player;
34 Score = score;
35 }
36 }
37
38 public event EventHandler<GameEventArgs> OnGameEventE;
39
40 private void Start()
41 {
42 GameObject playerObject = new GameObject("Player");
43
44 OnGameEventA += HandleGameEvent;
45 OnGameEventA += (id, player, score) =>
46 Debug.Log($"Event A Triggered. ID: {id}, Player: {player.name}, Score: {score}");
47 OnGameEventA?.Invoke(1, playerObject, 100);
48
49 OnGameEventB += HandleGameEvent;
50 OnGameEventB += (id, player, score) =>
51 Debug.Log($"Event B Triggered. ID: {id}, Player: {player.name}, Score: {score}");
52 OnGameEventB?.Invoke(2, playerObject, 200);
53
54 OnGameEventC += HandleGameEvent;
55 OnGameEventC += (id, player, score) =>
56 Debug.Log($"Event C Triggered. ID: {id}, Player: {player.name}, Score: {score}");
57 OnGameEventC?.Invoke(3, playerObject, 300);
58
59 OnGameEventD ??= new GameUnityEvent();
60 OnGameEventD.AddListener(HandleGameEvent);
61 OnGameEventD.AddListener((id, player, score) =>
62 Debug.Log($"Event D Triggered. ID: {id}, Player: {player.name}, Score: {score}"));
63 OnGameEventD.Invoke(4, playerObject, 400);
64
65 OnGameEventE += HandleGameEventE;
66 OnGameEventE += (sender, args) =>
67 Debug.Log($"Event E Triggered. ID: {args.Id}, Player: {args.Player.name}, Score: {args.Score}");
68 OnGameEventE?.Invoke(this, new GameEventArgs(5, playerObject, 500));
69 }
70
71 private void HandleGameEvent(int id, GameObject player, int score)
72 {
73 Debug.Log($"Handled Game Event. ID: {id}, Player: {player.name}, Score: {score}");
74 }
75
76 private void HandleGameEventE(object sender, GameEventArgs args)
77 {
78 Debug.Log($"Handled Game Event E. ID: {args.Id}, Player: {args.Player.name}, Score: {args.Score}");
79 }
80}
五種寫法各自適合什麼情境
如果只看語法,這五種寫法很像都能做到「通知別人」。但拉回 Unity 專案的日常工作,它們各自站的位置其實很不一樣。
| 類型 | 優點 | 缺點 | 適用情境 |
|---|---|---|---|
| 基本 delegate | 完全自訂,想怎麼定義方法簽名都可以。 | 需要額外宣告型別,通常比 Action 冗長。 | 想保留明確語意,或需要客製化委派型別時。 |
| C# Action | 簡潔、標準、可移植性高。 | 無法在 Inspector 直接配置。 | 最常見的程式邏輯事件。 |
| UnityAction | 和 Unity 事件系統關係密切,很多 Unity API 直接使用它。 | 若沒有和 Unity API 互動,和 Action 的差異不大。 | 已經在用 UnityEvent,或 API 明確要求 UnityAction 時。 |
| UnityEvent | 可序列化到 Inspector,能加持久化回呼。 | 程式碼側的註冊寫法較重,也更依賴 Unity 環境。 | UI 按鈕、動畫事件、設計師會直接配置的場合。 |
| EventHandler | 事件資料可集中管理,欄位擴充時比較穩。 | 寫法比 Action 長。 | 參數多、事件資料會成長,或想走標準 .NET 事件模式時。 |
如果想看官方對 UnityEvent 與 UnityAction 的定位,請參考:Unity Manual: UnityEvents、Unity Scripting API: UnityAction。EventHandler<TEventArgs> 的標準事件模式則可參考:Microsoft Learn: Standard .NET event patterns。
參數一多,EventHandler<TEventArgs> 開始有優勢
範例裡所有事件都傳了三個參數。這時候 Action<int, GameObject, int> 還看得懂,但已經開始接近臨界點了。再多一兩個欄位,或其中一個欄位要改型別,訂閱端和呼叫端都會一起變得很吵。
把事件資料包進 EventArgs 類別後,讀程式的人不必一直背參數順序。看到 args.Player、args.Score,意圖就很直接。後面如果還要加欄位,也是在 GameEventArgs 上擴充,而不是把每個訂閱方法全部改一輪。
這也是為什麼我在專案裡的習慣很明確:少參數、單純通知型事件,用 Action;事件資料開始成形,甚至已經像一筆獨立訊息時,就改成 EventHandler<TEventArgs>。這樣後面比較不會後悔。
總結
Unity 事件寫法很多,但真正需要先做的只有一件事:分清楚這個事件到底要服務誰。一般程式邏輯優先用 Action;需要 Inspector 綁定時用 UnityEvent;事件資料開始變複雜時,再改成 EventHandler<TEventArgs>。把這個順序抓穩,後面就不太會為了語法選型反覆重寫。
常見問題
Action 通常是最直觀也最好移植的做法。它夠簡潔,也不會把程式邏輯綁在 Unity 特定 API 上。EventHandler<TEventArgs> 會比較穩。因為資料可以包進 EventArgs 類別,可讀性和維護性都會比一長串參數更好。UnityEvent 的優勢在於可以在編輯器中配置,適合 UI、動畫事件或設計師會直接操作的情境;如果只是一般程式邏輯,Action 往往更乾脆。