【Unity】中如何統一管理回調函數(利用觀察者模式)

這次的內容有點類似設計模式裏的觀察者模式。但是和常規意義上的觀察者模式也不是完全一致,所以各位就不要咬文嚼字啦!咦?設計模式?!不懂!沒關係,說不定你以前就用過。


開場白


我們來想象一個場景。在加載一個模型時,你需要從網上下載,但是你並不知道下載需要花費多少時間。你所知道的是,當下載完成後,就可以把模型放在特定位置上,開始遊戲。那麼,我們怎樣才能判斷下載完成呢?
一個簡單的方法是,在每一幀的時候都判斷下載是否完成,完成後就可以繼續後面的工作。因此,我們可以這樣做,我們告訴一個管理器,嗨,你幫我盯着點,看下載完了沒有,完了就叫我一聲,好讓我執行XXX函數。我們今天要做的,就是構造這樣一個管理器。


實現

注意,下面的代碼依賴於之前所講到的單例模式

我們不防把上面這樣一件工作成爲一個計數器——Timer(這個名字可能不太恰當),把需要被通知者成爲觀察者——Oberver,而像下載管理器這樣的對象成爲一個主題——Subject。

首先,我們來定義觀察者和主題對象。TimerObserverOrSubject.cs如下:
[csharp] view plain copy
 print?
  1. using UnityEngine;  
  2. using System.Collections;  
  3.   
  4. public class TimerObserverOrSubject : MonoBehaviour {  
  5.       
  6.     virtual protected void OnDestroy ()  
  7.     {  
  8.         if(Singleton.IsCreatedInstance("TimerController"))  
  9.         {  
  10.             (Singleton.getInstance("TimerController"as TimerController).ClearTimer(this);  
  11.         }  
  12.     }  
  13. }  


TimerObserverOrSubject.cs的內容非常簡單,它的工作就是在該腳本被析構時,及時地從計數器管理器裏面刪除涉及這個對象的所有Timer。

計數器管理器的腳本——TimerController.cs如下:
[csharp] view plain copy
 print?
  1. using UnityEngine;  
  2. using System.Collections;  
  3. using System.Collections.Generic;  
  4.    
  5. public class TimerController : MonoBehaviour {  
  6.       
  7.     public delegate void OnCallBack(object arg);  
  8.     public delegate bool OnIsCanDo(object arg);  
  9.       
  10.     public class Timer {  
  11.         public TimerObserverOrSubject m_Observer;  
  12.         public OnCallBack m_Callback = null;  
  13.         public object m_Arg = null;  
  14.           
  15.         public TimerObserverOrSubject m_Subject;  
  16.         public OnIsCanDo m_IsCanDoFunc = null;   
  17.         public object m_ArgForIsCanDoFunc = null;  
  18.           
  19.         public float m_PassTime = 0;  
  20.           
  21.         public Timer(TimerObserverOrSubject observer, OnCallBack callback, object arg,   
  22.             TimerObserverOrSubject subject, OnIsCanDo isCanDoFunc, object argForIsCanDo) {  
  23.             m_Observer = observer;  
  24.             m_Callback = callback;  
  25.             m_Arg = arg;  
  26.                
  27.             m_Subject = subject;  
  28.             m_IsCanDoFunc = isCanDoFunc;  
  29.             m_ArgForIsCanDoFunc = argForIsCanDo;  
  30.               
  31.             m_PassTime = 0;  
  32.                 }  
  33.           
  34.         public Timer(TimerObserverOrSubject observer, OnCallBack callback, object arg, float time) {  
  35.             m_Observer = observer;  
  36.             m_Callback = callback;  
  37.             m_Arg = arg;  
  38.               
  39.             m_Subject = null;  
  40.             m_IsCanDoFunc = null;  
  41.             m_ArgForIsCanDoFunc = null;  
  42.               
  43.             m_PassTime = time;  
  44.         }  
  45.         }  
  46.     private List<Timer> m_Timers = new List<Timer>();  
  47.     private List<Timer> m_NeedRemoveTimer = new List<Timer>();  
  48.     private List<Timer> m_CurRunTimer = new List<Timer>();  
  49.        
  50.     /// <summary>  
  51.     /// Sets the timer.  
  52.     /// </summary>  
  53.     /// <param name='observer'>  
  54.     /// The TimerObserverOrSubject you need to listen  
  55.     /// </param>  
  56.     /// <param name='callback'>  
  57.     /// The callback when condition is true.  
  58.     /// </param>  
  59.     /// <param name='arg'>  
  60.     /// Argument of the callback.  
  61.     /// </param>  
  62.     /// <param name='observer'>  
  63.     /// The TimerObserverOrSubject you need to observe  
  64.     /// </param>  
  65.     /// <param name='isCanDoFunc'>  
  66.     /// The condition function, must return a boolean.  
  67.     /// </param>  
  68.     /// <param name='argForIsCanDo'>  
  69.     /// Argument for condition function.  
  70.     /// </param>  
  71.     public void SetTimer(TimerObserverOrSubject observer, OnCallBack callback ,object arg,  
  72.         TimerObserverOrSubject subject, OnIsCanDo isCanDoFunc,object argForIsCanDo) {  
  73.         if (observer == null || subject == null || callback == null || isCanDoFunc == nullreturn;  
  74.           
  75.         if (isCanDoFunc(argForIsCanDo)) {  
  76.             callback(arg);  
  77.             return;  
  78.         }  
  79.           
  80.         Timer timer = new Timer(observer, callback, arg, subject, isCanDoFunc, argForIsCanDo);       
  81.         m_Timers.Add(timer);  
  82.     }  
  83.        
  84.     /// <summary>  
  85.     /// Sets the timer.  
  86.     /// </summary>  
  87.     /// <param name='observer'>  
  88.     /// The TimerObserverOrSubject you need to listen  
  89.     /// </param>  
  90.     /// <param name='callback'>  
  91.     /// The callback when time is up.  
  92.     /// </param>  
  93.     /// <param name='arg'>  
  94.     /// Argument of the callback.  
  95.     /// </param>  
  96.     /// <param name='timepass'>  
  97.     /// Timepass before calling the callback.  
  98.     /// </param>  
  99.     public void SetTimer(TimerObserverOrSubject observer, OnCallBack callback , object arg, float timepass) {  
  100.         if (observer != null && callback != null) {             
  101.             Timer timer = new Timer(observer, callback, arg, timepass);  
  102.             m_Timers.Add(timer);  
  103.         }  
  104.     }  
  105.        
  106.     /// <summary>  
  107.     /// Clears all Timers of the observer.  
  108.     /// </summary>  
  109.     /// <param name='observer'>  
  110.     /// The TimerObserverOrSubject you need to clear  
  111.     /// </param>  
  112.     public void ClearTimer(TimerObserverOrSubject observer) {  
  113.         List<Timer> needRemovedTimers = new List<Timer>();  
  114.           
  115.         foreach (Timer timer in m_Timers) {  
  116.             if (timer.m_Observer == observer || timer.m_Subject) {  
  117.                 needRemovedTimers.Add(timer);  
  118.             }  
  119.         }  
  120.           
  121.         foreach (Timer timer in needRemovedTimers) {  
  122.             m_Timers.Remove(timer);  
  123.         }  
  124.     }  
  125.        
  126.         // Update is called once per frame  
  127.         void Update ()   
  128.     {  
  129.         InitialCurTimerDict();  
  130.         RunTimer();  
  131.         RemoveTimer();  
  132.         }  
  133.       
  134.     private void InitialCurTimerDict() {  
  135.         m_CurRunTimer.Clear();  
  136.           
  137.         foreach (Timer timer in m_Timers) {  
  138.             m_CurRunTimer.Add(timer);  
  139.         }  
  140.     }  
  141.       
  142.     private void RunTimer() {  
  143.         m_NeedRemoveTimer.Clear();  
  144.           
  145.         foreach (Timer timer in m_CurRunTimer) {          
  146.             if (timer.m_IsCanDoFunc == null) {  
  147.                 timer.m_PassTime =  timer.m_PassTime - Time.deltaTime;  
  148.                 if (timer.m_PassTime < 0) {  
  149.                     timer.m_Callback(timer.m_Arg);  
  150.                     m_NeedRemoveTimer.Add(timer);  
  151.                 }  
  152.             } else {  
  153.                 if (timer.m_IsCanDoFunc(timer.m_ArgForIsCanDoFunc)) {  
  154.                     timer.m_Callback(timer.m_Arg);  
  155.                     m_NeedRemoveTimer.Add(timer);  
  156.                 }  
  157.             }     
  158.         }  
  159.     }  
  160.       
  161.     private void RemoveTimer() {  
  162.         foreach (Timer timer in m_NeedRemoveTimer) {  
  163.             if (m_Timers.Contains(timer)) {  
  164.                 m_Timers.Remove(timer);  
  165.             }  
  166.         }  
  167.     }  
  168.   
  169. }  

首先,它定義了回調函數的類型:
[csharp] view plain copy
 print?
  1. public delegate void OnCallBack(object arg);  
  2. public delegate bool OnIsCanDo(object arg);  

關於C#的委託機制,如果有童鞋不瞭解,請詳見官方文檔。簡單來說,委託類似一個函數指針,常被用於回調函數。

然後,定義了一個數據類型Timer用於保存一個計數器的各個信息。

接下來,就是TimerController的兩個重要的SetTimer函數。我們先看第一個SetTimer函數:
[csharp] view plain copy
 print?
  1. /// <summary>  
  2. /// Sets the timer.  
  3. /// </summary>  
  4. /// <param name='observer'>  
  5. /// The observer to observe the subject  
  6. /// </param>  
  7. /// <param name='callback'>  
  8. /// The callback when condition is true.  
  9. /// </param>  
  10. /// <param name='arg'>  
  11. /// Argument of the callback.  
  12. /// </param>  
  13. /// <param name='subject'>  
  14. /// The subject you need to observe  
  15. /// </param>  
  16. /// <param name='isCanDoFunc'>  
  17. /// The condition function, must return a boolean.  
  18. /// </param>  
  19. /// <param name='argForIsCanDo'>  
  20. /// Argument for condition function.  
  21. /// </param>  
  22. public void SetTimer(TimerObserverOrSubject observer, OnCallBack callback ,object arg,  
  23.     TimerObserverOrSubject subject, OnIsCanDo isCanDoFunc,object argForIsCanDo) {  
  24.     if (observer == null || subject == null || callback == null || isCanDoFunc == nullreturn;  
  25.       
  26.     if (isCanDoFunc(argForIsCanDo)) {  
  27.         callback(arg);  
  28.         return;  
  29.        }  
  30.       
  31.     Timer timer = new Timer(observer, callback, arg, subject, isCanDoFunc, argForIsCanDo);       
  32.     m_Timers.Add(timer);  
  33. }  


根據函數說明可以看出,它負責建立一個計數器,當subject的isCanDoFunc(argForIsCanDo)函數返回true時,通知observer,執行observer的callback(arg)函數。

第二個SetTimer函數更簡單:
[csharp] view plain copy
 print?
  1. /// <summary>  
  2. /// Sets the timer.  
  3. /// </summary>  
  4. /// <param name='observer'>  
  5. /// The observer to observe the subject  
  6. /// </param>  
  7. /// <param name='callback'>  
  8. /// The callback when time is up.  
  9. /// </param>  
  10. /// <param name='arg'>  
  11. /// Argument of the callback.  
  12. /// </param>  
  13. /// <param name='timepass'>  
  14. /// Timepass before calling the callback.  
  15. /// </param>  
  16. public void SetTimer(TimerObserverOrSubject observer, OnCallBack callback , object arg, float timepass) {  
  17.     if (observer != null && callback != null) {             
  18.         Timer timer = new Timer(observer, callback, arg, timepass);  
  19.         m_Timers.Add(timer);  
  20.     }  
  21. }  

它負責建立一個計數器,在timepass的時間後,通知observer,執行observer的callback(arg)函數。

Update()函數裏面負責檢查所有Timer是否可以觸發以及是否需要刪除。


例子



在這個例子裏,我們需要在程序開始運行5秒後,打印一些信息。當然這個的實現有很多方法,這裏我們使用今天實現的TimerController來實現。

TimerSample.cs的內容如下:
[csharp] view plain copy
 print?
  1. using UnityEngine;  
  2. using System.Collections;  
  3.   
  4. public class TimerSample : TimerObserverOrSubject {  
  5.       
  6.     private TimerController m_TimerCtr = null;  
  7.       
  8.     private bool m_IsCanDisplay = false;  
  9.       
  10.     private string m_DisplayContent = "Hello, candycat!";  
  11.       
  12.     // Use this for initialization  
  13.     void Start () {  
  14.         m_TimerCtr = Singleton.getInstance("TimerController"as TimerController;  
  15.           
  16.         //m_TimerCtr.SetTimer(this, Display, m_DisplayContent, 5);  
  17.           
  18.         m_TimerCtr.SetTimer(this, Display, nullthis, IsCanDisplay, null);  
  19.           
  20.         StartCoroutine(DelayDisplay());  
  21.     }  
  22.       
  23.     void Display(object arg) {  
  24.         if (arg == null) {  
  25.             Debug.Log(m_DisplayContent);  
  26.         } else {  
  27.             string content = arg as string;  
  28.           
  29.             Debug.Log(content);  
  30.         }  
  31.     }  
  32.       
  33.     bool IsCanDisplay(object arg) {  
  34.         return m_IsCanDisplay;  
  35.     }  
  36.       
  37.     IEnumerator DelayDisplay() {  
  38.         yield return new WaitForSeconds(5.0f);  
  39.           
  40.         m_IsCanDisplay = true;  
  41.     }  
  42.       
  43.     // Update is called once per frame  
  44.     void Update () {  
  45.       
  46.     }  
  47. }  

首先,它向TimerController請求註冊了一個計時器。這裏,它的條件是IsCanDisplay函數,它返回bool值m_IsCanDisplay。而這個值將會在5秒後,通過協同函數DelayDisplay來由false置爲true。當其爲true時,TimerController就將通知TimerSample調用Display函數。

我們將第16行代碼註釋解開,並將18-20行代碼註釋掉,則可以達到相同的效果。


結束語


C#的委託機制還是非常常用的,使用Unity的童鞋最好還是瞭解一下。關於TimerController的執行效率,由於它是每一幀都要去判斷所有的condition函數,所以應當讓condition函數中的邏輯儘可能簡單。

好了,這次就到這裏,如果有更好的想法,或者這裏的代碼有什麼問題,都非常歡迎指正。謝謝閱讀!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章