首页 > 学院 > 开发设计 > 正文

【Unity】中如何统一管理回调函数(利用观察者模式)

2019-11-09 15:42:57
字体:
来源:转载
供稿:网友

这次的内容有点类似设计模式里的观察者模式。但是和常规意义上的观察者模式也不是完全一致,所以各位就不要咬文嚼字啦!咦?设计模式?!不懂!没关系,说不定你以前就用过。

开场白

我们来想象一个场景。在加载一个模型时,你需要从网上下载,但是你并不知道下载需要花费多少时间。你所知道的是,当下载完成后,就可以把模型放在特定位置上,开始游戏。那么,我们怎样才能判断下载完成呢?一个简单的方法是,在每一帧的时候都判断下载是否完成,完成后就可以继续后面的工作。因此,我们可以这样做,我们告诉一个管理器,嗨,你帮我盯着点,看下载完了没有,完了就叫我一声,好让我执行XXX函数。我们今天要做的,就是构造这样一个管理器。

实现

注意,下面的代码依赖于之前所讲到的单例模式。我们不防把上面这样一件工作成为一个计数器——Timer(这个名字可能不太恰当),把需要被通知者成为观察者——Oberver,而像下载管理器这样的对象成为一个主题——Subject。首先,我们来定义观察者和主题对象。TimerObserverOrSubject.cs如下:[csharp] view plain copy PRint?using UnityEngine;  using System.Collections;    public class TimerObserverOrSubject : MonoBehaviour {            virtual protected void OnDestroy ()      {          if(Singleton.IsCreatedInstance("TimerController"))          {              (Singleton.getInstance("TimerController") as TimerController).ClearTimer(this);          }      }  }  TimerObserverOrSubject.cs的内容非常简单,它的工作就是在该脚本被析构时,及时地从计数器管理器里面删除涉及这个对象的所有Timer。计数器管理器的脚本——TimerController.cs如下:[csharp] view plain copy print?using UnityEngine;  using System.Collections;  using System.Collections.Generic;     public class TimerController : MonoBehaviour {            public delegate void OnCallBack(object arg);      public delegate bool OnIsCanDo(object arg);            public class Timer {          public TimerObserverOrSubject m_Observer;          public OnCallBack m_Callback = null;          public object m_Arg = null;                    public TimerObserverOrSubject m_Subject;          public OnIsCanDo m_IsCanDoFunc = null;           public object m_ArgForIsCanDoFunc = null;                    public float m_PassTime = 0;                    public Timer(TimerObserverOrSubject observer, OnCallBack callback, object arg,               TimerObserverOrSubject subject, OnIsCanDo isCanDoFunc, object argForIsCanDo) {              m_Observer = observer;              m_Callback = callback;              m_Arg = arg;                             m_Subject = subject;              m_IsCanDoFunc = isCanDoFunc;              m_ArgForIsCanDoFunc = argForIsCanDo;                            m_PassTime = 0;                  }                    public Timer(TimerObserverOrSubject observer, OnCallBack callback, object arg, float time) {              m_Observer = observer;              m_Callback = callback;              m_Arg = arg;                            m_Subject = null;              m_IsCanDoFunc = null;              m_ArgForIsCanDoFunc = null;                            m_PassTime = time;          }          }      private List<Timer> m_Timers = new List<Timer>();      private List<Timer> m_NeedRemoveTimer = new List<Timer>();      private List<Timer> m_CurRunTimer = new List<Timer>();             /// <summary>      /// Sets the timer.      /// </summary>      /// <param name='observer'>      /// The TimerObserverOrSubject you need to listen      /// </param>      /// <param name='callback'>      /// The callback when condition is true.      /// </param>      /// <param name='arg'>      /// Argument of the callback.      /// </param>      /// <param name='observer'>      /// The TimerObserverOrSubject you need to observe      /// </param>      /// <param name='isCanDoFunc'>      /// The condition function, must return a boolean.      /// </param>      /// <param name='argForIsCanDo'>      /// Argument for condition function.      /// </param>      public void SetTimer(TimerObserverOrSubject observer, OnCallBack callback ,object arg,          TimerObserverOrSubject subject, OnIsCanDo isCanDoFunc,object argForIsCanDo) {          if (observer == null || subject == null || callback == null || isCanDoFunc == null) return;                    if (isCanDoFunc(argForIsCanDo)) {              callback(arg);              return;          }                    Timer timer = new Timer(observer, callback, arg, subject, isCanDoFunc, argForIsCanDo);               m_Timers.Add(timer);      }             /// <summary>      /// Sets the timer.      /// </summary>      /// <param name='observer'>      /// The TimerObserverOrSubject you need to listen      /// </param>      /// <param name='callback'>      /// The callback when time is up.      /// </param>      /// <param name='arg'>      /// Argument of the callback.      /// </param>      /// <param name='timepass'>      /// Timepass before calling the callback.      /// </param>      public void SetTimer(TimerObserverOrSubject observer, OnCallBack callback , object arg, float timepass) {          if (observer != null && callback != null) {                         Timer timer = new Timer(observer, callback, arg, timepass);              m_Timers.Add(timer);          }      }             /// <summary>      /// Clears all Timers of the observer.      /// </summary>      /// <param name='observer'>      /// The TimerObserverOrSubject you need to clear      /// </param>      public void ClearTimer(TimerObserverOrSubject observer) {          List<Timer> needRemovedTimers = new List<Timer>();                    foreach (Timer timer in m_Timers) {              if (timer.m_Observer == observer || timer.m_Subject) {                  needRemovedTimers.Add(timer);              }          }                    foreach (Timer timer in needRemovedTimers) {              m_Timers.Remove(timer);          }      }                 // Update is called once per frame          void Update ()       {          InitialCurTimerDict();          RunTimer();          RemoveTimer();          }            private void InitialCurTimerDict() {          m_CurRunTimer.Clear();                    foreach (Timer timer in m_Timers) {              m_CurRunTimer.Add(timer);          }      }            private void RunTimer() {          m_NeedRemoveTimer.Clear();                    foreach (Timer timer in m_CurRunTimer) {                      if (timer.m_IsCanDoFunc == null) {                  timer.m_PassTime =  timer.m_PassTime - Time.deltaTime;                  if (timer.m_PassTime < 0) {                      timer.m_Callback(timer.m_Arg);                      m_NeedRemoveTimer.Add(timer);                  }              } else {                  if (timer.m_IsCanDoFunc(timer.m_ArgForIsCanDoFunc)) {                      timer.m_Callback(timer.m_Arg);                      m_NeedRemoveTimer.Add(timer);                  }              }             }      }            private void RemoveTimer() {          foreach (Timer timer in m_NeedRemoveTimer) {              if (m_Timers.Contains(timer)) {                  m_Timers.Remove(timer);              }          }      }    }  首先,它定义了回调函数的类型:[csharp] view plain copy print?public delegate void OnCallBack(object arg);  public delegate bool OnIsCanDo(object arg);  关于C#的委托机制,如果有童鞋不了解,请详见官方文档。简单来说,委托类似一个函数指针,常被用于回调函数。然后,定义了一个数据类型Timer用于保存一个计数器的各个信息。接下来,就是TimerController的两个重要的SetTimer函数。我们先看第一个SetTimer函数:[csharp] view plain copy print?/// <summary>  /// Sets the timer.  /// </summary>  /// <param name='observer'>  /// The observer to observe the subject  /// </param>  /// <param name='callback'>  /// The callback when condition is true.  /// </param>  /// <param name='arg'>  /// Argument of the callback.  /// </param>  /// <param name='subject'>  /// The subject you need to observe  /// </param>  /// <param name='isCanDoFunc'>  /// The condition function, must return a boolean.  /// </param>  /// <param name='argForIsCanDo'>  /// Argument for condition function.  /// </param>  public void SetTimer(TimerObserverOrSubject observer, OnCallBack callback ,object arg,      TimerObserverOrSubject subject, OnIsCanDo isCanDoFunc,object argForIsCanDo) {      if (observer == null || subject == null || callback == null || isCanDoFunc == null) return;            if (isCanDoFunc(argForIsCanDo)) {          callback(arg);          return;         }            Timer timer = new Timer(observer, callback, arg, subject, isCanDoFunc, argForIsCanDo);           m_Timers.Add(timer);  }  根据函数说明可以看出,它负责建立一个计数器,当subject的isCanDoFunc(argForIsCanDo)函数返回true时,通知observer,执行observer的callback(arg)函数。第二个SetTimer函数更简单:[csharp] view plain copy print?/// <summary>  /// Sets the timer.  /// </summary>  /// <param name='observer'>  /// The observer to observe the subject  /// </param>  /// <param name='callback'>  /// The callback when time is up.  /// </param>  /// <param name='arg'>  /// Argument of the callback.  /// </param>  /// <param name='timepass'>  /// Timepass before calling the callback.  /// </param>  public void SetTimer(TimerObserverOrSubject observer, OnCallBack callback , object arg, float timepass) {      if (observer != null && callback != null) {                     Timer timer = new Timer(observer, callback, arg, timepass);          m_Timers.Add(timer);      }  }  它负责建立一个计数器,在timepass的时间后,通知observer,执行observer的callback(arg)函数。Update()函数里面负责检查所有Timer是否可以触发以及是否需要删除。

例子

在这个例子里,我们需要在程序开始运行5秒后,打印一些信息。当然这个的实现有很多方法,这里我们使用今天实现的TimerController来实现。TimerSample.cs的内容如下:[csharp] view plain copy print?using UnityEngine;  using System.Collections;    public class TimerSample : TimerObserverOrSubject {            private TimerController m_TimerCtr = null;            private bool m_IsCanDisplay = false;            private string m_DisplayContent = "Hello, candycat!";            // Use this for initialization      void Start () {          m_TimerCtr = Singleton.getInstance("TimerController") as TimerController;                    //m_TimerCtr.SetTimer(this, Display, m_DisplayContent, 5);                    m_TimerCtr.SetTimer(this, Display, null, this, IsCanDisplay, null);                    StartCoroutine(DelayDisplay());      }            void Display(object arg) {          if (arg == null) {              Debug.Log(m_DisplayContent);          } else {              string content = arg as string;                        Debug.Log(content);          }      }            bool IsCanDisplay(object arg) {          return m_IsCanDisplay;      }            IEnumerator DelayDisplay() {          yield return new WaitForSeconds(5.0f);                    m_IsCanDisplay = true;      }            // Update is called once per frame      void Update () {            }  }  首先,它向TimerController请求注册了一个计时器。这里,它的条件是IsCanDisplay函数,它返回bool值m_IsCanDisplay。而这个值将会在5秒后,通过协同函数DelayDisplay来由false置为true。当其为true时,TimerController就将通知TimerSample调用Display函数。我们将第16行代码注释解开,并将18-20行代码注释掉,则可以达到相同的效果。

结束语

C#的委托机制还是非常常用的,使用Unity的童鞋最好还是了解一下。关于TimerController的执行效率,由于它是每一帧都要去判断所有的condition函数,所以应当让condition函数中的逻辑尽可能简单。好了,这次就到这里,如果有更好的想法,或者这里的代码有什么问题,都非常欢迎指正。谢谢阅读!
发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表