|
前言
用了2年多的Lua了,却不知道C#是怎么调用到Lua的细节和底层原理,特此学习一下。 C#调用Lua,网上有很多种解决方案,比如slua、ulua、tolua等等等等,我们项目用的是tolua,据说是性能就好的一种解决方案(具体性能测试见Unity项目常见Lua解决方案性能比较),接下来我们先来了解一下tolua
tolua是Unity静态绑定lua的一个解决方案,它通过C#提供的反射信息分析代码并生成包装的类(Wrap.cs文件)。
它是一个用来简化在C#中集成Lua的插件,可以自动生成用于在Lua中访问Unity的绑定代码,并把C#中的常量、变量、函数、属性、类以及枚举暴露给Lua。
它是从cstolua衍变而来。从它的名字可以看出,它是集成了原来的ToLua代码通过二次封装写了一个C#与tolua C 的一个中间层。
tolua 源码: topameng/tolua 下面详细介绍一下C#和Lua的交互细节
一、C#调用Lua
大体流程图
C#调用Lua VM大体流程
1.1 编译tolua.dll
先上一下unity C#和lua交互的流程图,我们项目用的是tolua,unity C#先是生成tolua C#(warp文件),然后通过这些自动生成的tolua C#和tolua C交互。
tolua C 起到承上启下的作用,是C#和lua的中间层,在和C#交互方面,作为非c#托管代码,会提供一些函数让c# DllImport,c#会通过Marshal等与非托管代码交互
还有一些lua的扩展库,比如 cjson、LuaSocket、sqlite3、lpeg、bit、pbc等手机游戏常用库,这些库扩展了lua的能力,具体编译过程见 如何编译各平台使用的库-以编译tolua为例 这边不足赘述。
windows平台叫做tolua.dll,android叫做libtolua.so,mac平台叫tolua.bundle,而iOS平台由于不允许使用动态库,所以会编译成静态库libtolua.a。
编译出tolua.dll,复制到unity的Plugins文件夹下,然后就可以在C#层以DllImport导入接口,然后就可以在C#层调用到C层的方法了。
1.2 C#调用Lua文件
创建虚拟机——绑定数据——调用Lua代码 //C# 层调用 lua层 demo
void Start()
{
LuaState m_LuaState= new LuaState();//建立Lua虚拟机
m_LuaState.Start();//启动虚拟机
//1. 使用string调用Lua
string luaString = @"print('这是一个使用DoString的Lua程序')";
m_LuaState.DoString(luaString);
//2. 使用文件调用Lua
//手动添加一个lua文件搜索地址
m_LuaState.AddSearchPath(string.Format("{0}/{1}", Application.dataPath, "Lua"));
//读取main.lua文件
m_LuaState.DoFile("main");// 方法1
m_LuaState.Require(luaFile);// 方法2
m_LuaState.Dispose();//使用完毕回收虚拟机
Debug.LogFormat("当前虚拟机状态:{0}", null == m_LuaState);//验证虚拟机状态
}
ToLua直接调用Lua代码的方式有两种:DoString、DoFile。这两个只是运行一次,运行之后保存在缓存中
此外还有一个Require方法,这个方法和前两个方法不同的是,ToLua会将调用的Lua文件载入Lua栈中
在上述代码中要注意,使用DoFile和Require方法时,要手动给目标文件添加一个文件搜索位置。
1.3 C#中调用Lua变量/函数
上面实现了C#调用Lua文件和string,其实对于ToLua而且,直接调用string和文件并没有本质区别,最后都会转换成byte[]进行载入。
接下来实现一下ToLua调用指定Lua变量和函数
//找到mian方法并调用
LuaFunction main = m_LuaState.GetFunction("main");
main.Call();
main.Dispose();
main = null;二、Lua调用C#
这个有一篇图文并茂的文章写得特别好:【Unity游戏开发】tolua之wrap文件的原理与使用
ToLua是通过方法名绑定的方式来实现这个映射的,
首先构造一个Lua虚拟机(LuaState state = new LuaState();),
在虚拟机启动后对所需的方法进行绑定(LuaBinder.Bind(state)),
在虚拟机运行时可以在Lua中调用特定方法,虚拟机变相地实现了一个解释器的功能,
在Lua调用特定方法和对象时,虚拟机会在已绑定的方法中找到对应的C#方法和对象进行操作 2.1 生成wrap文件
首先将自己写的类添加到CustomSettings.cs文件的customTypeList数组里,然后点击unity菜单栏Lua-GenerateAll按钮,就会生成自己写的类对应的wrap文件。具体的generate过程,下面这篇文章有具体分析,tolua#是Unity静态绑定lua的一个解决方案
生成的Wrap文件分析:
public static class LuaBinder
{
public static void Bind(LuaState L)
{
...
UnityEngine_RectTransformWrap.Register(L);
...
}
}
public class UnityEngine_GameObjectWrap
{
public static void Register(LuaState L)
{
L.BeginClass(typeof(UnityEngine.RectTransform), typeof(UnityEngine.Transform));
...
L.RegFunction("GetComponent", GetComponent);// 绑定方法
L.RegVar("transform", get_transform, null);//绑定变量
...
L.EndClass();
}
}
public void RegFunction(string name, LuaCSFunction func)
{
IntPtr fn = Marshal.GetFunctionPointerForDelegate(func);
LuaDLL.tolua_function(L, name, fn);
}
tolua.dll
LUALIB_API void tolua_function(lua_State *L, const char *name, lua_CFunction fn)
{
lua_pushstring(L, name);
tolua_pushcfunction(L, fn);
lua_rawset(L, -3);
/*lua_pushstring(L, name);
lua_pushcfunction(L, fn);
lua_rawset(L, -3);*/
}
生成Wrap文件自动生成一个Register方法供LuaBinder调用,然后讲需要暴露给lua的方法变量,通过调用tolua.dll这个润滑层的方法tolua_function,最后调用到lapi.c的方法lua_pushstring,将方法注册到lua虚拟机
2.2 lua调用Wrap文件,访问C#
接下来,以常见的写法gameobj.transform.position = pos进行分析,看lua层写下这一行代码,发生了什么。像这样的写法,在unity中是再常见不过的事情,但是在lua中,大量使用这种写法是非常糟糕的。为什么呢?
因为短短一行代码,却发生了非常非常多的事情,为了更直观一点,我们把这行代码调用过的关键luaApi以及ToLua相关的关键步骤列出来(以ToLua+cstolua导出为准,gameobj是GameObject类型,pos是Vector3): 第一步:.transform.position
UnityEngine_GameObjectWrap.get_transform lua想从gameobj拿到transform,对应gameobj.transform
LuaDLL.luanet_rawnetobj 把lua中的gameobj变成c#可以辨认的id
ObjectTranslator.TryGetValue 用这个id,从ObjectTranslator中获取c#的gameobject对象
gameobject.transform 准备这么多,这里终于真正执行c#获取gameobject.transform了
ObjectTranslator.AddObject 给transform分配一个id,这个id会在lua中用来代表这个transform,
transform要保存到ObjectTranslator供未来查找
LuaDLL.luanet_newudata 在lua分配一个userdata,把id存进去,用来表示即将返回给lua的transform
LuaDLL.lua_setmetatable 给这个userdata附上metatable,让你可以transform.position这样使用它
LuaDLL.lua_pushvalue 返回transform,后面做些收尾
LuaDLL.lua_rawseti
LuaDLL.lua_remove
第二步:= pos
TransformWrap.set_position lua想把pos设置到transform.position
LuaDLL.luanet_rawnetobj 把lua中的transform变成c#可以辨认的id
ObjectTranslator.TryGetValue 用这个id,从ObjectTranslator中获取c#的transform对象
LuaDLL.tolua_getfloat3 从lua中拿到Vector3的3个float值返回给c#
lua_getfield + lua_tonumber 3次 拿xyz的值,退栈
lua_pop
transform.position = new Vector3(x,y,z) 准备了这么多,终于执行transform.position = pos赋值了
这大概就是我当时刚开始写lua,天天被主程锤的主要原因吧!随便一个.transform就有如此多的操作,所以我们封装了一个LuaHelper中间层,用来处理lua和C#的交互,一些lua想要调用C#层的东西都会封装到这个类里,供lua层使用,比如这个position,我们就封装了
public static void SetLocalPositionEx(this Component cmpt, float x, float y, float z)
{
cmpt.transform.localPosition = new Vector3(x, y, z);
}
这样就实力将第一步的漫长步骤砍掉了,砍掉的还有 lua Vector3的3个float值返回给c#,下面的文章有实际的测试,可以减少66%的时间 用好Lua+Unity,让性能飞起来——Lua与C#交互篇
2.3 OjbectTranslator深入了解
简单来说就是C#中的对象在传给lua时并不是直接把对象暴露给了lua,而是在这个OjbectTranslator里面注册并返回一个索引(可以理解为windows编程中的句柄),并把这个索引包装成一个userdata传递给lua,并且设置元表 // tolua# 代码
static void PushUserData(IntPtr L, object o, int reference)
{
int index;
ObjectTranslator translator = ObjectTranslator.Get(L);
if(translator.Getudata(o, out index))
{
if(LuaDLL.tolua_pushudata(L, index))
{
return;
}
translator.Destroyudata(index);
}
index = translator.AddObject(o);
LuaDLL.tolua_pushnewudata(L, reference, index);
}
// tolua++ 代码
LUALIB_API void tolua_pushnewudata(lua_State* L, int metaRef, int index)
{
lua_getref(L, LUA_RIDX_UBOX);
tolua_newudata(L, index);
lua_getref(L, metaRef);
lua_setmetatable(L, -2);
lua_pushvalue(L, -1);
lua_rawseti(L, -3, index);
lua_remove(L, -2);
}
参考:
ToLua框架下C#与Lua代码的互调 |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|