天天看点

Unity协程实现分析以及Lua协程与Unity协程的混合使用

1,节选翻译一篇stackoverflow关于Unity协程实现的讨论

The big clues are in the C# version. Firstly, note that the return type for the function is IEnumerator. And secondly, note that one of the statements is yield return. This means that yield must be a keyword, and as Unity’s C# support is vanilla C# 3.5, it must be a vanilla C# 3.5 keyword. Indeed, here it is in MSDN – talking about something called ‘iterator blocks.’ So what’s going on?

…最大的线索是在上面的C#版本代码中。首先,注意方法的返回类型是IEnumerator。第二,注意有一个语句是 yield return。这就意味着yield是一个关键字,并且Unity采用的C#3.5,它一定是一个vanilla C#3.5关键字。的确,它在MSDN这里–这里讲的是一些关于 ‘iterator blocks’ 迭代器块的内容。那么这是怎么回事呢?

Firstly, there’s this IEnumerator type. The IEnumerator type acts like a cursor over a sequence, providing two significant members: Current, which is a property giving you the element the cursor is presently over, and MoveNext(), a function that moves to the next element in the sequence. Because IEnumerator is an interface, it doesn’t specify exactly how these members are implemented; MoveNext() could just add one toCurrent, or it could load the new value from a file, or it could download an image from the Internet and hash it and store the new hash in Current… or it could even do one thing for the first element in the sequence, and something entirely different for the second. You could even use it to generate an infinite sequence if you so desired. MoveNext() calculates the next value in the sequence (returning false if there are no more values), and Current retrieves the value it calculated.

首先,先了解一下IEnumerator type,枚举器类。IEnumerator就好像一个在代码行序列上的鼠标箭头,它有两个重要的成员:Current,鼠标箭头当前所在位置的元素,和MoveNext(),一个可以使箭头移到此序列下一个元素的方法。由于IEnumerator是一个接口,它没有具体写出这些成员是如何运作的。MoveNext()可能是给Current加一,可能是从一个文件中读取一个新的值,或则它可能是从网络上下载一张图片并给它赋予哈希值并把哈希值存到Current处,或者它可以给序列中的第一个元素做一些工作,并给第二个元素做完全不同的事情。你甚至可以用它生产一个无限的序列。MoveNext()计算序列中的下一个值(返回false如果没有下一个值),而Current取回这个值。

Ordinarily, if you wanted to implement an interface, you’d have to write a class, implement the members, and so on. Iterator blocks are a convenient way of implementing IEnumerator without all that hassle – you just follow a few rules, and the IEnumerator implementation is generated automatically by the compiler.

一般情况下,如果你想使用一个接口,你需要写一个类,成员,以及其他。‘Iterator blocks’ 迭代器块是一个可以方便的操作IEnumerator的方法,他不需要上面那些繁琐的操作,只需要服从一些简单的规则,编译器会自动生成IEnumerator。

An iterator block is a regular function that (a) returns IEnumerator, and (b) uses the yield keyword. So what does the yield keyword actually do? It declares what the next value in the sequence is – or that there are no more values. The point at which the code encounters a yield return X or yield break is the point at which IEnumerator.MoveNext() should stop; a yield return X causes MoveNext() to return true andCurrent to be assigned the value X, while a yield break causes MoveNext() to return false.

一个‘iterator block’迭代器块是一个普通方法,它有两个特点,1,返回IEnumerator。2,使用yield关键字。yield关键字是做什么的呢?它声明序列中的下一个值是什么,或者没有任何一下个值。当代码遇到yield return X或者yield break,IEnumerator.MoveNext()将会停止。yield return X将会导致MoveNext()返回true,Current被赋值为X。yield break导致MoveNext()返回false。

2,Unity协程底层实现猜测

Unity协程实现分析以及Lua协程与Unity协程的混合使用

上文中提到了C#实现IEnumerator接口的方法会放回一个枚举集合,它有两个特点,一是遇到yield语句会暂停,而是可以利用MoveNext()方法回到上次暂停处。

那利用这两个特点再结合update,我可能会这么实现协程:C#先对实现IEnumerator接口的fun1函数进行解析和转换,Unity负责实现协程的模块获取该函数的Enumerator,在Update中不断进行MoveNext(),逻辑流会在fun1的current处开始,在yield处暂停,Unity负责将yield返回的值解析为条件(例如等待时间),再在update中进行条件判断继续MoveNext或等待下一个update。

3,Lua协程

