本文主要介绍了ToLua框架下C#与Lua代码之间的互调操作,具有很好的参考价值。希望对大家有帮助。来和边肖一起看看吧。
Lua是目前国内使用最广泛的语言,有很多基于Lua的热更新框架。最近学习了ToLua的热更新框架,主要问题是C#和Lua的互调,我就把我的学习做个记录,以备将来参考。
所谓“互调”当然包括两个方面,一个是通过C#调用Lua代码,一个是通过Lua代码调用C#脚本,第二点也包括C#脚本中注册的Unity对象。
1. ToLua的简单实现原理
ToLua框架主要通过静态绑定实现C#和Lua的交互。基本原理是构建一个Lua虚拟机来映射C#脚本,然后通过这个虚拟机运行Lua脚本。Lua脚本运行时,可以通过虚拟机依次调用C#脚本中注册的对象。这种方法的优点是比使用反射uLua更有效。
在ToLua框架下,实现可以分为三个部分:通用Unity C#部分、ToLua虚拟机部分和Lua脚本部分。结构见下图:
甲苯结构
目前国内手游需要更新的主要逻辑框架和组件功能一般都是用C#实现的,而具体的功能和调用都放在Lua中。因为C#不能打包成AssetBundle,所以不能通过AssetBundle修改代码。但是,Lua是一种即时编译语言,可以打包成AssetBundle。当需要修改简单函数时,可以通过AssetBundle更新Lua代码。
2. ToLua的下载的安装
首先是下载地址:
托卢阿
这是作者的github地址。进入后,点击下载Zip。完成后解压到需要的目录,然后用Unity打开。
点击压缩文件下载。
第一次打开项目时,会提示您是否需要自动生成注册文件。新手可以选择直接生成。如果选择取消,您也可以在编辑器菜单中手动注册它。3354这是一个很重要的操作,后面会提到。
开始下面关于用法的文字。
3. ToLua的基本使用
ToLua的基本实现前面已经提到了,这里可以进一步细化:创建虚拟机——绑定数据——,调用Lua代码。这组步骤在框架附带的示例中也非常清楚。
首先,除了示例之外,实现这三个步骤。
ToLua虚拟机的创建非常简单,只需要创建一个新的LuaState即可。我们可以构建一个C#脚本作为入口,引用LuaInterface命名空间,输入以下代码,将文件挂载到场景中的一个空对象上。
使用LuaInterface
使用UnityEngine
公共类LuaScene : MonoBehaviour
{
字符串luaString=@ '
Print(“这是一个使用DoString的Lua程序”)
;
string luaFile=' LuaStudy
LuaState州;
无效开始()
{
state=new Lua state();//创建Lua虚拟机
状态。start();//启动虚拟机
//用字符串调用Lua
状态。do string(Lua string);
//用文件调用Lua
//手动添加lua文件搜索地址
string scene file=application . data path '/Lua study ';
状态。AddSearchPath(scene file);
状态。DoFile(Lua file);
状态。require(Lua file);
状态。dispose();//使用后回收虚拟机
调试。LogFormat('当前虚拟机状态:{0} ',null==state);//验证虚拟机状态
}
}
这里使用的Lua脚本也很简单。
打印(“这是一个使用DoFile的Lua程序”)
Lua挂载
ToLua直接调用Lua代码有两种方式,一种是DoString,一种是DoFile;此外,还有一个必需方法。这种方法不同于前两种方法。ToLua会将调用的Lua文件加载到Lua栈中,而前两个方法只运行一次,运行后保存在缓存中。虽然以后可以调用它们,但是它们会的。
请注意,在上面的代码中,当使用DoFile和Require方法时,您需要手动将文件搜索位置添加到目标文件中。
运行结果如下:
Lua运行结果
最后,用完后记得清理虚拟机。我用null==state来判断,最后输出“true”,表示调用LuaState后虚拟机已经被清理了。处置()。
4. C#中调用Lua变量/函数
上面我们实现了C#调用Lua文件和字符串。其实对于ToLua来说,直接调用string和file没有本质区别,最终都会转换成byte[]进行加载。
接下来,实现调用ToLua来指定Lua变量和函数。这里,Lua代码是通过一个文件导入的。
首先是我们的Lua代码。这个Lua代码有一个公共变量,一个带函数的表,一个不带参数的函数和一个带参数的函数。它的功能很简单,这段代码里不调用。
数量=0
mytable={1,2,3,4}
mytable.tableFunc=function()
Print('调用table func ');
结束
函数计数()
数字=数字1
打印('计数器1,当前计数器是'.数字)
退货数量;
结束
函数输入值(参数)
Print ('[Call: ]InputValue方法在Lua中传入了参数:'.tostring( param))
结束
然后就是C#代码,还是一样的套路。首先,创建一个虚拟机并读入Lua文件。普通变量、无参数函数、无参数函数、表的调用将依次讲解。
注意:如果有本地的logo,在C#中是无法直接获取的。
普通变量
普通变量的调用很简单。文件加载后,可以直接以LuaState[string]的形式获取,也可以直接用这个表达式赋值。
无参函数
调用的函数有两种方式,一种是在调用之前缓存为LuaFunction类型,另一种是直接调用Call方法。
有参函数
参数调用和参数调用的区别在于参数的传递。在ToLua中,重载了大量的参数调用函数,与参数调用的调用方法相同。参数调用也有两种调用方式。这里详细解释了传递参数的不同方式。
传入参和调用分离。
一般这种方法需要先将函数缓存为LuaFunction,然后用BeginPcall方法标记函数,再用Push或PushArgs方法将参数传入函数,最后调用Pcall,或者调用EndPcall标记结束。
//将参数传入方法
LuaFunction valueFunc=state。GetFunction(' input value ');
valueFuncbegin call();
ValueFuncPush(' - push push方法从C#-')传入参数;
valueFuncPCall();
调用时,直接传入参数。
这是最符合逻辑的操作方式,但是当你看实现代码的时候,你会发现它其实只是一组封装在LuaFunction中的实现,本质和前面的一样。
4.table
是表lua里的百宝箱。一切都可以过去。表中可以有普通的变量、表和方法。
在ToLua中,分析了表的数据结构,并实现了多种方法。在这里,table可以作为LuaState来操作,两者没有区别。
以下是完整的C#代码,并附上运行结果。
使用LuaInterface
使用UnityEngine
公共类LuaAccess : MonoBehaviour
{
string luaFile=' LuaAccess
LuaState州;
无效开始()
{
state=new Lua state();
状态。start();
//用文件调用Lua
//手动添加lua文件搜索地址
string scene file=application . data path '/Lua study ';
状态。AddSearchPath(scene file);
状态。require(Lua file);//加载文件
//获取Lua变量
调试。Log('获取文件中的变量:' state[' num ']);
state[' num ']=10;
调试。Log('设置文件中的变量是:' state[' num ']);
//调用Lua方法
luaFunc=状态。GetFunction(' Count ');
luaFunc。call();
调试。Log('C#调用LuaFunc,函数返回值:' state[' num ']);
调试。Log('C#直接调用Count方法。);
状态。Call('Count ',false);
//将参数传入方法
LuaFunction valueFunc=state。GetFunction(' input value ');
valueFuncbegin call();
ValueFuncPush(' - push push方法从C#-')传入参数;
valueFuncPCall();
valueFuncendp call();
ValueFuncCall(' -直接调用方法从C#-')传入参数;
//获取LuaTable
LuaTable table=state。GetTable(' my table ');
桌子。调用(' table func ');
LuaFunction tableFunc=table。getluafunch(' table func ');
调试。Log('C#调用表中的func ');
tableFunc。call();
调试。Log('获取table中的num值:' table[' num ']);
//直接通过下标获取
for(int I=0;我表。长度;我)
{
调试。Log('获取table的值:' table[I]);
}
//转换为LuaDictTable
表。todict table();
foreach(可口述的变量项)
{
调试。LogFormat('遍历表:{0}-{1} ',item.key,item . value);
}
状态。dispose();
}
}
Lua访问变量
5. Lua中调用C#方法/变量
之前在@ Rorschach L的文章里看过一个关于lua调用C#的笔记,但是总觉得少了点什么,所以自己做笔记的时候特别注意具体的实现。
在@ Rorschach L的文章中,将一个C#对象作为参数传入列表,然后直接在Lua代码中运行对应的方法名。少了几个关键步骤。如果只进行这些步骤,Lua中的引用是无法实现的。
首先,我们先从实现原理说起。在文章的第一部分,我提到了ToLua的基本实现思想,并将系统分为三个部分。在这三个部分中,ToLua作为沟通Lua脚本和C#的桥梁。我们知道Lua的本质是通过字节码封装C,特点是即时编译。从C#或其他语言调用Lua并不太难。你只需要提前约定好一个具体的方法名,然后加载脚本,但是C#需要提前编译。如何通过解释器调用C#中的对象是主要难点。ToLua实现了这两个功能。
从这方面来说,我觉得大多数人会想到的最直接的实现思路,大概都是通过反思来实现的,uLua也是通过反思来实现的,但是反思的效率很低。虽然可以实现,但是问题还是很明显的。
ToLua通过绑定方法名来实现这种映射。首先构造一个Lua虚拟机,虚拟机启动后绑定需要的方法。当虚拟机运行时,可以在Lua中调用特定的方法。虚拟机实现了伪装的解释器的功能。当Lua调用特定的方法和对象时,虚拟机在绑定的方法中找到对应的C#方法和对象进行操作,ToLua已经自动实现了一些绑定的方法。
基本原理大概了解以后,我们就可以来看看它的具体实现了。
第一步是设置并启动虚拟机。为了实现Lua对C#的调用,首先我们要调用binding方法,所以我们的代码变成如下。正如你所看到的,这里和之前唯一的不同是添加了LuaBinder。绑定(状态)方法。这个方法里面其实是很多定义好的方法的绑定,也就是上面说的绑定方法。
使用LuaInterface
使用UnityEngine
公共类CSharpAccess : MonoBehaviour
{
私有字符串luaFile=' LuaCall
LuaState州;
无效开始()
{
state=new Lua state();
状态。start();
string scene file=application . data path '/Lua study ';
状态。AddSearchPath(scene file);
//注册方法调用
卢阿宾德。Bind(状态);
状态。require(Lua file);//加载文件
}
}
然后我们添加一个变量和一个方法。我们要实现的是在Lua中完成对这个方法和变量的调用。
公共字符串AccessVar='这是初始值';
公共void PrintArg(字符串参数)
{
调试。Log('C#输出变量值:' arg ');
}
有了目标方法之后,我们将把这个变量和方法绑定到虚拟机中。
查看LuaState的实现代码可以发现,绑定有三种方法:RegFunction、RegVar和RegConstant,分别用于绑定函数/委托、变量和常量。在这里,ToLua通过一个委托实现方法的映射。这个委托需要传入一个IntPtr类型的luaState变量。这个变量的本质是一个句柄。在实际操作中,虚拟机将作为变量传入。
公共委托int LuaCSFunction(int ptr Lua state);
public void RegFunction(字符串名,LuaCSFunction func);
public void RegVar(字符串名,LuaCSFunction get,LuaCSFunction set);
public void RegConstant(字符串名,双d);
public void RegConstant(字符串名,bool标志);
总结一下几个方法的特点:
这些方法都需要传入一个字符串名,这个字符串名就是稍后在Lua中要调用的变量或方法名。
RegConstant方法很简单,只需传入一个名称,然后传入一个常数;
函数和RegVar都是由LuaCSFunction类型的委托实现的;
正则函数需要一个LuaCSFunction委托,这个委托需要对原方法重新进行一次实现;
雷瓦尔除了名字之外,还需要两个LuaCSFunction委托,可以理解为一个变量的获取/设置方法,如果只有得到或设置,另一个留空即可。
接下来我们对AccessVar和PrintArg方法进行一下LuaCSFunction形式的实现。
private int PrintCall(System .IntPtr L)
{
尝试
{
托卢阿CheckArgsCount(L,2);//对参数进行校验
csharp access obj=(csharp access)ToLua .CheckObject(L,1,类型为(csharp访问));//获取目标对象并转换格式
string arg0=ToLua .CheckString(L,2);//获取特定值
obj .PrintArg(arg 0);//调用对象方法
返回1;
}
接住(系统。例外情况e)
{
返回Lua dll。total _ exception(L,e);
}
}
private int GetAccesVar(System .IntPtr L)
{
对象o=空
尝试
{
o=托卢阿ToObject(L,1);//获得变量实例
cs harp access obj=(cs harp access)o;//转换目标格式
字符串ret=obj .AccessVar//获取目标值
托卢阿Push(L,ret);//将目标对象传入虚拟机
返回1;
}
接住(系统。例外情况e)
{
回报鲁阿德。tol ual _ exception(L,e,o,’尝试在零值上索引访问var’);
}
}
private int SetAccesVar(System .IntPtr L)
{
对象o=空
尝试
{
o=托卢阿ToObject(L,1);//获得变量实例
cs harp access obj=(cs harp access)o;//转换目标格式
obj .AccessVar=ToLua .ToString(L,2);//将要修改的值进行设定,注意这里如果是值类型可能会出现拆装箱
返回1;
}
接住(系统。例外情况e)
{
回报鲁阿德。tol ual _ exception(L,e,o,’尝试在零值上索引访问var’);
}
}
可以看到这三个方法的格式都是一致的,通用的步骤如下:
使用托卢阿中的方法对L句柄进行校验,出现异常则抛出,本例中使用托卢阿CheckArgsCount方法;
获得目标类的实例,并转换格式,具体转换方法较多,可以根据需要在托卢阿类中选择,本例中使用了托卢阿。检查对象和托卢阿。对象等方法;
调用对应方法,不同的方法调用略有区别。
值得注意的是,在托卢阿的ToObjectQuat、ToObjectVec2等获取值类型的方法中,会出现拆装箱的情况。
下一步将几个方法注册进左上臂虚拟机。
注意这里有两对方法,分别是开始模块\结束模块和BeginClass\EndClass,BeginModule\EndModule用于绑定命名空间,可以逐层嵌套;而开始类\结束类用于开启具体的类型空间,具体的方法和变量绑定必须在这成对的方法之中,否则会导致托卢阿崩溃(百试百灵,别问我怎么知道的)。
私有空的绑定(卢阿塞特L)
{
长度开始模块(空);
长度BeginClass(typeof(CSharpAccess),typeof(UnityEngine .MonoBehaviour));
状态RegFunction('Debug ',打印调用);
状态RegVar('AccessVar ',GetAccesVar,SetAccesVar);
长度end class();
长度EndModule();
}
最后是我们的左上臂代码,非常简单,注意调试和AccessVar调用的区别。
打印('-进入左上臂调用- ')
本地go=UnityEngine .游戏对象。查找(' LuaScene ')
本地访问=go:get组件(“csharp访问”)
访问:调试(' Lua调用C#方法)
访问AccessVar=' -这是修改值- '
打印('- Lua调用结束- ')
完整C#代码
使用Lua接口
使用联合工程
公共类CSharpAccess : MonoBehaviour
{
私有字符串luaFile=' LuaCall
卢阿塞特州;
无效开始()
{
state=new Lua state();
状态. start();
字符串场景文件=应用程序。数据路径'/Lua研究';
状态AddSearchPath(场景文件);
//注册方法调用
卢阿宾德绑定(状态);
绑定(状态);
调试。日志('访问变量初始值:' access var);
状态. require(Lua文件);//载入文件
调试。日志(' C#查看:' access var);
状态dispose();
}
私有空的绑定(卢阿塞特L)
{
长度开始模块(空);
长度BeginClass(typeof(CSharpAccess),typeof(UnityEngine .MonoBehaviour));
状态RegFunction('Debug ',打印调用);
状态RegVar('AccessVar ',GetAccesVar,SetAccesVar);
长度end class();
长度EndModule();
}
private int PrintCall(System .IntPtr L)
{
尝试
{
托卢阿。CheckArgsCount(L,2);//检查参数
csharp access obj=(csharp access)ToLua。CheckObject(L,1,type of(csharp access));//获取目标对象并转换格式
string arg0=ToLua。CheckString(L,2);//获取特定值
obj。PrintArg(arg 0);//调用对象方法
返回1;
}
catch(系统。例外情况e)
{
返回Lua dll . total _ exception(L,e);
}
}
公共void PrintArg(字符串参数)
{
调试。Log('C#输出变量值:' arg ');
}
【系统。未序列化]
公共字符串AccessVar='这是初始值';
private int GetAccesVar(System。IntPtr L)
{
对象o=null
尝试
{
o=托卢阿。ToObject(L,1);//获取变量实例
csharp access obj=(csharp access)o;//转换目标格式
字符串ret=obj。AccessVar//获取目标值
托卢阿。Push(L,ret);//将目标对象传入虚拟机
返回1;
}
catch(系统。例外情况e)
{
return Lu adll . tol ual _ exception(L,e,o,'尝试在零值上索引access var ');
}
}
private int SetAccesVar(System。IntPtr L)
{
对象o=null
尝试
{
o=托卢阿。ToObject(L,1);//获取变量实例
csharp access obj=(csharp access)o;//转换目标格式
obj。AccessVar=ToLua。ToString(L,2);//设置要修改的值。
返回1;
}
catch(系统。例外情况e)
{
return Lu adll . tol ual _ exception(L,e,o,'尝试在零值上索引access var ');
}
}
}
运行结果
Lua调用C#
最后,让我们回到这一节的开头。@罗夏L的文章哪里出错了?
我在lua中添加了一行access:PrintArg('PrintArg ')调用方法,发现Unity报错了这个:
直接调用方法名时出错。png
说明不能简单的这么做就直接调用方法。仔细读完文章,我发现他提到了这样一件事:
首先要做的是把自己的类放到CustomSettings中,这个类是CallLuafunction。
BindType[]自定义类型列表
放在这个数组里注册给lua使用。
他在这里没有详细说明吗?我找到了这个课,发现这个课里面记录了很多Unity自己的课。这让我想起了第一次启动Lua时的提示,心中生出一个疑问:这些数据是用来自动注册生成类的吗?
//在此添加要导出并注册到lua的类型列表。
公共静态绑定类型[]自定义类型列表=
{
_ GT(type of(luaeinjectionstation)),
_GT(typeof(InjectType)),
_GT(调试器类型)。SetNameSpace(null),
.以下部分省略。
沿着调用链,我找到了这个变量的引用,果然,最多的数据是用于类型注册的。
我把这个类放在数组末尾,点击清除换行文件,完成后马上弹出自动生成数据的对话框。点击确定,
重新生成注册
自动生成
接下来,我重新运行lua脚本:
打印('-输入Lua调用-')
本地go=UnityEngine。GameObject.Find('LuaScene ')
local access=go:get component(' csharp access ')
访问:调试(“Lua调用C#方法”)
访问。AccessVar=' -这是修改后的值-'
打印('Lua调用结束-')
访问:PrintArg('PrintArg ')
运行成功。
成功的运行表明,ToLua已经实现了一套完整的绑定方案,只需要配置需要的内容即可。
6.总结
本来只是想写一个简单的调用方法,最后写了一篇很长的文章,但是从计划开始到整篇文章结束用了将近一整天的时间。
即便如此,收获还是很大的,使用这套工具的熟练程度已经上了一个台阶,以后要加强总结。
以上ToLua框架下C#与Lua代码的互调操作,就是边肖分享的全部内容。希望给大家一个参考,多多支持我们。
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。