Unity時鐘定時器插件——Vision Timer源碼分析之一

 

By D.S.Qiu

尊重他人的勞動,支持原創,轉載請註明出處:http.dsqiu.iteye.com

 

       因爲項目中,UI的所有模塊都沒有MonBehaviour類(純粹的C#類),只有像NGUI的基本組件的類是繼承MonoBehaviour。因爲沒有繼承MonoBehaviour,這也不能使用Update,InVoke,StartCoroutine等方法,這樣就會顯得很蹩腳。後來一個同事添加vp_Timer和vp_TimeUtility這兩個類。後來研究了下vp_Timer至少可以彌補沒有Update,InVoke,InVokeRepeat的不足。

       之前用的時候,就粗略的研究過vp_Timer這個類,一直就想仔細剖析下,但是由於不僅工作任務中,自己也有很多要瑣碎的事情,我都很久自己好好學習了,雖然還有一堆事情要做,但是憋了太久了,先滿足下自己,所以纔有開始認真分析vp_Timer的代碼並纔有這篇博客。

       

       vp_Timer 一共有3個class,都各司其職:vp_Timer,Event,Handle

                 1)vp_Timer:提供的使用接口,通過靜態方法vp_Timer.In(),加入定時器事件(函數,這裏將傳入的函數稱爲事件)

                 2)Event:用來封裝傳入的事件(函數),保持事件的狀態

                 3)Handle:對事件狀態提供查詢接口(事件執行了多長時間,結束時間,是否還是Active)以及提供 Excute(立即執行事件),Cancel(取消事件),Pause(暫停事件)等操作

       很容易就可以理清這三者的關係,通過vp_Timer.In方法將傳入的事件(函數)封裝爲Event對象,然後返回(雖然是通過參數)Handle對象讓調用者可以查詢事件的狀態和進行相關操作。

       

