Unity的中自定义协程指令和协程的模拟实现

  • Post author:
  • Post category:其他




本文分享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;
}

首先是构造方法, 接受一个迭代器, 即我们写的协程方法的返回值.

接下来是核心算法的说明:

  1. 协程迭代一次(MoveNext), 如果存在当前指令, 则跳转第四步, 否则跳转下一步
  2. 路径迭代一次(MoveNext), 如果返回false, 代表协程方法执行完毕, 结束整个协程运行, 否则跳转下一步
  3. 通过Current获取当前指令, 如果是null, 则暂停协程运行, 等待下次路径迭代, 否则跳转下一步
  4. 指令迭代一次(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真实的实现.

这里是

完整的模拟代码

.

希望能够对大家有所启发和帮助.



版权声明:本文为woodengm原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。