天天看點

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 修改,添加内容

繼續閱讀