unity 委託事件簡單應用

TheAI發表於2017-12-19
[csharp] view plain copy
  1. public ObjA objA;  
  2. public ObjB objB;  
  3. public MyUI myUI;  
  4.   
  5. void DoSomething(){  
  6.   objA.do();  
  7.   objB.do();  
  8.   myUI.show("233");  
  9. }  

上面程式碼所展示的問題呢就是:變數過多,呼叫複雜,可能會出現呼叫死迴圈,維護麻煩,邏輯混亂等等,那麼,我們就得想辦法解決了。本次主要講解事件的使用,需要有對於委託和事件的基本認識,如果有不太理解的朋友呢請參考這裡

好了,假設我們現在需要設計一款計時器(Timer)的功能,它有一些基礎的事件:開始計時、暫停計時、停止計時(以下簡稱“計時器事件”)。並且要將這個事件告訴給A,那肯定會有人透過宣告一個變數A ,然後在計時器事件中進行呼叫。那現在突然需求改變了,一下子增加了B、C、D、E.....Z。這下怎麼辦呢? 肯定不能一個個放進去了吧。所以我們就需要有個機制讓計時器用大喇叭喊,這樣所有的人就能聽到了,這樣就引入了“事件”這個概念。那麼就有以下程式:

1、計時器:

[csharp] view plain copy
  1. using UnityEngine;  
  2. using System.Collections;  
  3. /// <summary>  
  4. ///計時器   
  5. /// </summary>  
  6. public class Timer : MonoBehaviour {  
  7.     public delegate void MyEventHandler(float currentTime);  
  8.     #region 計時器的三種基礎事件  
  9.   
  10.     public static event MyEventHandler onTimerStart;  
  11.     public static event MyEventHandler onTimerPause;  
  12.     public static event MyEventHandler onTimerStoped;  
  13.  
  14.     #endregion  
  15.     private bool isStarted=false;  
  16.     public bool IsStarted{  
  17.         get{   
  18.             return isStarted;  
  19.         }  
  20.     }  
  21.     private bool isStoped = true;  
  22.     public bool IsStoped{  
  23.         get{   
  24.             return isStoped;  
  25.         }  
  26.     }  
  27.     private float totalTime = 0;  
  28.     // Update is called once per frame  
  29.     void Update () {  
  30.   
  31.         //空格鍵當作“開始/暫停”鍵  
  32.         if (Input.GetKeyDown (KeyCode.Space)) {  
  33.             OnChangeState ();  
  34.         }  
  35.         //Enter鍵當作“停止”鍵  
  36.         if (Input.GetKeyDown (KeyCode.Return)) {  
  37.             OnSetStop ();  
  38.         }  
  39.   
  40.         if (isStarted) {  
  41.             isStoped = false;  
  42.             totalTime += Time.deltaTime;  
  43.         }  
  44.     }  
  45.     void OnChangeState(){  
  46.         var _startState = !isStarted;  
  47.         isStarted = _startState;  
  48.         if (isStarted) {  
  49.             //檢查onTimerStart是否為空,防止報空 (廢話了。。。下面不做贅述)  
  50.             if (onTimerStart != null) {  
  51.                 onTimerStart (totalTime);  
  52.             } else {  
  53.                 Debug.Log ("onTimerStart is Empty");  
  54.             }  
  55.         } else {  
  56.             if (onTimerPause != null) {  
  57.                 onTimerPause (totalTime);  
  58.             } else {  
  59.                 Debug.Log ("onTimerPause is Empty");  
  60.             }  
  61.         }  
  62.     }  
  63.     void OnSetStop(){  
  64.         if (onTimerStoped != null) {  
  65.             onTimerStoped (totalTime);  
  66.         } else {  
  67.             Debug.Log ("onTimerStoped is Empty");  
  68.         }  
  69.         isStarted = false;  
  70.         isStoped = true;  
  71.         totalTime = 0;  
  72.     }  
  73. }  

然後,我們透過這三行程式碼就宣告瞭計時器的三種事件啦:

[csharp] view plain copy
  1. public static event MyEventHandler onTimerStart;  
  2. public static event MyEventHandler onTimerPause;  
  3. public static event MyEventHandler onTimerStoped;  
但是感覺還是不習慣哈,不要急,繼續往下看~


注意這裡:

[csharp] view plain copy
  1. var _startState = !isStarted;  
  2. isStarted = _startState;  
我為什麼要這樣進行賦值呢? 有人可能會說我多此一舉,但是我想解釋下的是,程式設計中切記不要編聰明的程式,可能你在編寫的時候很容易很簡單,但是等你開始維護你的專案的時候,你會發現非常棘手,你自己可能都看不懂你程式碼是什麼意思。

2、監聽者:

[csharp] view plain copy
  1. using UnityEngine;  
  2. using System.Collections;  
  3.   
  4. public class Listener : MonoBehaviour {  
  5.       
  6.     /// <summary>  
  7.     /// 註冊事件監聽  
  8.     /// </summary>  
  9.     void OnEnable () {  
  10.         Timer.onTimerPause += new Timer.MyEventHandler (OnTimerPause);  
  11.     }  
  12.     /// <summary>  
  13.     ///考慮到在有的需求裡,某個指令碼或者GameObject會重複啟用停用多次,  
  14.     /// 故在其停用的時候取消事件的註冊,以免此方法被重複呼叫   
  15.     /// </summary>  
  16.     void OnDisable(){  
  17.         Timer.onTimerPause -= new Timer.MyEventHandler (OnTimerPause);  
  18.     }  
  19.     public void OnTimerPause(float currentTime){  
  20.         Debug.Log ("[計時器暫停]當前計時:" + currentTime);  
  21.     }  
  22. }  
到這裡思路就已經很清晰明瞭,Timer作為事件的傳送者,Listener為其監聽者,依賴系統的功能廣播它的事件並呼叫“監聽”了這些事件的方法,這樣做就避免了上面提到問題,讓程式的耦合性降低了不少。

但是這樣還僅僅只是事件在專案的其中一種使用方式,大家也看出來了,感覺使用起來好像特別麻煩啊,每次使用一個事件都要先判斷事件為不為空,使用者還要使用“+=、-=”這樣的運算子,感覺理解起來好像也很複雜的樣子啊。

下面我們就要針對這個問題進行進一步最佳化啦,還沒緩過神來的朋友們先喝杯茶緩緩哈。

好,下面我們就要先解決下使用和理解複雜這個問題啦,所以我們先做點準備。

[csharp] view plain copy
  1. using UnityEngine;  
  2. using System.Collections;  
  3.   
  4. /// <summary>  
  5. ///存放介面、委託、結構體、列舉 等  
  6. /// </summary>  
  7. namespace MyResources{  
  8.     /// <summary>  
  9.     /// 基礎的委託  
  10.     /// </summary>  
  11.     public delegate void BaseEventHandler(float T);  
  12.     /// <summary>  
  13.     /// 基礎的事件介面  
  14.     /// </summary>  
  15.     public interface IBaseEvent{  
  16.         void AddListener (BaseEventHandler call);  
  17.         void RemoveListener (BaseEventHandler call);  
  18.         void TriggerEvent (float T);  
  19.     }  
  20.     /// <summary>  
  21.     ///計時器的事件結構體   
  22.     /// </summary>  
  23.     public struct TimerEvent:IBaseEvent{  
  24.         private BaseEventHandler timerEventHandler;  
  25.         /// <summary>  
  26.         /// 註冊此事件的監聽  
  27.         /// </summary>  
  28.         /// <param name="call">回撥函式.</param>  
  29.         public void AddListener(BaseEventHandler call){  
  30.             timerEventHandler += call;  
  31.         }  
  32.         /// <summary>  
  33.         /// 移除此事件的監聽  
  34.         /// </summary>  
  35.         /// <param name="call">回撥函式.</param>  
  36.         public void RemoveListener(BaseEventHandler call){  
  37.             timerEventHandler -= call;  
  38.         }  
  39.         /// <summary>  
  40.         ///觸發此事件  
  41.         /// </summary>  
  42.         public void TriggerEvent(float T){  
  43.             if (timerEventHandler != null) {  
  44.                 timerEventHandler (T);  
  45.             } else {  
  46.                 Debug.Log ("timerEventHandler is empty");  
  47.             }  
  48.         }  
  49.     }  
  50.     /// <summary>  
  51.     /// 高階的事件結構體  
  52.     /// 增加了註冊“一次性”回撥的入口  
  53.     /// </summary>  
  54.     public struct AdvanceEvent:IBaseEvent{  
  55.         private BaseEventHandler timerEventHandler;  
  56.         private BaseEventHandler removebleHandler;  
  57.         public void AddListener(BaseEventHandler call){  
  58.             timerEventHandler += call;  
  59.         }  
  60.         public void AddListener(BaseEventHandler call,CallbackOption option){  
  61.               
  62.             switch (option) {  
  63.             case CallbackOption.EvryTime:  
  64.                 timerEventHandler += call;  
  65.                 break;  
  66.             case CallbackOption.Once:  
  67.                 timerEventHandler += call;  
  68.                 removebleHandler += call;  
  69.                 break;  
  70.             }  
  71.         }  
  72.         public void RemoveListener(BaseEventHandler call){  
  73.             timerEventHandler -= call;  
  74.         }  
  75.   
  76.         public void TriggerEvent(float T){  
  77.             if (timerEventHandler != null) {  
  78.                 timerEventHandler (T);  
  79.                 timerEventHandler -= removebleHandler;  
  80.                 removebleHandler = null;  
  81.             } else {  
  82.                 Debug.Log ("timerEventHandler is empty");  
  83.             }  
  84.         }  
  85.     }  
  86.   
  87.     public enum CallbackOption{  
  88.         EvryTime,Once  
  89.     }  
  90. }  

