• tolua源码分析(七)带out参数的C#函数


    tolua源码分析(七)带out参数的C#函数

    上一节我们提到了如何将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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    例子中调用的Raycast方法是带有out参数的,即

    public static bool Raycast(Ray ray, out RaycastHit hitInfo, float maxDistance, int layerMask);
    
    • 1

    下面我们来深入源码一探究竟。首先在调用UnityEngine.BoxCollider时,会触发到C#层的LuaOpen_UnityEngine_BoxCollider函数,这个函数是在默认的LuaBinder.Bind函数中出现的:

    L.BeginPreLoad();
    L.AddPreLoad("UnityEngine.BoxCollider", LuaOpen_UnityEngine_BoxCollider, typeof(UnityEngine.BoxCollider));
    ...
    L.EndPreLoad();
    
    • 1
    • 2
    • 3
    • 4

    这里的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);                    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    解决思路就是让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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    那么容易猜到的是,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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    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;            
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    显然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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    每个lua层自己实现的struct,都会设置metatable为自身,例如Ray:

    UnityEngine.Ray = Ray
    setmetatable(Ray, Ray)
    return Ray
    
    • 1
    • 2
    • 3

    然后在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"
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    接着在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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    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;
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    接着是第二个参数,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);
        ...         
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    这个函数在调用时,会新建一个LuaOut类对象,将其push到lua层:

    static int GetOutRaycastHit(IntPtr L)
    {
        ToLua.PushOut(L, new LuaOut());
        return 1;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这个对象纯粹是起到一个类型检查的作用,但是每次调用时都会新建,是有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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    第三个参数为5000,这没啥说的,直接判断是不是lua的number类型即可。那么还剩最后一个参数,这个参数其实也是number,它是由lua层的LayerMask.NameToLayer返回值决定的:

    function LayerMask.NameToLayer(name)
    	return Layer[name]
    end
    
    • 1
    • 2
    • 3

    由于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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    这个函数会把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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    之前我们提到过,把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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    如果Raycast返回了true,说明射线命中,out参数是有意义的,因此还需要将out参数push到lua层;如果返回false,push nil即可。

    下一节我们将关注lua继承扩展C#类的机制。

    如果你觉得我的文章有帮助,欢迎关注我的微信公众号 我是真的想做游戏啊

  • 相关阅读:
    java-php-net-python-简历网站计算机毕业设计程序
    盘点|国内5款主流低代码开发平台介绍
    计算机毕业设计springboot+vue基本微信小程序的学生作业管理小程序 uniapp
    Java 21 虚拟线程如何限流控制吞吐量
    【PyTorch实战】图像描述——让神经网络看图讲故事
    【jquery Ajax 】art-template模板引擎案例——新闻列表
    实现对python源码加密的方法
    在excel中做好活动进度矩阵,直接通过代码一键生成工作日报,可以md格式也可以word格式
    华为机试真题 Python 实现【字符串重新排列】【2022.11 Q4新题】
    Win10如何删除登录账号?Win10删除登录账号的方法
  • 原文地址:https://blog.csdn.net/weixin_45776473/article/details/131142612