vp_Timer:

       先看下vp_Timer的成員變量(c#應該稱爲filed)

GameObject m_MainObject:vp_Timer是一個MonoBehaviour,就一定要掛載GameObject上,m_MainObject會在第一次調用vp_Timer.In方法時創建:

C#代碼  收藏代碼
  1. // setup main gameobject  
  2. if (m_MainObject == null)  
  3. {  
  4.     m_MainObject = new GameObject("Timers");  
  5.     m_MainObject.AddComponent<vp_Timer>();  
  6.     UnityEngine.Object.DontDestroyOnLoad(m_MainObject);  
  7.  
  8. #if (UNITY_EDITOR && !DEBUG)  
  9.     m_MainObject.gameObject.hideFlags = HideFlags.HideInHierarchy;  
  10. #endif  
  11. }  

 

 

 List<Event> m_Active 和 List<Event> m_Pool :這個List都是Event的緩存,其中,m_Active緩存Active的Event,m_Pool緩存無效的Event,這裏的Acitive是事件仍然需要執行,無效說明不會再被調用。之所有要緩存無效的Event,是爲了節省創建Event對象的消耗。m_Pool就好比垃圾箱,m_Active是一個成品工廠,每次m_Active要生產(Add)新的Event,都去m_Pool取沒用的原料(Event),當m_Active的成品沒用了,用放會m_Pool中去,這樣就達到了循環利用作用。

 

 Event m_NewEvent :在Schedule方法裏使用的變量,其實完全可以聲明爲Schedule的局部變量,爲了節省重複創建和銷燬的消耗,vp_Timer就聲明一個成員變量。

private static int m_EventCount = 0;

 

// variables for the Update method

int m_EventBatch 和int m_EventIterator:在Update使用的變量,m_EventBatch記錄在一次Update中執行事件的次數,m_EventIterator記錄是每次執行事件在m_Active的索引。

 

int MaxEventsPerFrame :一次循環(Update)執行事件最大次數

 

       假設MaxEventPerFarme = 10 , m_Active.Count = 5,那麼每次Update都會遍歷2次m_Active的Event,看是否可以執行(調用Excute函數)。這樣就可以理解這三個參數的具體含義了。

 

Event:

 

C#代碼  收藏代碼
  1.     private class Event  
  2.     {  
  3.   
  4.         public int Id;   //標記Event,如果Id = 0 ,表示該Event已經無效,就被Add進m_Pool中,Handle對象和Evnt就是通過Id來關聯的  
  5.   
  6.         public Callback Function = null;   //函數委託 Callback和ArgCallback是vp.Timer定義的函數委託(原型)  
  7.         public ArgCallback ArgFunction = null;  
  8.         public object Arguments = null;  
  9.   
  10.         public int Iterations = 1;   //事件的迭代(執行次數)  
  11.         public float Interval = -1.0f;   //執行時間間隔  
  12.         public float DueTime = 0.0f;    //下一個事件執行的時間 DueTime = Time.time + Time.deltaTime  
  13.         public float StartTime = 0.0f;   //事件開始執行事件 StartTime = Time.Time + delayTime  
  14.         public float LifeTime = 0.0f;   //事件累積的總時間 LifeTime += Time.deltaTime  
  15.         public bool Paused = false;  
  16.  
  17. #if (DEBUG && UNITY_EDITOR)  
  18.         private string m_CallingMethod = "";  
  19. #endif  
  20.                //省略其他代碼  
  21.        }  

 

 

當然還有幾個方法:

       Excute():執行Function和ArgFunction

       Recyle():

C#代碼  收藏代碼
  1. private void Recycle()  
  2. {  
  3.   
  4.     Id = 0;  
  5.     DueTime = 0.0f;  
  6.     StartTime = 0.0f;  
  7.   
  8.     Function = null;  
  9.     ArgFunction = null;  
  10.     Arguments = null;  
  11.   
  12.     if (vp_Timer.m_Active.Remove(this))  //從m_Active進入m_Pool  
  13.         m_Pool.Add(this);  
  14.  
  15. #if (UNITY_EDITOR && DEBUG)  
  16.     EditorRefresh();  
  17. #endif  
  18.   
  19. }  

         MethodName:由於D.S.Qiu對delegate還沒有深入研究理解,目前還說不清如何比較兩個委託是否相等,但是得到一個經驗就是不能用 函數 來比較,所以看到很多插件(最典型的就是Unity的StopCoroutine只有字符串作爲參數和NGUI的EventDelegate)都使用的字符串來標記delegate,看下面的代碼:

C#代碼  收藏代碼
  1. public string MethodName  
  2. {  
  3.     get  
  4.     {  
  5.         if (Function != null)  
  6.         {  
  7.             if (Function.Method != null)  
  8.             {  
  9.                 if (Function.Method.Name[0] == '<')  
  10.                     return "delegate";  
  11.                 else return Function.Method.Name;  
  12.             }  
  13.         }  
  14.         else if (ArgFunction != null)  
  15.         {  
  16.             if (ArgFunction.Method != null)  
  17.             {  
  18.                 if (ArgFunction.Method.Name[0] == '<')  
  19.                     return "delegate";  
  20.                 else return ArgFunction.Method.Name;  
  21.             }  
  22.         }  
  23.         return null;  
  24.     }  
  25. }  

 這樣vp_Timer纔有Cancel(string methodName)的方法:

C#代碼  收藏代碼
  1. public static void CancelAll(string methodName)  
  2. {  
  3.     for (int t = vp_Timer.m_Active.Count - 1; t > -1; t--)  
  4.     {  
  5.         if (vp_Timer.m_Active[t].MethodName == methodName)  
  6.             vp_Timer.m_Active[t].Id = 0;  
  7.     }  
  8. }  

 

Handle:

        前面介紹過,Handle是用來查詢和操作Event的對象,Handle對象和Event桶Id關聯起來。

C#代碼  收藏代碼
  1. public int Id  
  2. {  
  3.     get  
  4.     {  
  5.         return m_Id;  
  6.     }  
  7.     set  
  8.     {  
  9.         m_Id = value;  
  10.   
  11.         if (m_Id == 0)  
  12.         {  
  13.             m_Event.DueTime = 0.0f;  
  14.             return;  
  15.         }  
  16.   
  17.         m_Event = null;  
  18.         for (int t = vp_Timer.m_Active.Count - 1; t > -1; t--)  
  19.         {  
  20.             if (vp_Timer.m_Active[t].Id == m_Id)  
  21.             {  
  22.                 m_Event = vp_Timer.m_Active[t];  
  23.                 break;  
  24.             }  
  25.         }  
  26.         if (m_Event == null)  
  27.             UnityEngine.Debug.LogError("Error: (vp_Timer.Handle) Failed to assign event with Id '" + m_Id + "'.");  
  28.   
  29.         // store some initial event info  
  30.         m_StartIterations = m_Event.Iterations;  
  31.         m_FirstDueTime = m_Event.DueTime;  
  32.   
  33.     }  
  34. }  

 

 還說vp_Timer

        前面介紹了vp_Timer的成員變量以及Event和Handle,就差vp_Timer的使用了,通過調用vp_Timer.In函數將事件加入vp_Timer的mActive隊列:

C#代碼  收藏代碼
  1. // time + callback + [timer handle]  
  2.     public static void In(float delay, Callback callback, Handle timerHandle = null)  
  3.     { Schedule(delay, callback, nullnull, timerHandle, 1, -1.0f); }  
  4.   
  5.     // time + callback + iterations + [timer handle]  
  6.     public static void In(float delay, Callback callback, int iterations, Handle timerHandle = null)  
  7.     { Schedule(delay, callback, nullnull, timerHandle, iterations, -1.0f); }  
  8.   
  9.     // time + callback + iterations + interval + [timer handle]  
  10.     public static void In(float delay, Callback callback, int iterations, float interval, Handle timerHandle = null)  
  11.     { Schedule(delay, callback, nullnull, timerHandle, iterations, interval); }  
  12.   
  13.     // time + callback + arguments + [timer handle]  
  14.     public static void In(float delay, ArgCallback callback, object arguments, Handle timerHandle = null)  
  15.     { Schedule(delay, null, callback, arguments, timerHandle, 1, -1.0f); }  
  16.   
  17.     // time + callback + arguments + iterations + [timer handle]  
  18.     public static void In(float delay, ArgCallback callback, object arguments, int iterations, Handle timerHandle = null)  
  19.     { Schedule(delay, null, callback, arguments, timerHandle, iterations, -1.0f); }  
  20.   
  21.     // time + callback + arguments + iterations + interval + [timer handle]  
  22.     public static void In(float delay, ArgCallback callback, object arguments, int iterations, float interval, Handle timerHandle = null)  
  23.     { Schedule(delay, null, callback, arguments, timerHandle, iterations, interval); }  

 看到都是對Schedule的封裝:

C#代碼  收藏代碼
  1. private static void Schedule(float time, Callback func, ArgCallback argFunc, object args, Handle timerHandle, int iterations, float interval)  
  2. {  
  3. if (func == null && argFunc == null)  
  4. {  
  5.     UnityEngine.Debug.LogError("Error: (vp_Timer) Aborted event because function is null.");  
  6.     return;  
  7. }  
  8. // setup main gameobject  
  9. if (m_MainObject == null)  //new 一個 m_MainObject,掛載vp_Timer_  
  10. {  
  11.     m_MainObject = new GameObject("Timers");  
  12.     m_MainObject.AddComponent<vp_Timer>();  
  13.     UnityEngine.Object.DontDestroyOnLoad(m_MainObject);  
  14.  
  15. #if (UNITY_EDITOR && !DEBUG)  
  16.         m_MainObject.gameObject.hideFlags = HideFlags.HideInHierarchy;  
  17. #endif  
  18. }  
  19.   
  20. // force healthy time values  
  21. time = Mathf.Max(0.0f, time);  
  22. iterations = Mathf.Max(0, iterations);  
  23. interval = (interval == -1.0f) ? time : Mathf.Max(0.0f, interval);  
  24.   
  25. // recycle an event - or create a new one if the pool is empty:先從m_Pool中去Event,如果m_Pool爲空則直接new一個  
  26. m_NewEvent = null;  
  27. if (m_Pool.Count > 0)     
  28. {   
  29.     m_NewEvent = m_Pool[0];  
  30.     m_Pool.Remove(m_NewEvent);  
  31. }  
  32. else  
  33.     m_NewEvent = new Event();  
  34.   
  35. // iterate the event counter and store the id for this event  
  36. vp_Timer.m_EventCount++;  
  37. m_NewEvent.Id = vp_Timer.m_EventCount;   //Event的Id爲當前隊列的總數  
  38.   
  39. // set up the event with its function, arguments and times  
  40. if (func != null)  
  41.     m_NewEvent.Function = func;  
  42. else if (argFunc != null)  
  43. {  
  44.     m_NewEvent.ArgFunction = argFunc;  
  45.     m_NewEvent.Arguments = args;  
  46. }  
  47. m_NewEvent.StartTime = Time.time;  //設置Event的成員變量  
  48. m_NewEvent.DueTime = Time.time + time;  
  49. m_NewEvent.Iterations = iterations;  
  50. m_NewEvent.Interval = interval;  
  51. m_NewEvent.LifeTime = 0.0f;  
  52. m_NewEvent.Paused = false;  
  53.   
  54. // add event to the Active list  
  55. vp_Timer.m_Active.Add(m_NewEvent);  
  56.   
  57. // if a timer handle was provided, associate it to this event,  
  58. // but first cancel any already active event using the same  
  59. // handle: there can be only one ...  
  60. if (timerHandle != null)  
  61. {  
  62.     if (timerHandle.Active)  
  63.         timerHandle.Cancel();  
  64.     // setting the 'Id' property associates this handle with  
  65.     // the currently active event with the corresponding id  
  66.     timerHandle.Id = m_NewEvent.Id;   //關聯Handle和Event,然後Handle就可以通過關聯的Event查詢其狀態和操作  
  67. }  

 Update:通過比對Time.time和Event.DueTime,如果Time.time >= Event.DueTime 則執行Event的Excute方法。

C#代碼  收藏代碼
  1. private void Update()  
  2. {  
  3.   
  4.     //  NOTE: this method never processes more than 'MaxEventsPerFrame',  
  5.     // in order to avoid performance problems with excessive amounts of  
  6.     // timers. this may lead to events being delayed a few frames.  
  7.     // if experiencing delayed events 1) try to cut back on the amount  
  8.     // of timers created simultaneously, and 2) increase 'MaxEventsPerFrame'  
  9.   
  10.     // execute any active events that are due, but only check  
  11.     // up to max events per frame for performance  
  12.     m_EventBatch = 0;  
  13.     while ((vp_Timer.m_Active.Count > 0) && m_EventBatch < MaxEventsPerFrame)  
  14.     {  
  15.   
  16.         // if we reached beginning of list, halt until next frame  
  17.         if (m_EventIterator < 0)  
  18.         {  
  19.             // this has two purposes: 1) preventing multiple iterations  
  20.             // per frame if our event count is below the maximum, and  
  21.             // 2) preventing reaching index -1  
  22.             m_EventIterator = vp_Timer.m_Active.Count - 1;  
  23.             break;  
  24.         }  
  25.   
  26.         // prevent index out of bounds  
  27.         if (m_EventIterator > vp_Timer.m_Active.Count - 1)  
  28.             m_EventIterator = vp_Timer.m_Active.Count - 1;  
  29.   
  30.         // execute all due events  
  31.         if (Time.time >= vp_Timer.m_Active[m_EventIterator].DueTime ||   // time is up  
  32.             vp_Timer.m_Active[m_EventIterator].Id == 0)                 // event has been canceled ('Execute' will kill it)  
  33.             vp_Timer.m_Active[m_EventIterator].Execute();  
  34.         else  
  35.         {  
  36.             // handle pausing  
  37.             if (vp_Timer.m_Active[m_EventIterator].Paused)  
  38.                 vp_Timer.m_Active[m_EventIterator].DueTime += Time.deltaTime;  
  39.             else  
  40.                 // log lifetime  
  41.                 vp_Timer.m_Active[m_EventIterator].LifeTime += Time.deltaTime;  
  42.         }  
  43.   
  44.         // going backwards since 'Execute' will remove items from the list  
  45.         m_EventIterator--;  
  46.         m_EventBatch++;  
  47.     }  
  48.   
  49. }  

 

        差不多就這樣了,很久沒寫博客了,感覺一點也不順暢,雖然理解的很透徹,還是很爲自己的寫作功底捉急。

        

小結:

       D.S.Qiu覺得在項目中很有必要有“管理”的思想,很多功能都是用一個類實現的,其他人只要調用就可以了,具體的邏輯只需要在一個類內部維護,可以做的統一控制,可以做到更自如,就拿vp_Timer和MonoBehaviour的InVokeRepeat方法來對比就有明顯的優勢:

               1)vp_Timer可以隨時查詢事件的狀態(事件被執行了次數等)還可以暫停事件,而InVokeRepeat做不到的

               2)vp_Timer可以設置時間delatTime受不受Time.timeScale影響,而InVokeRepeat是沒有這個參數設置的

               3)vp_Timer可以對事件進行統一的管理,如果暫停所有事件的執行,這個點當Time.timeScale = 0 時特別管用,而InVokeRepeat是分散的,沒有統一管理其他。

 這有點“一夫當萬夫莫開”的感覺。

       不管是InVokeRepeat方法,MonoBehaviour的很多方法都有類似的缺陷,因爲每一個MonoBehaviour都可以調用這些方法,就不能統一起來管理了,所以如果Unity當初能寫一個專門的類我想會方便很多。

 

       雖然覺得vp_Timer用的很爽,但是D.S.Qiu還是覺得有很多可以改進的地方:

              1)vp_Timer提供Pause(string methodName)和PauseAll()的方法,從“管理”的角度上就更加完美了,當然還有對應的Play方法。

              2)當Event的參數: Iterations 和  Interval 沒有很好處理 Interval 和 Time.deltaTime 的具體情況,假設我們的 Iterations =100 , interval = 0.01f  即我需要達到1s內執行100次的目的,但按照vp_Timer的實現結果是執行了100次,但是時間一定是>= 1s,即當Time.deltaTime > interval 時,還是隻執行一次,例如 Time.deltaTime = 0.02f, 理論上我們希望能執行兩次,但是卻只執行了一次。

             3)vp_Timer要是提供 string methodName 到 Event 或 Handle 的查詢接口就更加完美了。

             4)vp_Timer雖然用了很多設計,對象的重複利用避免 new 和銷燬對象的系統開銷,但是專門用Handle專門管理Event,Handle的的功能只是對Event的一個封裝,其實完全沒有必要,完全可以讓Event自己充當Handle的角色,直接返回Event對象會更直觀,只有在回調的時候用參數返回關聯的對象,要不然採用直接返回會更明白。

 

       雖然上面的分析文章寫得比較零亂,但是小結部分我還是很滿意的,至少D.S.Qiu之前從來沒有在這部分寫那麼多,算是分享自己的一些經驗和體會吧,也發現自己對delegate的不足,又到1:30了時間真是不夠用,4個小時就這麼過去了。

 

 

       如果您對D.S.Qiu有任何建議或意見可以在文章後面評論,或者發郵件([email protected])交流,您的鼓勵和支持是我前進的動力,希望能有更多更好的分享。

       轉載請在文首註明出處:http://dsqiu.iteye.com/blog/2020603

更多精彩請關注D.S.Qiu的博客和微博(ID:靜水逐風)

發佈了53 篇原創文章 · 獲贊 13 · 訪問量 11萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章