闲谈国产游戏脚本渊源
端游时代一款《天龙八部》就支撑了整个畅游公司。它引擎改造于orge+cegui。当c++代码工程随着项目业务日益庞大时,编译速度就实在很令人发狂。而C++对于开发人员要求还是蛮高的,驾驭不好给项目稳定性带来很大风险。再则为了更好分离引擎和业务逻辑,所以引入Lua做脚本语言作了部分业务逻辑开发。包括《魔兽世界》、《大话西游2》也引入Lua脚本。
早期页游大多基于Js+html+css开发,以Mop的《猫游记》最为代表,然而这种开发模式在浏览器兼容性和脚本执行效率、Dom效率等诸多问题上难以支撑重量产品。而后Flash As3发布,高效的脚本执行效率和统一的浏览器标准迎来页游黄金时代,mmorpg明星产品《神仙道》,SNS游戏《偷菜》风靡大江南北。这二种页游开发模式都是基于web部署,就无需考虑引入其他脚本做热更模块。
移动浪潮迎来手游时代,对于iOS游戏而言,app store的审核周期漫长无比,再加上IOS不允许动态下发可执行代码(不支持JIT的硬件环境)。就必须引入脚本做bug修复和一些需要及时发布的业务模块。而Android应用发布平台混乱,本身设计机制上就可以相对自由的替换so、dex、dll达到热更目的。在游戏App领域Cocos引擎,引入js和lua热更方案。其他传统App热更就百花齐放,如被封杀闹得沸沸扬扬的JsPatch。还有大量线上环境验证过的阿里AndFix和微信Tinker。
手游时代还有一个不能忽略的是Html5游戏,国外引擎有Three.js、Pixi.js、PlayCanvas,这些国外引擎工具链不太完善,设计理念并不太符合做国产游戏开发模式。而国内做的比较完善的引擎有Cocos2d-JS、Egret、Layabox。这些主流的引擎还是运行在JavaScript上。渲染模式部分采用Canvas模式。也有采用webGL(JS版的OpenGL)这种性能更好的模式,而且浏览器对它支持也越来越普及。由于手机浏览器性能差异、接口缺陷、SDK接入、系统级调用等诸多问题。大多数H5游戏发布还是绑在特定的runtime上,如发布在微信小游戏平台(WebGL)、发布原生包。正因本质还是web机制,所以做热更还是很容易的。
最后说说WebAssembly,它是一种二进制格式的类汇编代码,可以被浏览器加载和并进一步编译成可执行的机器码,从而在浏览器运行。它还可以作为高级语言的编译目标,理论上任何语言都可以编译为 WebAssembly。它接近 native code 比 JS 快这是显然的,作为浏览器四大巨头google、apple、firefox、microsoft合作共谋的产物,前途一片光明。目前支持的游戏引擎有 Egret。而Unity在发布WebGl时也可以选择Linker Target为WebAssembly。
常见的Lua for Unity3d框架
Lua Interface是一个用于Lua语言和Microsoft .NET的公共语言运行时(CLR)之间集成的库,LuaInterface的作者后来开发出跨平台升级版本NLua。最早一批热更新框架Ulua(首发时间2014年3月,后期停止维护。目前另有人保留了一个分支版本ulua2)就是基于Lua Interface开发的,Tolua(git首发时间2014年的9月,最早叫CsTolua)又是从ulua演化来的,再后来的Slua(2015年初)、Xlua(2015年3月)可以说都是前辈思想的变种者。
还有ILRuntime采用C#热更,以后单做分析。
性能测评差异
这些Lua框架目前本质上都是静态绑定代码里调用Lua CAPI来实现C#、Lua、C交互,理论上没有巨大性能差异,但网上性能测评都是体现出了不少差异。差异主要体现在下面二个方面:
- Lua 的 C API 的调用频次。
- C#、Lua、C、C++之间数据(值类型、引用类型)传递的实现方式和选择,以及数据交换产生的GC问题。
早期Lua框架调用C#采用反射实现(因为Lua Interface就是反射实现),会有性能损耗(几个框架测评相互嘲讽的点),目前大多数框架都是采用Wrap方式生成胶水代码,因此这个性能差异基本抹平了。
这些测评用例基本测试方向是对Unity独有类型Vector2, Vector3,Vector4,Quaternion)使用、对象读写、相互函数调用、复杂类型传递等,下面就这些情况的实现方式分别讨论。
Xlua v2.1.12
Unity独有类型使用
1.构造Vector3默认的映射方案是Vector3 -> userdata,userdata(size=12)比table(64位空table size ~= 80)更省内存,但操作字段比table性能稍低。 因经过了C#直接操作内存,所以性能略有损失。其他Unity类型Color、Quaternion等都是采用这种方案。
核心Lua CAPI调用过程如下:
lua_tonumber:lua虚拟栈3次出栈拿到xyz
xlua_pushstruct:构造userdata
xlua_pack_float3: 写入到数据到userdata
下列代码是完整调用过程:
--lua端
CS.UnityEngine.Vector3(1998, 2010, 1984)
// UnityEngineVector3Wrap.cs Vector3构造函数
static int __CreateInstance(RealStatePtr L){
...
float _x = (float)LuaAPI.lua_tonumber(L, 2);
float _y = (float)LuaAPI.lua_tonumber(L, 3);
float _z = (float)LuaAPI.lua_tonumber(L, 4);
UnityEngine.Vector3 gen_ret = new UnityEngine.Vector3(_x, _y, _z);
translator.PushUnityEngineVector3(L, gen_ret);
...
}
//WrapPusher.cs userdata
void PushUnityEngineVector3(RealStatePtr L, UnityEngine.Vector3 val){
IntPtr buff = LuaAPI.xlua_pushstruct(L, 12, UnityEngineVector3_TypeID);
CopyByValue.Pack(buff, 0, val)
}
//PackUnPack.cs userdata
bool Pack(IntPtr buff, int offset, UnityEngine.Vector3 field){
if(!LuaAPI.xlua_pack_float3(buff, offset, field.x, field.y, field.z)){
return false;
}
return true;
}
/*xlua.c*/
typedef struct {
int fake_id;
unsigned int len;
char data[1];
} CSharpStruct;
LUA_API void *xlua_pushstruct(lua_State *L, unsigned int size, int meta_ref) {
CSharpStruct *css = (CSharpStruct *)lua_newuserdata(L, size + sizeof(int) + sizeof(unsigned int));
css->fake_id = -1;
css->len = size;
lua_rawgeti(L, LUA_REGISTRYINDEX, meta_ref);
lua_setmetatable(L, -2);
return css;
}
LUALIB_API int xlua_pack_float3(void *p, int offset, float f1, float f2, float f3) {
CSharpStruct *css = (CSharpStruct *)p;
if (css->fake_id != -1 || css->len < offset + sizeof(float) * 3) {
return 0;
} else {
float *pos = (float *)(&(css->data[0]) + offset);
pos[0] = f1;
pos[1] = f2;
pos[2] = f3;
return 1;
}
}
2.lua里写入一个值到C# Vector3字段x,代码看出操作了userdata
核心Lua CAPI调用过程如下:
lua_tonumber:lua虚拟栈1次出栈拿到x值
Vector3.x = value:更新x值
lua_touserdata:取userdata
xlua_pack_float3: 更新到数据到userdata(实际xyz都写入了)
下列代码是完整调用过程:
static int _s_set_x(RealStatePtr L){
...
UnityEngine.Vector3 gen_to_be_invoked;
translator.Get(L, 1, out gen_to_be_invoked);
gen_to_be_invoked.x = (float)LuaAPI.lua_tonumber(L, 2);
translator.UpdateUnityEngineVector3(L, 1, gen_to_be_invoked);
...
}
//WrapPusher.cs userdata
void UpdateUnityEngineVector3(RealStatePtr L, int index, UnityEngine.Vector3 val){
IntPtr buff = LuaAPI.lua_touserdata(L, index);
CopyByValue.Pack(buff, 0, val);
}
//PackUnPack.cs userdata
bool Pack(IntPtr buff, int offset, UnityEngine.Vector3 field){
if(!LuaAPI.xlua_pack_float3(buff, offset, field.x, field.y, field.z)){
return false;
}
return true;
}
3.将Vector3写入transform到position有二种方式:传LUA_TUSERDATA或者LUA_TTABLE
核心Lua CAPI调用过程如下:
lua_type:判断类型
//type为userdata
lua_touserdata:取userdata
xlua_unpack_float3: 读userdata中xyz
//type为table
{
xlua_pushasciistring:入栈FieldName:x
lua_rawget:通过FieldName找table中x的值并入虚拟栈
lua_tonumber:出栈拿到x值
}*3次
下列代码是完整调用过程:
--lua端
transform.position = CS.UnityEngine.Vector3(1, 2, 3) --userdata
transform.position = {x = 2008, y = 8, z = 8} --table
// UnityEngineVector3Wrap.cs
static int _s_set_position(RealStatePtr L){
UnityEngine.Transform gen_to_be_invoked = (UnityEngine.Transform)translator.FastGetCSObj(L, 1);
UnityEngine.Vector3 gen_value;
translator.Get(L, 2, out gen_value);
gen_to_be_invoked.position = gen_value;
}
//WrapPusher.cs 判断是userdata还是table
public void Get(RealStatePtr L, int index, out UnityEngine.Vector3 val)
{
LuaTypes type = LuaAPI.lua_type(L, index);
if (type == LuaTypes.LUA_TUSERDATA )
{
if (LuaAPI.xlua_gettypeid(L, index) != UnityEngineVector3_TypeID)
{
throw new Exception("invalid userdata for UnityEngine.Vector3");
}
IntPtr buff = LuaAPI.lua_touserdata(L, index);
if (!CopyByValue.UnPack(buff, 0, out val))
{
throw new Exception("unpack fail for UnityEngine.Vector3");
}
}
else if (type ==LuaTypes.LUA_TTABLE)
{
CopyByValue.UnPack(this, L, index, out val);
}
else
{
val = (UnityEngine.Vector3)objectCasters.GetCaster(typeof(UnityEngine.Vector3))(L, index, null);
}
}
//userdata
public static bool UnPack(IntPtr buff, int offset, out UnityEngine.Vector3 field)
{
field = default(UnityEngine.Vector3);
float x = default(float);
float y = default(float);
float z = default(float);
if(!LuaAPI.xlua_unpack_float3(buff, offset, out x, out y, out z))
{
return false;
}
field.x = x;
field.y = y;
field.z = z;
return true;
}
//table
public static void UnPack(ObjectTranslator translator, RealStatePtr L, int idx, out UnityEngine.Vector3 val){
val = new UnityEngine.Vector3();
int top = LuaAPI.lua_gettop(L);
if (Utils.LoadField(L, idx, "x")){
translator.Get(L, top + 1, out val.x);
}
LuaAPI.lua_pop(L, 1);
if (Utils.LoadField(L, idx, "y")){
translator.Get(L, top + 1, out val.y);
}
LuaAPI.lua_pop(L, 1);
if (Utils.LoadField(L, idx, "z")){
translator.Get(L, top + 1, out val.z);
}
LuaAPI.lua_pop(L, 1);
}
//table
public static bool LoadField(RealStatePtr L, int idx, string field_name)
{
idx = idx > 0 ? idx : LuaAPI.lua_gettop(L) + idx + 1;// abs of index
LuaAPI.xlua_pushasciistring(L, field_name);
LuaAPI.lua_rawget(L, idx);
return !LuaAPI.lua_isnil(L, -1);
}
/*userdata*/
LUALIB_API int xlua_unpack_float3(void *p, int offset, float *f1, float *f2, float *f3) {
CSharpStruct *css = (CSharpStruct *)p;
if (css->fake_id != -1 || css->len < offset + sizeof(float) * 3) {
return 0;
} else {
float *pos = (float *)(&(css->data[0]) + offset);
*f1 = pos[0];
*f2 = pos[1];
*f3 = pos[2];
return 1;
}
}
4.也可以将自定义的C# struct映射到lua table或者userdata 。
自定义struct规则: a.含无参构造函数 b.只包含值类型 c.可以嵌套其它只包含值类型的struct
//ReImplementInLua.cs--table
[GCOptimize(OptimizeFlag.PackAsTable)]
public struct PushAsTableStruct
{
public int x;
public int y;
}
//NoGc.cs -- userdata
[GCOptimize]
[LuaCallCSharp]
public struct MyStruct
{
public MyStruct(int p1, int p2)
{
a = p1;
b = p2;
c = p2;
e.c = (byte)p1;
}
public int a;
public int b;
public decimal c;
public Pedding e;
}
5.xlua.genaccessor支持lua使用C#类型直接在lua侧完成,而且省掉了wrap代码以达成省text段的效果。 xlua.genaccessor不经过C#直接操作内存,效率应该是最高的。
--具体事例参见ReImplementInLua.cs
local get_x, set_x = xlua.genaccessor(0, 8)
/*xlua.c*/
static const luaL_Reg xlualib[] = {
{"sethook", profiler_set_hook},
{"genaccessor", gen_css_access},
{"structclone", css_clone},
{NULL, NULL}
};
LUA_API int gen_css_access(lua_State *L) {
int offset = xlua_tointeger(L, 1);
int type = xlua_tointeger(L, 2);
if (offset < 0) {
return luaL_error(L, "offset must larger than 0");
}
if (type < T_INT8 || type > T_DOUBLE) {
return luaL_error(L, "unknow tag[%d]", type);
}
lua_pushvalue(L, 1);
lua_pushcclosure(L, direct_getters[type], 1);
lua_pushvalue(L, 1);
lua_pushcclosure(L, direct_setters[type], 1);
lua_pushcclosure(L, nop, 0);
return 3;
}
对象获取
C#侧ObjectPool(数组)保存的是id<->c# object映射, Lua侧用userdata建立和C#关系,GC过程都是基于这个模型构建。
--lua
local gobject = CS.UnityEngine.GameObject.Find('helloworld')
gobject.transform
//UnityEngineGameObjectWrap.cs
static int _g_get_transform(RealStatePtr L){
try {
ObjectTranslator translator = ObjectTranslatorPool.Instance.Find(L);
//通过id(udata)在ObjectTranslator.objects(对象池Array)取c#对象,有boxing(装箱)行为
UnityEngine.GameObject gen_to_be_invoked = (UnityEngine.GameObject)translator.FastGetCSObj(L, 1);
//Push做了二步工作:
//1.把transform push到objects中
//2.transform对象id通过xlua_pushcsobj push到userdata
translator.Push(L, gen_to_be_invoked.transform);
} catch(System.Exception gen_e) {
return LuaAPI.luaL_error(L, "c# exception:" + gen_e);
}
return 1;
}
//ObjectTranslator.cs
internal object FastGetCSObj(RealStatePtr L,int index){
return getCsObj(L, index, LuaAPI.xlua_tocsobj_fast(L,index));
}
/*xlua.c*/
/*通过userdata拿id*/
LUA_API int xlua_tocsobj_fast (lua_State *L,int index) {
int *udata = (int *)lua_touserdata (L,index);
if(udata!=NULL)
return *udata;
return -1;
}
LUA_API void xlua_pushcsobj(lua_State *L, int key, int meta_ref, int need_cache, int cache_ref) {
int* pointer = (int*)lua_newuserdata(L, sizeof(int));
*pointer = key;
if (need_cache) cacheud(L, key, cache_ref);
lua_rawgeti(L, LUA_REGISTRYINDEX, meta_ref);//t[n] 的值压栈,t 是指索引LUA_REGISTRYINDEX的表
lua_setmetatable(L, -2);
}
Lua调用CSharp函数
先包装一个LuaCSFunction函数,再通过Utils.RegisterFunc来注册C#函数, 包装函数必须打[MonoPInvokeCallbackAttribute]标签,为什么打标签参见Unity文档和Mono文档。
因为注册函数只需要消耗一次,其性能瓶颈主要就在c#函数的参数出栈(xlua_toXXX)和返回值的入栈(xlua_pushXXX)上。
--lua
xxx:TestFunc(2008)
//c#函数
public void TestFunc(int i)
{
}
//按规则包装一个函数给C
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate int lua_CSFunction(IntPtr L);
[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
static int _m_TestFunc(RealStatePtr L)
{
....
//函数参数:出栈取值
int _i = LuaAPI.xlua_tointeger(L, 2);
//c#函数调用
gen_to_be_invoked.TestFunc( _i );
//返回值处理
...
//返回值个数
return 0;
....
}
//注册函数,这样非托管代码就可以反向调用托管代码了
LuaAPI.xlua_pushasciistring(L, name);
IntPtr fn = Marshal.GetFunctionPointerForDelegate(function);
/*lua_pushcfunction*/
xlua_push_csharp_function(L, fn, n);
LuaAPI.lua_rawset(L, idx);
CSharp调用Lua
1.读G表数据
--lua
money = 99999
//c#
luaenv.Global.Get<int>("money")
在LuaTable.Get主要做了如下LuaCAPI工作:
lua_pushstring(L,"money")
lua_gettable(L,-2)
xlua_tointeger(L,-1);
2.C#调Lua函数是把Lua函数映射到delegate的模式然后调用 这种模式因为函数参数和返回值类型双方都明确,从而避免了Boxing。
--lua
function f(a, b)
print('a', a, 'b', b)
return 1, {f1 = 1024}
end
//用例CSCallLua.cs
[CSharpCallLua]
public delegate int FDelegate(int a, string b, out DClass c);
//在DelegatesGensBridge.cs里生成映射参数关系,调用大致做下面三个步骤
public int __Gen_Delegate_Imp13(int p0, string p1, out CSCallLua.DClass p2)
{
....
LuaAPI.xlua_pushXXX //参数处理
LuaAPI.lua_pcall //lua函数执行
LuaAPI.xlua_toXXX //返回值处理
....
}
Slua v1.6.0
Unity独有类型使用
1.Vector3映射为luatable。
核心Lua CAPI调用过程如下:
luaL_checknumber //xlua直接用的luaL_tonumber
LuaDLL.luaS_pushVector3(l, v3.x, v3.y, v3.z)//构建table和写入值一次性全在c端做
--lua
import "UnityEngine"
Vector3(10,10,10)
//Lua_UnityEngine_Vector3.cs
static public int constructor(IntPtr l) {
...
System.Single a1;
checkType(l,2,out a1);
System.Single a2;
checkType(l,3,out a2);
System.Single a3;
checkType(l,4,out a3);
o=new UnityEngine.Vector3(a1,a2,a3);
pushValue(l,true);//lua_pushboolean
pushValue(l,o);//luaS_pushVector3
...
}
/*slua.c*/
LUA_API void luaS_pushVector3(lua_State *L, float x, float y, float z) {
lua_newtable(L);
lua_pushnumber(L, x);
lua_rawseti(L, -2, 1);
lua_pushnumber(L, y);
lua_rawseti(L, -2, 2);
lua_pushnumber(L, z);
lua_rawseti(L, -2, 3);
setmetatable(L, -2, MT_VEC3);
}
2.设置Vector3.x
整个赋值操作基本就是对lua table的处理
核心Lua CAPI调用过程如下:
luaL_checknumber //xlua直接用的luaL_tonumber
LuaDLL.luaS_checkVector3(l, p, out x, out y, out z) //取table值构造Vector3
Vector3.x = value:更新x值
LuaDLL.luaS_setDataVec(l, 1, v.x, v.y, v.z, float.NaN)//更新到数据到table
/*slua.c*/
LUA_API int luaS_checkVector3(lua_State *L, int p, float* x, float *y, float *z) {
p=lua_absindex(L,p);
if(lua_type(L,p)!=LUA_TTABLE)
return -1;
luaL_checktype(L, p, LUA_TTABLE);
lua_rawgeti(L, p, 1);
*x = (float)lua_tonumber(L, -1);
lua_rawgeti(L, p, 2);
*y = (float)lua_tonumber(L, -1);
lua_rawgeti(L, p, 3);
*z = (float)lua_tonumber(L, -1);
lua_pop(L, 3);
return 0;
}
LUA_API void luaS_setDataVec(lua_State *L, int p, float x, float y, float z, float w) {
p=lua_absindex(L,p);
setelementid(L, p, x, 1);
setelementid(L, p, y, 2);
setelementid(L, p, z, 3);
setelementid(L, p, w, 4);
}
3.给transform.position赋值
相比xlua映射lua table的处理,slua整个取值过程(luaS_checkVector3)基本合并到C端处理了,性能更好一点。
核心Lua CAPI调用过程如下:
LuaDLL.luaS_checkVector3(l, p, out x, out y, out z) //取table值构造Vector3
self.position=v //c#端更新
--lua端
transform.position = Vector3(1, 2, 3) --table
4.复杂类型 slua只是对特定Unity类型,做了table映射处理,lua capi都在C侧做的批处理优化。xlua提供userdata和table二种选择, 而且xlua.genaccessor做的更通用,几乎完全在C侧完成操作。slua和xlua都提供Struct映射机制解决boxing问题。
对象获取
C#侧ObjectCache(Dictionary)保存的是IntPtr<->c# object映射, Lua侧用userdata建立和C#关系。 C#侧对象必须在lua侧gc后才解引用,最后Gc。
xlua在lua侧使用lua_rawgeti可以看出table索引是int。 slua在C#侧拿getAQName(xx)做key是String传到lua侧,元表处理用luaL_getmetatable(L,keyStr)。 这里就体现出差异了。
--lua
gobject.transform
static public int get_transform(IntPtr l) {
UnityEngine.GameObject self=(UnityEngine.GameObject)checkSelf(l);//c#映射表取
pushValue(l,true);
/*
1.push到c#映射表
2.luaS_pushobject push映射到lua侧userdata
*/
pushValue(l,self.transform);
return 2;
}
/*slua.c*/
LUA_API int luaS_pushobject(lua_State *l, int index, const char* t, int gco, int cref) {
int is_reflect = 0;
luaS_newuserdata(l, index);
if (gco) cacheud(l, index, cref);
luaL_getmetatable(l, t);//t对应的原表入栈
if (lua_isnil(l, -1))
{
lua_pop(l, 1);
luaL_getmetatable(l, "LuaVarObject");
is_reflect = 1;
}
lua_setmetatable(l, -2);
return is_reflect;
}
函数调用
lua函数调用c#并回调,参数生成代码做了映射关系。
--lua
Deleg.testAction( self.action )
function self.action(a,b)
print("callback from action")
print(a,b)
end
//Deleg.cs
public static void testAction(Action<int, string> f)
{
f(1998, "bob");
}
Slua(2016年版本)c#调用lua,本质是调用包装的LuaFunction,它在call参数处理上就有boxing性能问题。
--lua
appMain:SetQuitCallback(function() self:OnApplicationQuit() end)
function luaApp:OnApplicationQuit()
-- do something
end
function foo(a,b,c)
return a,b,c,"slua"
end
//
private SLua.LuaFunction _AppQuitCallback;
public void SetQuitCallback(SLua.LuaFunction func)
{
_AppQuitCallback = func;
}
_AppQuitCallback.call();
LuaSvr.mainState.getFunction("foo").call(1, 2, 3)
Slua新的版本做了改进,可以CustomLuaClass标记导出对应delegate,调用luafunction.cast转化对应的delegate,以避免gc开销.
[CustomLuaClass]
public delegate void GetMoneyDelegate(int x,int y);
GetMoneyDelegate ud;
LuaFunction getFunction;
getMoney = getFunction.cast<GetMoneyDelegate>();//实现不够简练
if (getMoney != null) getMoney(1,2);
Tolua v1.0.7.392
Unity类型使用
ToLua对于Lua CAPI调用优化点在于做了很多批处理,并对Unity很多类型做了Lua侧实现. 性能测试用例如果是测这些类型就太占优势了。但Tolua做的太过耦合,导致新增加一种自定义类型就必须 硬编码很多过程。有Xlua在这一点上做的更通用一些。在lua和C#相互持的处理上和Xlua方式一样,ObjectTranslator用index(int)持有c#对象 传给lua侧用userdata持有c#对象。
--具体代码参见ToLua\Lua\UnityEngine\Vector3.lua
function Vector3.New(x, y, z)
local t = {x = x or 0, y = y or 0, z = z or 0}
setmetatable(t, Vector3)
return t
end
--调用
local v = Vector3.New(19,98,10)
[MonoPInvokeCallbackAttribute(typeof(LuaCSFunction))]
static int set_position(IntPtr L)
{
object o = null;
try
{
o = ToLua.ToObject(L, 1);
UnityEngine.Transform obj = (UnityEngine.Transform)o;
UnityEngine.Vector3 arg0 = ToLua.ToVector3(L, 2);
obj.position = arg0;
return 0;
}
catch(Exception e)
{
return LuaDLL.toluaL_exception(L, e, o, "attempt to index position on a nil value");
}
}
public static Vector3 ToVector3(IntPtr L, int stackPos)
{
float x = 0, y = 0, z = 0;
LuaDLL.tolua_getvec3(L, stackPos, out x, out y, out z);
return new Vector3(x, y, z);
}
void tolua_openluavec3(lua_State *L)
{
lua_getglobal(L, "Vector3");
if (!lua_istable(L, 1))
{
luaL_error(L, "Vector3 does not exist or not be loaded");
return;
}
lua_pushstring(L, "New");
lua_rawget(L, -2);
lua_rawseti(L, LUA_REGISTRYINDEX, LUA_RIDX_PACKVEC3);
lua_pushstring(L, "Get");
lua_rawget(L, -2);
lua_rawseti(L, LUA_REGISTRYINDEX, LUA_RIDX_UNPACKVEC3);
lua_pop(L, 1);
}
LUALIB_API void tolua_getvec3(lua_State *L, int pos, float* x, float* y, float* z)
{
lua_getref(L, LUA_RIDX_UNPACKVEC3);
lua_pushvalue(L, pos);
lua_call(L, 1, 3);
*x = (float)lua_tonumber(L, -3);
*y = (float)lua_tonumber(L, -2);
*z = (float)lua_tonumber(L, -1);
lua_pop(L, 3);
}
LUALIB_API void tolua_pushvec3(lua_State *L, float x, float y, float z)
{
lua_getref(L, LUA_RIDX_PACKVEC3);
lua_pushnumber(L, x);
lua_pushnumber(L, y);
lua_pushnumber(L, z);
lua_call(L, 3, 1);
}
C#调用Lua函数
//CallLuaFunction.cs
function luaFunc(num)
return num + 1
end
test = {}
test.luaFunc = luaFunc
//映射了函数参数和返回值类型
luaFunc = lua.GetFunction("test.luaFunc");
int num = luaFunc.Invoke<int, int>(123456);
public R1 Invoke<T1, R1>(T1 arg1)
{
BeginPCall();
PushGeneric(arg1);//通用push
PCall();
R1 ret1 = CheckValue<R1>();
EndPCall();
return ret1;
}
//c# 直接硬编码方式调用,简化PushGeneric内部查找操作,榨干最后一丝性能
num = CallFunc();
int CallFunc(int value)
{
luaFunc.BeginPCall();
luaFunc.Push(value);
luaFunc.PCall();
int num = (int)luaFunc.CheckNumber();
luaFunc.EndPCall();
return num;
}
num = CallFunc();
总结
总的来说,三个框架都相互吸收优点加以整合,为了效率,在lua、c、c#交互上都是尽量做到值拷贝,映射类型减少boxing和unboxing,避免GC。Tolua侧重简练实现,对Unity Vector3等特定类型在Lua侧做了很多实现,减少了和C#交互,其他接口的处理上基本都是坚持减少LuaCAPI调用频次的原则,所以性能稍占优势。但扩展必需借助硬编码方式。Tolua线上有大量游戏验证使用,集成了很多常见库。而Xlua更注重解耦、通用实现,从生成Wrap采用模板方式也可看出作者是很排斥硬编码模式, 必须做到可扩展性强。Xlua保持框架纯粹(也有Tencent license风险问题)并没有默认集成太多第三方功能,库独立提供使用。slua是较早放在github上开源,去反射改静态绑定。早期代码质量比cstolua好一些,当时一部分项目选择该框架,但后期这个优势就逐渐没有了。
最后讨论一下三个框架实现耦合度、可扩展性和集成C++库便利性。
类别 | Slua | ToLua | XLua |
---|---|---|---|
C# hotfix | 无 | 有 | 有 |
工程结构、代码质量 | ★★★☆☆ | ★★★★☆ 力求简洁实用 | ★★★★☆ 力求解耦、减少硬编码、可扩展性强 |
lib | luaprofile、socket等 | cjson、pb、socket、lpeg、luaprofile、int64等 | luasocket、luaprofile、int64 。其他库独立提供 |
烘焙lua | 烘焙 | 烘焙+打AssetBundle | 无 |
测试用例、教程 | 少量例子 | 简单文档说明、大量例子 | 例子、教程、测试用例、文档完备 |
生成wrap | Unity和Custom分别生成、可视化设置生成输出目录,开发需生成胶水代码。 | 拆分成wrap+delegates+binder,开发需生成胶水代码。 | 开发时可以用反射,发布时再生成。Unity和Custom合并生成,可以自定义模板输出自定义格式 |
Build Lua库 | 硬编码脚本Build、默认只集成了luajit,C#集成了luajit烘焙lua工具。Luac需自行集成。 | 独立runtime工程 mingw+Msys2 build、默认集成luajit,Luac需自行集成。 | 依赖CMake Build,集成lib很方便、luajit和lua最新几个版本都很好集成了 |
优化
实际开发中大多是Lua调用C#,保持二个原则提高性能:1.减少跨语言对象访问,如上分析框架得知这个交互过程其实很繁琐,代价很高。2.减少参数传递,尽量传valueType,映射好参数类型避免boxing。
说白了就是减少marshaling消耗!!!。
下列代码来源于实际项目中,可看出Lua对面板和面板上UI组件均采用句柄(int)方式持有,lua和c#传递valueType类型数据。在c#端包装静态方法,减少跨语言对象访问成本。
--lua
UIControlWrap.SetActive(self.wndHandle, self.msgWndHandle, true)
//UIControlWrap.cs 对象交互完全是 int 传递
public static void SetActive(int wndHandle, int ctrlHandle, bool active)
{
GameObject ctrl = GameWindow.GetWindowControl(wndHandle, ctrlHandle);
if (ctrl == null || ctrl.activeSelf == active) return;
if (ctrl.activeSelf != active)
{
ctrl.SetActive(active);
}
}
//UIControlWrap.cs 这里设置对象属性Vector3,只是传递二个float。
//对象transform访问完全是在c#侧直接操作,很大程度提升性能。
public static void SetWorldPositionXY(int wndHandle, int ctrlHandle, float x, float y)
{
GameObject ctrl = GameWindow.GetWindowControl(wndHandle, ctrlHandle);
if (ctrl == null) return;
//上面三个框架分析可知lua侧做这个操作代价有多高
Vector3 pos = ctrl.transform.position;
pos.x = x;
pos.y = y;
ctrl.transform.position = pos;
}