C#协程和Lua协程的交互原理
在Unity游戏开发中,C#语言+lua语言是一种常见的开发模式。本文主要研究下C#协程和lua协程的交互的基本原理。
C#的协程
- C#协程本质上是一个迭代器。 (后面可能再补充)
- yield return是C#提供的语法糖,利用这个语法糖可以不显式地定义迭代器。
- C#的协程是非抢占式的,需要主动调用迭代器的MoveNext方法,在Unity中则是引擎负责调用。
- 在Unity中,协程的调用由引擎管理。开启一个协程时将对应迭代器加入队列中,每一帧时检查迭代器的Current变量是否满足条件(比如定时结束、文件下载完成),如果满足则调用MoveNext方法,协程即往下执行。
lua的协程
- lua的协程本质上是一个函数。
- lua的协程也是非抢占式的。调用coroutine.resume()恢复协程,coroutine.yield()挂起协程。
- lua的协程管理需要自己处理,一般是一个主协程+n个次协程,大多数时间在主协程上执行。
在lua中,结合C#协程实现定时功能
上代码
main.lua
local yield_return = (require "util.cs_coroutine").yield_return
function Test1()
local co = coroutine.wrap(function()
for i = 1, 10 do
yield_return(CS.UnityEngine.WaitForSeconds(1)) -- 定时1s
print(i)
end
end)
co() -- 开启协程
end
function Test2()
local co = coroutine.create(function()
print('coroutine start!')
local s = os.time()
yield_return(CS.UnityEngine.WaitForSeconds(3)) -- 定时3s
print('wait interval:', os.time() - s)
local www = CS.UnityEngine.WWW('http://www.u3d8.com')
yield_return(www) -- 等待文件下载完成
if not www.error then
print(www.bytes)
else
print('error:', www.error)
end
end)
coroutine.resume(co)
end
Test1()
Test2()
cs_coroutine.lua
local util = require 'util.luautil'
local gameobject = CS.UnityEngine.GameObject('Coroutine_Runner')
CS.UnityEngine.Object.DontDestroyOnLoad(gameobject)
local cs_coroutine_runner = gameobject:AddComponent(typeof(CS.Coroutine_Runner))
local function async_yield_return(to_yield, cb) -- async_yield_return是异步的
cs_coroutine_runner:YieldAndCallback(to_yield, cb)
end
return {
-- yield_return是同步的(看上去是同步的)
yield_return = util.async_to_sync(async_yield_return)
}
luautil.lua
--传入一个async方法
local function async_to_sync(async_func, callback_pos)
--返回一个sync方法
return function(...)
local _co = coroutine.running() or error ('this function must be run in coroutine')
local rets
local waiting = false
--定义 完成时的回调
local function cb_func(...)
if waiting then
--等待后回调的情况
--从挂起的位置恢复协程,并传递结果
assert(coroutine.resume(_co, ...))
else
--未等待就直接回调的情况,直接设置结果
rets = {...}
end
end
--参数列表 把 “回调” 加入参数列表(默认加入最后)
local params = {...}
table.insert(params, callback_pos or (#params + 1), cb_func)
--调用异步方法,把 “通过同步方法传入的参数” 和 “回调” 全部传给异步方法。
async_func(unpack(params))
--如果没有立即返回,则标记为等待并挂起
if rets == nil then
waiting = true
--挂起! "coroutine.yield()" 的返回值为 coroutine.resume 传入的第二个参数
rets = {coroutine.yield()}
end
--将结果同步返回
return unpack(rets)
end
end
Coroutine_Runner.cs
using UnityEngine;
using System.Collections;
using System;
public class Coroutine_Runner : MonoBehaviour
{
public void YieldAndCallback(object to_yield, Action callback)
{
StartCoroutine(CoBody(to_yield, callback)); //开启C#协程
}
private IEnumerator CoBody(object to_yield, Action callback)
{
//to_yield就是lua中yield_return函数的参数
//在Test1()例子中就是CS.UnityEngine.WaitForSeconds(1)
if (to_yield is IEnumerator)
yield return StartCoroutine((IEnumerator)to_yield);
else
yield return to_yield;
callback(); //协程结束后调用回调,就是上面lua中的cb_func,恢复lua协程
}
}
总的来说,基本原理就是:
在创建一个lua协程,异步操作开始后将lua协程挂起,等异步操作完成调用回调方法后恢复lua协程。异步操作在C#协程中。变异步调用为同步等待。
带来的好处就是代码看起来同步了。当然缺点也是显而易见的,每一次调用yield_return函数都会创建一个C#协程;创建协程(无论lua还是C#)都会造成gc;而且创建的C#协程还不能主动中断。
lua协程替换C#协程
上调用代码
main.lua
function Test()
local start = (require "util.cs_coroutine").start
start(function()
print('coroutine start!')
local s = os.time()
coroutine.yield(CS.UnityEngine.WaitForSeconds(3)) -- 定时3s
print('wait interval:', os.time() - s)
local www = CS.UnityEngine.WWW('http://www.u3d8.com')
coroutine.yield(www) -- 等待文件下载完成
if not www.error then
print(www.bytes)
else
print('error:', www.error)
end
end)
end
Test()
cs_couroutine.lua
local util = require 'util.luautil'
local gameobject = CS.UnityEngine.GameObject('Coroutine_Runner')
CS.UnityEngine.Object.DontDestroyOnLoad(gameobject)
local cs_coroutine_runner = gameobject:AddComponent(typeof(CS.Coroutine_Runner))
return {
start = function(...)
return cs_coroutine_runner:StartCoroutine(util.cs_generator(...))
end;
stop = function(coroutine)
cs_coroutine_runner:StopCoroutine(coroutine)
end
}
cs_coroutine_runner:StartCoroutine()需要传入一个IEnumerator参数
public interface IEnumerator
{
object Current {get;}
bool MoveNext();
void Reset();
}
所以要在lua侧构造一个类IEnumerator
luautil.lua
local move_end = {}
local generator_mt = {
__index = {
MoveNext = function(self)
-- 每次C#侧调用MoveNext时,调用lua侧的coroutine.resume()恢复协程,
-- 通过coroutine.yield或者return返回Current
self.Current = self.co()
if self.Current == move_end then -- lua协程已结束
self.Current = nil
return false
else
return true
end
end;
Reset = function(self)
self.co = coroutine.wrap(self.w_func) -- 创建lua协程
end
}
}
local function cs_generator(func, ...)
local params = {...}
local generator = setmetatable({
w_func = function()
func(unpack(params))
return move_end -- lua协程结束时返回move_end
end
}, generator_mt)
generator:Reset()
return generator
end
return {
cs_generator = cs_generator
}
与上面的方法相比,在lua侧直接构造C#协程,无论协程多复杂(coroutine.yield的次数),都只有一个lua协程和一个C#协程,而且C#协程可以主动中断。
总结
Unity的C#协程提供了处理异步操作的一种方法,可以使代码看起来更连贯,易于理解。通过一定的交互手段,结合lua的协程,可以在lua侧实现相同的效果。