本文分享Unity的中自定义协程指令和协程的模拟实现
在上一篇文章中, 我们简单分享了Unity和Lua中协程的基本概念和用法, 并将两者做了一些比较.
在这篇文章中, 我们将进一步探索Unity对协程的实现, 并通过自定义协程来猜测和模拟Unity是如何实现协程的.
Unity中自定义协程指令
Unity默认提供的
WaitForSeconds
,
WaitForEndOfFrame
之类的指令继承于
YieldInstruction
.
也提供了一些灵活的方式来定义自定义的指令.
官方文档提供了两种实现自定义指令的方式, 下面逐一介绍.
通过继承CustomYieldInstruction类
Unity提供了
CustomYieldInstruction
类, 我们可以继承它用于实现自己的协程指令, 比如等待时间, 比如满足某种条件等.
自定义指令的核心要点是重写
CustomYieldInstruction
类的
keepWaiting
属性, 用于提供Unity判断改指令是否结束.
Unity会在每帧的
Update
和
LateUpdate
之间询问指令的该属性.
下面是示例1:
class WaitWhile : CustomYieldInstruction
{
Func<bool> m_Predicate;
public override bool keepWaiting { get { return m_Predicate(); } }
public WaitWhile(Func<bool> predicate) { m_Predicate = predicate; }
}
public class ExampleScript : MonoBehaviour
{
void Start()
{
StartCoroutine(waitForSomething());
}
// 打印start之后, 直到鼠标右键按下才会继续执行
public IEnumerator waitForSomething()
{
Debug.Log("start");
yield return new WaitWhile() {()=> return Input.GetMouseButtonDown(1);}
Debug.Log("Right mouse button pressed");
}
}
下面是示例2:
public class WaitForMouseDown : CustomYieldInstruction
{
public override bool keepWaiting
{
get
{
return !Input.GetMouseButtonDown(1);
}
}
public WaitForMouseDown()
{
Debug.Log("Waiting for Mouse right button down");
}
}
public class ExampleScript : MonoBehaviour
{
void Update()
{
if (Input.GetMouseButtonUp(0))
{
Debug.Log("Left mouse button up");
StartCoroutine(waitForMouseDown());
}
}
// 打印Update之后, 直到鼠标右键按下才会继续执行
public IEnumerator waitForMouseDown()
{
Debug.Log("Update");
yield return new WaitForMouseDown();
Debug.Log("Right mouse button pressed");
}
}
通过实现IEnumerator接口, 构造迭代器来实现自定义指令
Unity是通过迭代器和可迭代对象来实现协程的, 所以我们也可以自己构造迭代器来实现自己想要的效果.
下面是例子:
class WaitWhile : IEnumerator
{
Func<bool> m_Predicate;
public object Current { get { return null; } }
public bool MoveNext() { return m_Predicate(); }
public void Reset() {}
public WaitWhile(Func<bool> predicate) { m_Predicate = predicate; }
}
public class ExampleScript : MonoBehaviour
{
void Start()
{
StartCoroutine(waitForSomething());
}
// 打印start之后, 直到鼠标右键按下才会继续执行
public IEnumerator waitForSomething()
{
Debug.Log("start");
yield return new WaitWhile() {()=> return Input.GetMouseButtonDown(1);}
Debug.Log("Right mouse button pressed");
}
}
可以看到, 这种方式和示例1很像, 只不过将keepWaiting来替代了MoveNext, 并提供了接口的默认实现.
上面的介绍的使用方式很简单, 也没有什么更多的内容可说.
在接下来的篇幅, 我们将尝试自己借鉴Unity的方式, 自己来模拟实现协程.
模拟实现Unity协程
我们的模拟大概涉及到几个类:
-
MonoBehavior, 我们平常写代码的脚本, 协程的起点.
-
YieldInstruction, 指令类, 本身是可迭代类, 实现了IEnumerable接口.
-
WaitForFrames, 指令类, 继承于YieldInstruction, 用于等待一定帧数
-
Coroutine, 协程类, 继承于YieldInstruction, 提供具体的实现.
-
CustomYieldInstruction, 自定义指令类, 实现了IEnumerator接口, 提供自定义行为
-
WaitWhile, 自定义指令类, 继承于CustomYieldInstruction, 用于条件等待
Coroutine
首先我们来看看协程类.
协程类可以说是整个实现的核心(废话
_
). 它在各个点上起到承上启下的作用.
协程类继承于YieldInstruction, 本身是一个迭代器, 每一次迭代就进行一次代码的递进, 可能是执行到下一个yield, 也可能是执行到代码结尾, 也可能是执行一次问询.
Coroutine的属性
// 路径, 这个就是我们写的协程方法, 即public IEnumerator Wait(), 每执行一次MoveNext就走到下一个yield或者结束
protected IEnumerator m_Routine;
// 是当前指令, 即yield return new CustomYieldInstruction
protected IEnumerator m_CurInstruction;
m_Routine维护了一个迭代器, 也就是我们在Mono脚本中写的协程方法的返回值.
m_Routine的每一次MoveNext, 都会重新运行协程方法, 直到遇到yield或者方法结尾结束.
m_Routine执行MoveNext后, 其Current会指向一个新的迭代器(如果存在的话), 对应代码
yield return new xxx
, 这个xxx就是新的迭代器.
我们需要将Current指向的迭代器迭代完成才会继续协程方法的执行.
这个Current就是我们能够实现协程的核心.
我们把这个Current抽象为一个
yield指令
, 即可以是一个YieldInstruction, 也可以是一个Coroutine, 也可以是一个CustomYieldInstruction, 甚至可以是一个IEnumerator. 只要是实现了IEnumerator接口或者相似行为的都可以.
所以Coroutine的第二个属性, 我们定义为
当前指令
.
Coroutine的方法
public Coroutine(IEnumerator routine)
{
m_Routine = routine;
}
public override bool MoveNext()
{
if (m_CurInstruction != null)
{
// 调用CustomYieldInstruction结束
if (!m_CurInstruction.MoveNext())
{
m_CurInstruction = null;
}
return true;
}
// 调用yield, 获取一个CustomYieldInstruction, 即yield return new CustomYieldInstruction
// 如果返回值是false, 整个停止
if (!m_Routine.MoveNext())
return false;
var instruction = m_Routine.Current as IEnumerator;
// null, 暂停一帧
if (instruction == null)
return true;
// 调用CustomYieldInstruction的下一步
if (instruction.MoveNext())
{
m_CurInstruction = instruction;
}
return true;
}
首先是构造方法, 接受一个迭代器, 即我们写的协程方法的返回值.
接下来是核心算法的说明:
- 协程迭代一次(MoveNext), 如果存在当前指令, 则跳转第四步, 否则跳转下一步
- 路径迭代一次(MoveNext), 如果返回false, 代表协程方法执行完毕, 结束整个协程运行, 否则跳转下一步
- 通过Current获取当前指令, 如果是null, 则暂停协程运行, 等待下次路径迭代, 否则跳转下一步
- 指令迭代一次(MoveNext), 如果返回false, 代表指令执行完毕, 则暂停协程运行, 等待下次路径迭代
总体的意思就是根据路径的迭代获取指令, 指令完成后进行迭代路径, 直到路径迭代完成.
MonoBehavior, 协程的启动,停止和调用
接下来说说协程的启动和调用过程.
在Mono脚本中维护了一个协程列表:
List<Coroutine> m_DelayCallLst
.
在每次Update和LateUpdate之间会对列表内的协程进行迭代.
使用StartCoroutine/StopCoroutine进行协程的启动或停止.
下面是大概的代码:
public class MonoBehavior
{
List<Coroutine> m_DelayCallLst = new List<Coroutine>();
public MonoBehavior()
{
Start();
}
protected virtual void Start() { }
protected virtual void Update() { }
private void LateUpdate() { }
private void DoDelayCall()
{
for (int i = m_DelayCallLst.Count - 1; i >= 0; i--)
{
var call = m_DelayCallLst[i];
if (!call.MoveNext())
{
m_DelayCallLst.Remove(call);
}
}
}
public void MainLoop()
{
Update();
DoDelayCall();
LateUpdate();
}
public Coroutine StartCoroutine(IEnumerator routine)
{
var coroutine = new Coroutine(routine);
m_DelayCallLst.Add(coroutine);
return coroutine;
}
public void StopCoroutine(Coroutine coroutine)
{
m_DelayCallLst.Remove(coroutine);
}
}
在主循环中每帧调用Mono脚本的Update方法, 这里我们将每帧设定为100ms.
void Main()
{
var testMono = new TestMono();
int i = 0;
while (i < 20)
{
testMono.MainLoop();
Thread.Sleep(100);
i++;
}
}
指令相关类
YieldInstruction
指令类的基类, 常用的WaitForFrames, WaitForFixedUpdate, WaitForSeconds, WaitForSecondsRealtime等都是继承此类.
指令类定义了指令的行为, 最主要的就是迭代(MoveNext)和Current.
指令类默认情况下只有自身设定的条件完成后才通过MoveNext告诉外部指令执行完毕.
实现如下:
public class YieldInstruction : IEnumerator
{
public virtual bool MoveNext()
{
return false;
}
// 实现接口, 无用
public void Reset() { }
public object Current { get { return null; } }
}
WaitForFrames
等待指定多少帧后继续执行的指令类, 继承于YieldInstruction, 实现如下:
// 等待多少帧
public class WaitForFrames : YieldInstruction
{
private float m_Frames;
public WaitForFrames(float seconds)
{
m_Frames = seconds;
}
public override bool MoveNext()
{
m_Frames--;
return m_Frames > 0;
}
}
CustomYieldInstruction
也是指令类, 却不是继承于YieldInstruction而是实现迭代器接口, 将MoveNext的问询抽象到了一个
keepWaiting
属性上.
子类通过对该属性的设定来决定指令的结束与否. 实现如下:
public class CustomYieldInstruction : IEnumerator
{
public CustomYieldInstruction() { }
protected virtual bool keepWaiting { get; }
public bool MoveNext()
{
return keepWaiting;
}
// 实现接口, 无用
public void Reset() { }
public object Current { get { return null; } }
}
WaitWhile
根据委托判断, 达到指定条件时才继续执行的指令类, 继承于CustomYieldInstruction, 实现如下:
public class WaitWhile : CustomYieldInstruction
{
Func<bool> m_Predicate;
public WaitWhile(Func<bool> func)
{
m_Predicate = func;
}
protected override bool keepWaiting
{
get
{
return m_Predicate();
}
}
}
//---------------------------------------
// 使用示例
public IEnumerator Wait()
{
Console.WriteLine("End");
yield return new WaitWhile(() => { return m_i < 4; });
Console.WriteLine("End");
}
使用示例
public class TestMono : MonoBehavior
{
private int m_i = 1;
protected override void Start()
{
StartCoroutine(Wait());
}
protected override void Update()
{
Console.WriteLine($"------------------------------ Tick ...... {m_i}");
m_i++;
}
public IEnumerator Wait()
{
yield return new WaitForFrames(5);
Console.WriteLine("Begin at 6");
yield return new WaitWhile(() => { return m_i < 4; });
Console.WriteLine("Wait4");
yield return null;
Console.WriteLine("Wait5");
yield return null;
Console.WriteLine("Wait6");
yield return null;
yield return new WaitWhile(() => { return m_i < 10; });
Console.WriteLine("End at 10");
}
}
void Main()
{
var testMono = new TestMono();
int i = 0;
while (i < 20)
{
testMono.MainLoop();
Thread.Sleep(100);
i++;
}
}
代码比较简单, 这里就不再过多赘述.
总结
协程的实现主要是利用迭代器和可迭代类的特性.
核心算法是在一个协程类包裹了一条”路径”, 路径迭代过程中遇到”指令”就对指令进行迭代, 直到完成整条路径.
上面的过程是作者在参考Unity类的定义和自己的摸索进行的模拟, 并不代表Unity真实的实现.
这里是
完整的模拟代码
.
希望能够对大家有所启发和帮助.