上一节我们提到了如何将lua函数绑定到C#的委托。这一节我们来看一下带有out参数的C#函数,在lua层是如何使用的。example 14中给了如下一段lua代码:
local box = UnityEngine.BoxCollider
function TestPick(ray)
local _layer = 2 ^ LayerMask.NameToLayer('Default')
local time = os.clock()
local flag, hit = UnityEngine.Physics.Raycast(ray, RaycastHit.out, 5000, _layer)
if flag then
print('pick from lua, point: '..tostring(hit.point))
end
end
例子中调用的Raycast方法是带有out参数的,即
public static bool Raycast(Ray ray, out RaycastHit hitInfo, float maxDistance, int layerMask);
下面我们来深入源码一探究竟。首先在调用UnityEngine.BoxCollider
时,会触发到C#层的LuaOpen_UnityEngine_BoxCollider
函数,这个函数是在默认的LuaBinder.Bind
函数中出现的:
L.BeginPreLoad();
L.AddPreLoad("UnityEngine.BoxCollider", LuaOpen_UnityEngine_BoxCollider, typeof(UnityEngine.BoxCollider));
...
L.EndPreLoad();
这里的AddPreLoad
函数起到一个延迟注册的作用,在调用之后LuaOpen_UnityEngine_BoxCollider
不会立刻触发,而是绑定到了lua层package.preload
上,在lua层真正用到时,才会触发注册流程。回忆一下lua层访问C# namespace时,会触发module_index_event
,如果下一步要访问的C# class不存在,则会往package.preload
中查找,找到就发起require,lua的require会优先使用preload里自定义过的函数进行加载:
lua_getref(L, LUA_RIDX_PRELOAD); //stack: t key space preload
lua_pushvalue(L, -2);
lua_pushstring(L, ".");
lua_pushvalue(L, 2);
lua_concat(L, 3); //stack: t key space preload key
lua_pushvalue(L, -1); //stack: t key space preload key1 key1
lua_rawget(L, -3); //stack: t key space preload key1 value
if (!lua_isnil(L, -1))
{
lua_pop(L, 1); //stack: t key space preload key1
lua_getref(L, LUA_RIDX_REQUIRE);
lua_pushvalue(L, -2);
lua_call(L, 1, 1);
}
LuaOpen_UnityEngine_BoxCollider
函数的核心就是调用BoxColliderWrap类的注册函数,这没什么好说的。
然后,我们注意到lua层的函数TestPick
接收一个Ray参数,这个参数要从C#层传进去。而Ray类型是struct类型,我们不希望push到lua层产生任何的gc。来看看具体是怎么实现的:
public static void Push(IntPtr L, Ray ray)
{
LuaStatic.GetPackRay(L);
Push(L, ray.direction);
Push(L, ray.origin);
if (LuaDLL.lua_pcall(L, 2, 1, 0) != 0)
{
string error = LuaDLL.lua_tostring(L, -1);
throw new LuaException(error);
}
}
解决思路就是让lua层自己去构造lua层的ray,C#层只负责传数据,不再使用userdata。GetPackRay
会将C#层之前缓存的lua函数Ray.New
压入lua栈顶。这个函数接收两个lua vector3参数:
function Ray.New(direction, origin)
local ray = {}
ray.direction = direction:Normalize()
ray.origin = origin
setmetatable(ray, Ray)
return ray
end
那么容易猜到的是,C#的vector3类型也会通过类似的方式传到lua层:
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);
}
LUA_RIDX_PACKVEC3
这个就是lua函数Vector3.New
的reference。可以看出,一个C#的Ray其实被拆成了6个float,传到了lua层,然后lua层重新组装一遍,成为lua层的Ray。通过这样的方式,C#传递struct到lua层时,避免了gc开销。
接下来就是真正调用Raycast的时候了。问题的关键是C#层如何判断lua层传过来的参数类型,首先是Ray:
public bool CheckRay(IntPtr L, int pos)
{
if (LuaDLL.lua_type(L, pos) == LuaTypes.LUA_TTABLE)
{
return LuaDLL.tolua_getvaluetype(L, pos) == LuaValueType.Ray;
}
return false;
}
显然lua层的ray是不是ray这件事情,只有lua层自己知道,所以tolua_getvaluetype
这个函数最终会调用到lua层的GetValueType
:
local function GetValueType()
local getmetatable = getmetatable
local ValueType = ValueType
return function(udata)
local meta = getmetatable(udata)
if meta == nil then
return 0
end
return ValueType[meta] or 0
end
end
每个lua层自己实现的struct,都会设置metatable为自身,例如Ray:
UnityEngine.Ray = Ray
setmetatable(Ray, Ray)
return Ray
然后在tolua启动时,会把这些struct的lua都require进来,作为全局可访问的module table:
Mathf = require "UnityEngine.Mathf"
Vector3 = require "UnityEngine.Vector3"
Quaternion = require "UnityEngine.Quaternion"
Vector2 = require "UnityEngine.Vector2"
Vector4 = require "UnityEngine.Vector4"
Color = require "UnityEngine.Color"
Ray = require "UnityEngine.Ray"
Bounds = require "UnityEngine.Bounds"
RaycastHit = require "UnityEngine.RaycastHit"
Touch = require "UnityEngine.Touch"
LayerMask = require "UnityEngine.LayerMask"
Plane = require "UnityEngine.Plane"
Time = reimport "UnityEngine.Time"
接着在ValueType
这个table中,设置了这些module对应到C#的LuaValueType类中的index:
local ValueType = {}
ValueType[Vector3] = 1
ValueType[Quaternion] = 2
ValueType[Vector2] = 3
ValueType[Color] = 4
ValueType[Vector4] = 5
ValueType[Ray] = 6
ValueType[Bounds] = 7
ValueType[Touch] = 8
ValueType[LayerMask] = 9
ValueType[RaycastHit] = 10
ValueType[int64] = 11
ValueType[uint64] = 12
C#中的相关定义如下:
public partial struct LuaValueType
{
public const int None = 0;
public const int Vector3 = 1;
public const int Quaternion = 2;
public const int Vector2 = 3;
public const int Color = 4;
public const int Vector4 = 5;
public const int Ray = 6;
public const int Bounds = 7;
public const int Touch = 8;
public const int LayerMask = 9;
public const int RaycastHit = 10;
public const int Int64 = 11;
public const int UInt64 = 12;
public const int Max = 64;
...
}
接着是第二个参数,RaycastHit.out
,这个变量的注册是在C#的OpenLuaLibs
中完成的,它把C#的函数绑定到了对应lua类的out字段上:
public static void OpenLuaLibs(IntPtr L)
{
if (LuaDLL.tolua_openlualibs(L) != 0)
{
string error = LuaDLL.lua_tostring(L, -1);
LuaDLL.lua_pop(L, 1);
throw new LuaException(error);
}
SetOutMethods(L, "RaycastHit", GetOutRaycastHit);
...
}
这个函数在调用时,会新建一个LuaOut
类对象,将其push到lua层:
static int GetOutRaycastHit(IntPtr L)
{
ToLua.PushOut(L, new LuaOut());
return 1;
}
这个对象纯粹是起到一个类型检查的作用,但是每次调用时都会新建,是有gc开销的。在类型检查时,会根据lua层的userdata,转换到C#层的对应对象,判断是否为LuaOut
类型:
static bool IsUserData(IntPtr L, int pos)
{
object obj = null;
int udata = LuaDLL.tolua_rawnetobj(L, pos);
if (udata != -1)
{
ObjectTranslator translator = ObjectTranslator.Get(L);
obj = translator.GetObject(udata);
if (obj != null)
{
return obj is T;
}
else
{
return !IsValueType;
}
}
return false;
}
第三个参数为5000,这没啥说的,直接判断是不是lua的number类型即可。那么还剩最后一个参数,这个参数其实也是number,它是由lua层的LayerMask.NameToLayer
返回值决定的:
function LayerMask.NameToLayer(name)
return Layer[name]
end
由于Layer本身是可以用户自定义的,因此需要先在C#层传递给lua层,这是在InitLayer
这个函数中完成的:
static void InitLayer(IntPtr L)
{
LuaDLL.tolua_createtable(L, "Layer");
for (int i = 0; i < 32; i++)
{
string str = LayerMask.LayerToName(i);
if (!string.IsNullOrEmpty(str))
{
LuaDLL.lua_pushstring(L, str);
LuaDLL.lua_pushinteger(L, i);
LuaDLL.lua_rawset(L, -3);
}
}
LuaDLL.lua_pop(L, 1);
}
这个函数会把C#层定义的所有Layer传递给lua,在lua层新建一个Layer table,key为name,value为layer的值,这个值是一个int,所以最后一个参数的类型也是number。
4个参数的类型都检查完毕之后,我们来看下真正的执行逻辑:
UnityEngine.Ray arg0 = ToLua.ToRay(L, 1);
UnityEngine.RaycastHit arg1;
float arg2 = (float)LuaDLL.lua_tonumber(L, 3);
int arg3 = (int)LuaDLL.lua_tonumber(L, 4);
bool o = UnityEngine.Physics.Raycast(arg0, out arg1, arg2, arg3);
LuaDLL.lua_pushboolean(L, o);
if (o) ToLua.Push(L, arg1); else LuaDLL.lua_pushnil(L);
return 2;
之前我们提到过,把C#层的Ray push到lua层是不会产生gc的,那么反过来,从lua层取出C#的Ray是如何做到没有gc的呢?答案是类似的,C#层会调用lua层的函数,让lua层把Ray table展开为6个number放到栈上,C#层直接获取即可:
function Ray:Get()
local o = self.origin
local d = self.direction
return o.x, o.y, o.z, d.x, d.y, d.z
end
public static Ray ToRay(IntPtr L, int stackPos)
{
int top = LuaDLL.lua_gettop(L);
LuaStatic.GetUnpackRayRef(L);
stackPos = LuaDLL.abs_index(L, stackPos);
LuaDLL.lua_pushvalue(L, stackPos);
if (LuaDLL.lua_pcall(L, 1, 6, 0) == 0)
{
float ox = (float)LuaDLL.lua_tonumber(L, top + 1);
float oy = (float)LuaDLL.lua_tonumber(L, top + 2);
float oz = (float)LuaDLL.lua_tonumber(L, top + 3);
float dx = (float)LuaDLL.lua_tonumber(L, top + 4);
float dy = (float)LuaDLL.lua_tonumber(L, top + 5);
float dz = (float)LuaDLL.lua_tonumber(L, top + 6);
LuaDLL.lua_settop(L, top);
return new Ray(new Vector3(ox, oy, oz), new Vector3(dx, dy, dz));
}
else
{
string error = LuaDLL.lua_tostring(L, -1);
LuaDLL.lua_settop(L, top);
throw new LuaException(error);
}
}
如果Raycast返回了true,说明射线命中,out参数是有意义的,因此还需要将out参数push到lua层;如果返回false,push nil即可。
下一节我们将关注lua继承扩展C#类的机制。
如果你觉得我的文章有帮助,欢迎关注我的微信公众号 我是真的想做游戏啊