在Unity中协程是最常用的异步手段。
在读此文章之前,我假设你已经熟悉了协程的基本使用以及原理。
此文章只是作一些实践上的建议与总结。
1、通过参数传递的方式复用协程
协程通过C#迭代器在Monobehavior的生命周期当中逐步的完成异步任务。
所以协程依赖于Monobehavior。一旦Monobehavior被销毁,那么协程也将会被销毁。在Unity当中官方只提供了几个最简单的接口,让协程变得不那么容易管理。
CoroutineMonoBehaviour.StartCoroutine
MonoBehaviour.StartCoroutineStarts
MonoBehaviour.StopCoroutineStops
MonoBehaviour.StopAllCoroutinesStops
举个例子,例如我只需要实现最最简单的等待行为。我们也需要独立书写出一个迭代器函数来实现我的行为,并且每次需要用到的时候度需要实现一次,这导致了我们代码的碎片化,以及大量的重复代码。
IEnumerator WaitTwoSeconds()
{
yield return new WaitForSeconds(2);
//Do Something
}
这时,我们可以想到C#中的Lamda表达式。
IEnumerator WaitTwoSeconds(Action act, int seconds)
{
yield return new WaitForSeconds(seconds);
act();
}
我们可以实现很多类似于这种协程预置来避免书写大量的重复的迭代函数。
不过,这只是解决了一部分的问题。
我希望协程在后台一直运作而不希望依赖于哪一个特定的Monobehavior时,或者是希望找出特定的协程并对其进行暂停、取消暂停、停止等等操作,我们或许都需要写额外的代码来进行实现。
那么这个时候我们可以考虑用Dictionary来将协程存储起来。
2、协程管理器
我们可以将协程看做一个对象,其中包含了协程Id、迭代器、回调行为等属性,以及开始、暂停、取消暂停、停止等行为。
///
/// 协程任务类
///
public class CoroutineTask
{
private static long taskId = 1;
private IEnumerator iEnumer;
private System.Action callBack;
public string name
{
get;
private set;
}
public object bindObject
{
get;
private set;
}
public bool running
{
get;
private set;
}
public bool paused
{
get;
private set;
}
public CoroutineTask(
IEnumerator iEnumer,
System.Action callBack = null,
bool autoStart = true)
{
string taskName =
iEnumer.GetHashCode().ToString() +
" Id:" + taskId;
this.name = taskName;
this.iEnumer = iEnumer;
this.callBack = (comp) =>
{
taskList.Remove(name);
if (callBack != null)
callBack(comp);
};
if (bindObject == null)
{
this.bindObject = CoroutineTaskManager.Instance.gameObject;
}
else
{
this.bindObject = bindObject;
}
running = false;
paused = false;
if (autoStart == true)
{
Start();
}
taskId += 1;
}
public CoroutineTask(
IEnumerator iEnumer,
System.Action callBack = null,
object bindObject = null, bool autoStart = true)
: this(iEnumer, callBack, bindObject, autoStart)
{
this.name = name;
}
public void Start()
{
running = true;
CoroutineTaskManager.
Instance.StartCoroutine(DoTask());
}
public void Pause()
{
paused = true;
}
public void Unpause()
{
paused = false;
}
public void Stop()
{
running = false;
callBack(false);
}
//较为重要的函数DoTask
private IEnumerator DoTask()
{
IEnumerator e = iEnumer;
while (running)
{
if (bindObject.Equals(null))
{
Debug.logger.LogWarning("协程中断", "因为绑定
物体被删除所以停止协程");
Stop();
yield break;
}
if (paused)
{
yield return null;
}
else
{
if (e != null && e.MoveNext())
{
yield return e.Current;
}
else
{
running = false;
callBack(true);
}
}
}
}
}
核心的函数就是DoTask迭代器,在协程对象中有几个flag,代表了当前协程的状态,倘若是暂停状态,那么我们不进行下一步,而是进行空转。不做任何事情,知道状态切换回非暂停状态。当为停止状态时,则直接将迭代器break。
上述代码来自我的Unity增强库ResetCore中协程管理器中对协程对象的封装。大家也可以有自己的思路来书写自己的封装方法,这里只是一个参考。
那么既然我们将协程都封装起来了,那么后面的事情就很好办了。
我们只需要通过创建一个Monobehavior单例(我会在另一篇文章中进行介绍),然后将所有的协程运行都放到该单例上就可以了。
例如我创建一个协程,我就将其加入到我的协程词典当中,当我需要暂停或者停止协程,则直接利用键值取出并调用我们封装的协程对象即可。
/// <summary>
/// 添加一个新任务
/// </summary>
/// <param name="taskName"></param>
/// <param name="iEnumer"></param>
/// <param name="callBack"></param>
/// <param name="autoStart"></param>
public void AddTask(
IEnumerator iEnumer,
System.Action<bool> callBack = null,
object bindObject = null,
bool autoStart = true)
{
CoroutineTask task =
new CoroutineTask(iEnumer, callBack,
bindObject, autoStart);
AddTask(task);
}
/// <summary>
/// 添加一个新任务
/// </summary>
/// <param name="task"></param>
public void AddTask(CoroutineTask task)
{
if (taskList.ContainsKey(task.name))
{
Restart(task.name);
}
else
{
taskList.Add(task.name, task);
}
}
如果我们需要有常用的一些工具,例如等待等等,我们可以直接书写一些工具函数。
public CoroutineTask WaitSecondTodo(
System.Action<bool> callBack,
float time,
object bindObject = null)
{
CoroutineTask task = new CoroutineTask(
DoWaitTodo(time),
callBack, bindObject, true);
AddTask(task);
return task;
}
private IEnumerator DoWaitTodo(float time)
{
yield return new WaitForSeconds(time);
}
当然还有更多的方式来实现一些工具函数,例如循环协程、或者是等待事件成立(Unity5以后的WaitUntil),还有等待下载完成,等等。
ResetCore github地址:https://github.com/vgvgvvv/ResetCore
协程管理器的实现可以在 Assets/ResetCore/Core/Util/CoroutineTaskManager 中找到
如果有更好的协程实践的话,也欢迎与我一起分享!