為什麼 Unity 開發需要事件與委派?
在 Unity 遊戲開發 中,有效管理物件之間的溝通至關重要。 C# 的事件(Event)與委派(Delegate) 正是實現「解耦合」訊息傳遞的核心工具。
- 它們允許一個物件在事件觸發時,自動通知 所有監聽者。
- 事件發送者 不需要知道 接收者的細節,從而降低耦合度。
本文將逐步介紹 五種常見事件處理方式,並透過完整程式碼與比較表格,幫助你判斷在何種情境下該選擇哪一種。
五種事件與委派完整程式碼範例
以下 DelegateExample.cs 腳本展示了五種事件的宣告、訂閱與觸發方式,範例參數統一為:
int id
GameObject player
int score
👉 這樣方便直觀地比較它們的差異。
1using System;
2using UnityEngine;
3using UnityEngine.Events;
4
5public class DelegateExample : MonoBehaviour
6{
7 // A: 最基本的委派
8 public delegate void GameEventA(int id, GameObject player, int score);
9 public event GameEventA OnGameEventA;
10
11 // B: 使用 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 public UnityEvent<int, GameObject, int> OnGameEventD;
19
20 // E: 使用 EventHandler 委派
21 public class GameEventEArgs : EventArgs
22 {
23 public int id;
24 public GameObject player;
25 public int score;
26
27 public GameEventEArgs(int id, GameObject player, int score)
28 {
29 this.id = id;
30 this.player = player;
31 this.score = score;
32 }
33 }
34 public event EventHandler<GameEventEArgs> OnGameEventE;
35
36 private void Start()
37 {
38 // 取得一個範例用的 GameObject
39 GameObject playerObject = new GameObject("Player");
40
41 // A: 範例
42 OnGameEventA += HandleGameEvent;
43 OnGameEventA += (id, player, score) => Debug.Log($"Event A Triggered. ID: {id}, Player: {player.name}, Score: {score}");
44 OnGameEventA?.Invoke(1, playerObject, 100);
45
46 // B: 範例
47 OnGameEventB += HandleGameEvent;
48 OnGameEventB += (id, player, score) => Debug.Log($"Event B Triggered. ID: {id}, Player: {player.name}, Score: {score}");
49 OnGameEventB?.Invoke(2, playerObject, 200);
50
51 // C: 範例
52 OnGameEventC += HandleGameEvent;
53 OnGameEventC += (id, player, score) => Debug.Log($"Event C Triggered. ID: {id}, Player: {player.name}, Score: {score}");
54 OnGameEventC?.Invoke(3, playerObject, 300);
55
56 // D: 範例
57 OnGameEventD ??= new UnityEvent<int, GameObject, int>();
58 OnGameEventD.AddListener(HandleGameEvent);
59 OnGameEventD.AddListener((id, player, score) => Debug.Log($"Event D Triggered. ID: {id}, Player: {player.name}, Score: {score}"));
60 OnGameEventD.Invoke(4, playerObject, 400);
61
62 // E: 範例
63 OnGameEventE += HandleGameEventE;
64 OnGameEventE += (sender, args) => Debug.Log($"Event E Triggered. ID: {args.id}, Player: {args.player.name}, Score: {args.score}");
65 OnGameEventE?.Invoke(this, new GameEventEArgs(5, playerObject, 500));
66 }
67
68 // 處理委派事件的方法(A, B, C, D 共用)
69 public void HandleGameEvent(int id, GameObject player, int score)
70 {
71 Debug.Log($"Handled Game Event with ID: {id}, Player: {player.name}, Score: {score}");
72 }
73
74 // 處理 EventHandler 事件的方法(E 專用)
75 public void HandleGameEventE(object sender, GameEventEArgs args)
76 {
77 // 注意,這裡的程式碼比 HandleGameEvent 更易於閱讀和維護
78 Debug.Log($"Handled Game Event E with ID: {args.id}, Player: {args.player.name}, Score: {args.score}");
79 }
80}
Unity 中常見的五種事件用法比較
類型 | 優點 | 缺點 | 適用情境 |
---|---|---|---|
基本 delegate | 完全自訂義,語法直觀。 | 額外宣告類型,程式碼冗長。 | 需要 高度客製化方法簽名 時。 |
C# Action | 簡潔、標準,無需額外宣告。 | 無法在 Unity 編輯器中直接配置。 | 最常用,適合 輕量級事件傳遞。 |
UnityAction | 與 Action 相似,但 針對 Unity 最佳化。 | 與 Action 功能重疊。 | 當需 Unity API 整合 或習慣使用時。 |
UnityEvent | 編輯器可見,非程式人員可直接設定。 | 動態註冊語法較繁瑣。 | 適合 UI 按鈕、動畫事件 等由設計師配置的場合。 |
C# EventHandler | 標準化,適合多參數,可擴充性強。 | 需額外繼承 EventArgs 類別,語法稍繁瑣。 | 大型專案、參數多且可能擴充 的事件。 |
EventHandler:多參數事件的最佳解法
在範例中,所有事件都傳遞了三個參數。但對比 HandleGameEvent
與 HandleGameEventE
,你會發現 EventHandler 的優勢:
程式碼可讀性更佳
Action
:必須記住參數順序與意義,維護成本高。EventHandler
:所有資料封裝在args
,存取直觀 (args.id
、args.player
)。
避免參數順序錯誤
Action<int, GameObject, int>
很容易傳錯順序。EventHandler
只傳EventArgs
物件,完全避免此問題。
高度可擴充性
Action
:新增參數需修改所有訂閱方法。EventHandler
:只需在繼承EventArgs
的類別中新增屬性,不影響既有訂閱者 (其他程式碼)。
結論:如何選擇適合的事件類型?
- 小型專案、簡單事件 → 優先使用
Action
、UnityAction
,直觀又高效。 - 需要編輯器配置 → 選擇
UnityEvent
,方便設計師與企劃人員。 - 多參數、可擴充性 → 推薦使用
EventHandler
,更專業、健壯、好維護。
目前我在業界中,在無參數或少參數的需求下,Action
是 C# 與 Unity 專案的最佳首選,寫出來的程式碼可以任意的移植到 Unity 或非 Unity 的 C# 專案中 (只有 Unity 的話也能用 UnityAction
)。當參數較多的時候,則改用 EventHandler
的方式來進行,可以維持良好的可讀性、擴展與維護性。
UnityEvent
在程式設計上的優點,是它在呼叫前不需要像 Action
或 EventHandler
那樣進行 null
檢查,但其動態註冊的語法較為冗長。