统一管理回调函数——观察者模式
场景引入
想象这样一个场景:在加载一个模型时,需要从网上下载,但并不知道下载所需的时间。我们只知道,当下载完成后,就可以将模型放置在特定位置,然后开始游戏。那么,如何判断下载是否完成呢?
一种简单的方法是,在每一帧都检查下载是否完成,若完成则继续后续工作。我们可以告诉一个管理器:“你帮我留意一下,下载完成后通知我,以便我执行相应的函数。”接下来,我们就来构建这样一个管理器。
实现前提
需要注意的是,下面的代码依赖于之前所讲的单例模式。
概念定义
我们将上述工作中的计数器命名为 Timer(这个名称可能不太准确),把需要被通知的对象称为观察者(Observer),像下载管理器这样的对象则称为主题(Subject)。
代码实现
定义观察者和主题对象
TimerObserverOrSubject.cs 的代码如下:
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 的代码如下:
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>
/// 设置计时器
/// </summary>
/// <param name="observer">需要监听的 TimerObserverOrSubject</param>
/// <param name="callback">条件满足时的回调函数</param>
/// <param name="arg">回调函数的参数</param>
/// <param name="subject">需要观察的 TimerObserverOrSubject</param>
/// <param name="isCanDoFunc">条件判断函数,必须返回布尔值</param>
/// <param name="argForIsCanDo">条件判断函数的参数</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>
/// 设置计时器
/// </summary>
/// <param name="observer">需要监听的 TimerObserverOrSubject</param>
/// <param name="callback">时间到达时的回调函数</param>
/// <param name="arg">回调函数的参数</param>
/// <param name="timepass">回调函数调用前的时间</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>
/// 清除观察者的所有计时器
/// </summary>
/// <param name="observer">需要清除的 TimerObserverOrSubject</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 == observer)
{
needRemovedTimers.Add(timer);
}
}
foreach (Timer timer in needRemovedTimers)
{
m_Timers.Remove(timer);
}
}
// 每帧调用
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 -= 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);
}
}
}
}
代码解释
委托定义:
public delegate void OnCallBack(object arg);:定义了一个回调函数的委托,该函数接受一个object类型的参数。public delegate bool OnIsCanDo(object arg);:定义了一个条件判断函数的委托,该函数接受一个object类型的参数,并返回一个布尔值。
Timer类:用于保存一个计数器的各个信息,包括观察者、回调函数、参数、主题、条件判断函数等。SetTimer函数:- 第一个
SetTimer函数:负责建立一个计数器,当subject的isCanDoFunc(argForIsCanDo)函数返回true时,通知observer,执行observer的callback(arg)函数。 - 第二个
SetTimer函数:负责建立一个计数器,在timepass的时间后,通知observer,执行observer的callback(arg)函数。
- 第一个
ClearTimer函数:用于清除观察者的所有计时器。Update函数:负责检查所有Timer是否可以触发以及是否需要删除。
示例代码
在这个例子中,我们需要在程序开始运行 5 秒后,打印一些信息。这里我们使用 TimerController 来实现。
TimerSample.cs 的代码如下:
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!";
// 初始化
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;
}
// 每帧调用
void Update()
{
// 首先,它向 TimerController 请求注册了一个计时器。这里,它的条件是 IsCanDisplay 函数,它返回 bool 值 m_IsCanDisplay。
// 而这个值将会在 5 秒后,通过协同函数 DelayDisplay 来由 false 置为 true。当其为 true 时,TimerController 就将通知 TimerSample 调用 Display 函数。
}
}
示例解释
在 Start 函数中,TimerSample 向 TimerController 注册了一个计时器,其条件是 IsCanDisplay 函数,该函数返回 bool 值 m_IsCanDisplay。而 m_IsCanDisplay 将会在 5 秒后,通过协同函数 DelayDisplay 由 false 置为 true。当 m_IsCanDisplay 为 true 时,TimerController 就会通知 TimerSample 调用 Display 函数。
若将第 16 行代码注释解开,并将 18 - 20 行代码注释掉,也可以达到相同的效果。