Unity3D插件分析之More Effective Coroutines
在上一篇[Unity Coroutine Wrapper][0]介绍过对Unity Coroutine的封装都是对MoveNext()结果的延时处理,并不能进行真正的Stop,Pause等操作,所以基于Unity Coroutine的封装管理意义都不到或者功能很有限。
特性
这篇博客,我会对More Effective Coroutines实现和原理进行剖析,希望对这方面有需求的人有帮助。先罗列下这个插件的优势:
1.Effective:No Unity Default GC,Zero per-frame memory allocations,Twice as fast as Unity
2.Easy to Use:No need to inherit MonoBehaviour
3.Debug Easier:have call stacktrace
4.More Control:really Paused,Resume with a tagged string
5.Extensible:...
从Unity Coroutine切换到More Effective Courtines使用也很方便,插件内的Quick Start Guide.pdf有介绍。本质的不同是它把IEnumerator统一为IEnumerator<float>,分为三类float和Unity对应如下:
1.yield return null => yield return 0f 2.yield return new WaitForSeconds(0.1f) => yield return Timing.WaitForSeconds(0.1f) 3.yield return WaitUtil(objectToWaitFor) => yield return Timing.WaitUntilDone(objectToWaitFor)
通过代码发现Timing.WaitUntilDone的返回值是float.NaN,等待就意味着不可预知什么时候完成,用float.NaN来标识太牛逼了。
有一个需要注意的是调用 public static float WaitUntilDone(IEnumerator<float> otherCoroutine)函数的otherCoroutine,不只是IEnumerator<float>而是需要RunCoroutine返回的“coroutine”,要不然就不起作用了,我之前还因此发邮件给作者反馈才发现的。
定义
这个插件很简单,只有一个Timing类,下面我对Timing类进行剖析。
我们知道Unity会在MonoBehaviour不同的生命周期回调函数(Update,LateUpdate,FixedUpdate)调用IEnumerator的MoveNext函数,所以Timing对着这三类用Segment(还增加了一个SlowUpdate)来标记,并且分别分配了缓存数组:
public enum Segment
{
Update,
FixedUpdate,
LateUpdate,
SlowUpdate,
}
//数组缓存
private IEnumerator<float>[] UpdateProcesses = new IEnumerator<float>[InitialBufferSizeLarge];
private IEnumerator<float>[] LateUpdateProcesses = new IEnumerator<float>[InitialBufferSizeSmall];
private IEnumerator<float>[] FixedUpdateProcesses = new IEnumerator<float>[InitialBufferSizeMedium];
private IEnumerator<float>[] SlowUpdateProcesses = new IEnumerator<float>[InitialBufferSizeMedium];
//每帧处理的Coroutine数量,防止一帧内过多消耗
private const ushort FramesUntilMaintenance = 64;
//缓存数组不够,每次新增分配大小
private const int ProcessArrayChunkSize = 64;
//初始缓存数组大小
private const int InitialBufferSizeLarge = 256;
private const int InitialBufferSizeMedium = 64;
private const int InitialBufferSizeSmall = 8;
后面是Timing预定义的常量,可以根据项目需求进行调整,比如Coroutine的初始缓存数量,这也是为什么没有帧内存分配的原因。
有了缓存数组,我们还需要定义一个能唯一区分Coroutine的结构ProcessIndex,方便查找和存储:
private struct ProcessIndex : System.IEquatable<ProcessIndex>
{
public Segment seg;
public int i; //数组索引
public bool Equals(ProcessIndex other)
{
return seg == other.seg && i == other.i;
}
public override bool Equals(object other)
{
if (other is ProcessIndex)
return Equals((ProcessIndex)other);
return false;
}
public static bool operator ==(ProcessIndex a, ProcessIndex b)
{
return a.seg == b.seg && a.i == b.i;
}
public static bool operator !=(ProcessIndex a, ProcessIndex b)
{
return a.seg != b.seg || a.i != b.i;
}
public override int GetHashCode()
{
return (((int)seg - 2) * (int.MaxValue / 3)) + i;
}
}
为了实现前面提到的WaitUtilDone功能,Timing还定义了WaitingProcess结构:
private class WaitingProcess
{
public class ProcessData
{
public IEnumerator<float> Task;
public string Tag;
public Segment Segment;
}
public IEnumerator<float> Trigger;//需要等待Coroutine
public string TriggerTag;
public bool Killed;
public readonly List<ProcessData> Tasks = new List<ProcessData>();//子Coroutine任务
}
//缓存
private readonly List<WaitingProcess> _waitingProcesses = new List<WaitingProcess>();
为了可以根据tag字符串进行paused和resume,Timing针对tag做了Dictionary缓存:
private readonly Dictionary<ProcessIndex, string> _processTags = new Dictionary<ProcessIndex, string>();
private readonly Dictionary<string, HashSet<ProcessIndex>> _taggedProcesses = new Dictionary<string, HashSet<ProcessIndex>>();
实现
我们先从Timing的调用入口RunCoroutineOnInstance*顺藤摸瓜,根据Segment进行分类处理,都是类似的,我们只看Segment.Update部分即可:
public IEnumerator<float> RunCoroutineOnInstance(IEnumerator<float> coroutine, Segment timing, string tag)
{
if(coroutine == null)
return null;
//创建 ProcessIndex
ProcessIndex slot = new ProcessIndex {seg = timing};
switch(timing)
{
case Segment.Update:
//放入缓存数组
if(_nextUpdateProcessSlot >= UpdateProcesses.Length)
{
IEnumerator<float>[] oldArray = UpdateProcesses;
UpdateProcesses = new IEnumerator<float>[UpdateProcesses.Length + (ProcessArrayChunkSize * _expansions++)];
for(int i = 0;i < oldArray.Length;i++)
UpdateProcesses[i] = oldArray[i];
}
slot.i = _nextUpdateProcessSlot++;
UpdateProcesses[slot.i] = coroutine;
//添加到Tag
if(tag != null)
AddTag(tag, slot);
if(!_runningUpdate)
{
try
{
//立即执行第一帧
_runningUpdate = true;
SetTimeValues(slot.seg);
if(!UpdateProcesses[slot.i].MoveNext())
{
UpdateProcesses[slot.i] = null;
}
else if (UpdateProcesses[slot.i] != null && float.IsNaN(UpdateProcesses[slot.i].Current))
{
if(ReplacementFunction == null)
{
UpdateProcesses[slot.i] = null;
}
else
{
UpdateProcesses[slot.i] = ReplacementFunction(UpdateProcesses[slot.i], timing,
_processTags.ContainsKey(slot) ? _processTags[slot] : null);
ReplacementFunction = null;
if (UpdateProcesses[slot.i] != null)
UpdateProcesses[slot.i].MoveNext();
}
}
}
catch (System.Exception ex)
{
if (OnError == null)
_exceptions.Enqueue(ex);
else
OnError(ex);
UpdateProcesses[slot.i] = null;
}
finally
{
_runningUpdate = false;
}
}
return coroutine;
// FixedUpdate ,LateUpdate,SlowUpdate的处理
}
}
}
然后分别在Update,FixedUpdate,LateUpdate里面对Current和MoveNext调用处理,主要可以分成三步:
1.更新时间:UpdateTimeValues
2.用localTime和Current,进去比较,如果localTime大于等于Current说明条件达成就调用MoveNext
3.调用MoveNext,如果结束,就重置为null,否则进行4
4.如果Current等于NaN,说明在等待WaitForDone的完成,调用RepacementFunction,将当前coroutine添加到watingprocess的Tasks里面去
代码就不贴,可以去AssetStore上面下载查看,主要还是要介绍下WaitForDone的实现。
WaitForDone
WaitForSeconds很简单,就是要通过当前时间localTime和等待时间waitTIme设置当前IEnumerator的Current值:
public static float WaitForSeconds(float waitTime)
{
if (float.IsNaN(waitTime)) waitTime = 0f;
return (float)LocalTime + waitTime;
}
相当于Current和localTime做比较,然后Current一直比localTime大,当localTime不小于Current才调用MoveNext(即上面的1和2),十分直观容易理解
但是,WaitForDone,需要等待的IEnumerator是不知道具体需要延时多久,所以不能简单用localTime加上一个时间戳来处理,这里巧妙利用
1.NaN无穷大,即暂停(终止)该IEnumerator的运行
2.将该IEnumerator添加到waitingProcess的Tasks,等待Trigger完成回调
下面是省略了在_waitingProcesses中存在该等待otherCoroutine的情况(即多次调用等待同一个otherCoroutine),下面是最简单的情况:
public static float WaitUntilDone(IEnumerator<float> otherCoroutine, bool warnOnIssue, Timing instance)
{
//省略复杂情况的处理
//创建一个 WaitingProcess
WaitingProcess newProcess = new WaitingProcess { Trigger = otherCoroutine };
//替换otherCoroutine
if(instance.ReplaceCoroutine(otherCoroutine, instance._StartWhenDone(newProcess), out newProcess.TriggerTag))
{
ReplacementFunction = (input, segment, tag) =>
{
newProcess.Tasks.Add(new WaitingProcess.ProcessData
{
Task = input,
Tag = tag,
Segment = segment
});
//添加到_waitingProcesses保存起来后面回调在取出
instance._waitingProcesses.Add(newProcess);
//重置当前coroutine为null,已经不需要再处理了,在_StartWhenDone会回调处理
return null;
};
//返回Current
return float.NaN;
}
return -1f;
}
一共包括3个过程:
1.ReplaceCoroutine替换otherCoroutine为_StartWhenDone(newProcess)
2.ReplacementFunction,这个函数会在Update中调用(即上面Update过程的4)
3.返回NaN标记当前coroutine需要等待
那么,自然会想到这里的为什么要进行ReplaceCoroutine,_StartWhenDone的作用是什么?其实前面已经说过了,_StartWhenDone就是为了将被我们暂停(终止)的coroutine在等待的otherCoroutine执行完毕重新唤醒(CloseWaitingProcess):
private IEnumerator<float> _StartWhenDone(WaitingProcess processData)
{
try
{
//判断是否被Kill了
if (processData.Killed)
{
CloseWaitingProcess(processData);
yield break;
}
//WaitForSeconds
if (processData.Trigger.Current > localTime)
{
yield return processData.Trigger.Current;
if (processData.Killed)
{
CloseWaitingProcess(processData);
yield break;
}
}
//其他情况
while (processData.Trigger.MoveNext())
{
yield return processData.Trigger.Current;
if (processData.Killed)
{
CloseWaitingProcess(processData);
yield break;
}
}
}
finally
{
CloseWaitingProcess(processData);
}
}
private void CloseWaitingProcess(WaitingProcess processData)
{
if (_waitingProcesses.Contains(processData))
{
_waitingProcesses.Remove(processData);
foreach (WaitingProcess.ProcessData taskData in processData.Tasks)
RunCoroutineOnInstance(taskData.Task, taskData.Segment, taskData.Tag);
}
}
扩展
Timing增加了全局Paused和根据tag字符串标记Kill coroutine。
前面已经介绍过WaitForSeconds和WaitUtil的替换兼容,Timing还对WWW,AsynOperation和CustomYieldInstruction**进行了扩展兼容,也很简单,仅列举WWW:
public static float WaitUntilDone(WWW wwwObject)
{
ReplacementFunction = (input, timing, tag) => _StartWhenDone(wwwObject, input);
return float.NaN;
}
private static IEnumerator<float> _StartWhenDone(WWW www, IEnumerator<float> pausedProc)
{
while (!www.isDone)
yield return 0f;
ReplacementFunction = delegate { return pausedProc; };
yield return float.NaN;
}
此外,还有CallDelay,CallContinously,CallPeriodically三个辅助函数。
总结
作者利用IEnumerator<float>对所有情况进行抽象,特别是WaitForDone的处理,不是一眼就能看明白,很巧妙。
读完代码我有几个思考:
1.不能对单个coroutine进行Paused操作,可能是为了减少对每个coroutine的localTime存储,或者这个需求本身用处不多,不过如果那样其实就是Tween动画了。
2.能否增加返回值,类似返回IEnumerator<T,float>的结构
3.代码精简,Update和ReplacementFunction都是有大量的重复代码
哦,对了,有需要可以支持下作者的pro版本More Effective Coroutines Pro。