相对于Unity协程,lua协程也同样是非对称协程(协程函数只能将控制权交还给调用者),不同点在于lua可以控制在何处恢复某一协程,lua还可通过yield,resume传递参数,Unity只能在启动协程时传入参数,协程可控制在何处挂起但是主线程无法控制协程在何处恢复。

func1=function(para1)				//下面将会调用的协程函数既是协程体body
	...
	ret3=coroutine.yield(var2)		//暂停协程,将控制焦点以及var2返回给调用者,var2成为ret2。ret3为协程调用者下次调用resume传进来的参数。
	...
end

co=coroutine.create(func1)			//创建协程
r,ret2=coroutine.resume(co,var1)	//启动协程并传入var1变为para1。
					 				//返回值r为协程状态(suspended,running,normal,dead)
									//ret2为协程yield的返回值

coroutine.resume(co,var3)			//恢复协程,逻辑流从协程body上一个yield处开始,var3传为ret3
									//表达式没有左值,因此不再接收协程状态和协程body yield返回的值
           

4, xLua和Unity协程的混合使用

Unity的协程使用非常简单

void Update()
{
    if (Input.GetKeyDown("f")) 
    {
        StartCoroutine("Fade");
    }
}
IEnumerator Fade() 
{
    for (float ft = 1f; ft >= 0; ft -= 0.1f) 
    {
        Color c = renderer.material.color;
        c.a = ft;
        renderer.material.color = c;
        yield return null;
    }
}
           

Lua协程虽然已经可以满足逻辑流的挂起与恢复功能以实现异步的效果,但是在有些场景必须需要Unity协程的辅助,例如在Lua协程body中挂起等下一帧或n秒后再运行下一行代码。

任何继承MonoBehaviour的脚本都可以调用StartCoroutine方法开启协程。在xLua中可以以N种方法拿到继承它的CS脚本实例(Unity Component),然后在C#部分调用StartCoroutine方法。

Lua协程与Unity协程混合使用的基本思路:

Lua主线程开启Lua协程==>Lua协程挂起==>Lua主线程开启Unity协程 ==>Unity协程yield(条件)==>Unity协程中调用Lua主线程回调函数==>Lua主线程回调函数中恢复Lua协程==>…

Lua协程不像Unity协程可以自动管理挂起协程的恢复,因此需要在一个回调函数中手动resume挂起的Lua协程,Lua支持函数作为参数,因此可以将回调方法传入C#,C#再以Action方式调用。Lua主线程开启Unity协程后,当Unity协程走到第一个yield处时,Lua主线程继续执行代码,将之后Lua协程的恢复交给了Unity协程。这样不需要任何辅助代码,简单实现了一个Lua协程根据Unity协程YieldInstruction挂起与恢复的功能。

C#部分(未测试)

public void YieldAndCallback(object unityCondition, Action callback)
    {
        StartCoroutine(CoBody(unityCondition, callback));
    }

    private IEnumerator CoBody(object unityCondition, Action callback)
    {
        if (unityCondition is IEnumerator)
            yield return StartCoroutine((IEnumerator)unityCondition);
        else
            yield return unityCondition;

        callback();
    }

           

Lua部分(未测试)

--self.cb要引用到C#实例的YieldAndCallback方法

function LuaCoroutine:CallBack(...)
	self:MoveNext()
end

function LuaCoroutine:MoveNext()
	local status, unityCondition = coroutine.resume(self.CoBody)
    self.cb(unityCondition, handler(self.CallBack, self))
end

function LuaCoroutine:TestFunction()
	--do something 1
	coroutine.yield(CS.UnityEngine.WaitForSeconds(0.5))
	--do something 2
	coroutine.yield()
	--do something 3
end

function LuaCoroutine:TestStart()
	self.CoBody = coroutine.create(self.TestFunction)
	self:MoveNext()
end


function handler(method, obj)
    return function(...)
        if obj==nil then
            return method(...)
        else
            return method(obj, ...)
        end
    end
end
           

xLua官方的协程实现可以在不使用lua协程将协程体函数作为参数传给一个util.cs_generator,在协程体内可直接yield Unity YieldInstruction,util.cs_generator利用了一些底层机制隐藏了相关机制的一些细节。

参考:

http://stackoverflow.com/questions/12932306/how-does-startcoroutine-yield-return-pattern-really-work-in-unity

–James McMahon

C#高级编程第九版 --C.Nagel等

维护日志:

2019-12-25 修改,添加内容

继续阅读