準備完啦,上面呢就是宣告瞭一個事件所需要的委託,一個事件介面,和計時器基本的事件的結構體啦。之所以要宣告介面再讓計時器事件結構體繼承它呢就是為了規範所有包含事件的結構體的基本框架啦,這下不會有人吐槽說:啊呀,為什麼我感覺我的專案裡從來沒用過介面呢。。。  這下不就用上了嘛~

我猜你們肯定也注意到了怎麼感覺這個介面有點熟悉呢? 是不是在哪裡見過呢?  對! 沒錯!  它和 UnityEvent還是比較像的,大家可能經常會在使用Button元件的時候用到這個方法,長得都差不多啦,是不是感覺很好用? 

[csharp] view plain copy
  1. Button btn;  
  2. btn.OnClick.AddListener(Do);  
我們在TimerEvent中也宣告瞭AddListener(),用以更簡潔地註冊事件。其原理和上面那個是一樣的,只是進行了下封裝。相同的,在觸發事件上也進行了封裝,並檢測了是否有報空指標的問題,所以在之後使用中直接呼叫TriggerEvent()方法也更加放心啦,並且還有一個好處就是隱藏了其中的委託,也更加安全了。

好,準備工作做好了,我們現在把Timer的程式改改:

[csharp] view plain copy
  1. using UnityEngine;  
  2. using System.Collections;  
  3. using MyResources;  
  4.   
  5. public class NewTimer : MonoBehaviour {  
  6.     public delegate void MyEventHandler(float currentTime);  
  7.     #region 計時器的三種基礎事件  
  8.   
  9.     public static TimerEvent OnTimerStart;  
  10.     public static TimerEvent OnTimerPause;  
  11.     public static TimerEvent OnTimerStop;  
  12.     #endregion  
  13.     private bool isStarted=false;  
  14.     public bool IsStarted{  
  15.         get{   
  16.             return isStarted;  
  17.         }  
  18.     }  
  19.     private bool isStoped = true;  
  20.     public bool IsStoped{  
  21.         get{   
  22.             return isStoped;  
  23.         }  
  24.     }  
  25.     private float totalTime = 0;  
  26.     // Update is called once per frame  
  27.     void Update () {  
  28.   
  29.         //空格鍵當作“開始/暫停”鍵  
  30.         if (Input.GetKeyDown (KeyCode.Space)) {  
  31.             OnChangeState ();  
  32.         }  
  33.         //Enter鍵當作“停止”鍵  
  34.         if (Input.GetKeyDown (KeyCode.Return)) {  
  35.             OnSetStop ();  
  36.         }  
  37.   
  38.         if (isStarted) {  
  39.             isStoped = false;  
  40.             totalTime += Time.deltaTime;  
  41.         }  
  42.     }  
  43.     void OnChangeState(){  
  44.         var _startState = !isStarted;  
  45.         isStarted = _startState;  
  46.         if (isStarted) {  
  47.             OnTimerStart.TriggerEvent (totalTime);  
  48.         } else {  
  49.             OnTimerPause.TriggerEvent (totalTime);  
  50.         }  
  51.     }  
  52.     void OnSetStop(){  
  53.         OnTimerStop.TriggerEvent (totalTime);  
  54.         isStarted = false;  
  55.         isStoped = true;  
  56.         totalTime = 0;  
  57.     }  
  58. }  

這樣我們就可以直接宣告計時器的事件啦,是不是一看TimerEvent就知道是啥啦:

[csharp] view plain copy
  1. public static TimerEvent OnTimerStart;  
  2. public static TimerEvent OnTimerPause;  
  3. public static TimerEvent OnTimerStop;  

是不是超級簡單~

然後在事件的監聽上呢直接AddListener()就好了,觸發事件也只需要TriggerEvent()就好了,媽媽再也不用擔心我報空指標啦~


接下來就是更改監聽器的程式了,簡直不能再簡單了。

[csharp] view plain copy
  1. using UnityEngine;  
  2. using System.Collections;  
  3.   
  4. public class NewListener : MonoBehaviour {  
  5.   
  6.     // Use this for initialization  
  7.     void OnEnable () {  
  8.         NewTimer.OnTimerPause.AddListener (OnNewTimerPause);  
  9.     }  
  10.     void OnDisable(){  
  11.         NewTimer.OnTimerPause.RemoveListener (OnNewTimerPause);  
  12.     }  
  13.     void OnNewTimerPause (float currentTime) {  
  14.         Debug.Log ("[計時器暫停]當前計時:" + currentTime);  
  15.     }  
  16. }  

相關文章