Unity中使用XLua时为C#对象绑定代理对象

16 篇文章 1 订阅
3 篇文章 0 订阅

本文分享Unity中使用XLua时为C#对象绑定代理对象

XLua导出的对象在Lua中是Userdata类型. 我们无法动态的向一个Userdata类型的对象附加变量和方法, 如果想要方便的使用, 我们还需要设计一种代理对象来负责对象的访问.

Lua中类的实现

代理对象可以是一个普通的table, 然后对其进行一些特殊的处理后可以代理userdata对象.

如果对所有的table都执行这种操作可能不太方便, 所以我们一般模拟的概念来设计, 后续所有相关类型的类都继承拥有绑定userdata能力的祖先类.

所以我们首先来介绍如何在Lua中模拟类机制.

Lua中的元表

Lua是一门非常简洁, 入门非常简单, 同时也是非常灵活的语言, 当然其底层实现还是很复杂的(对作者而言_).

如果在日常使用中有什么概念算得上是稍微有些复杂的话, 那元表一定是一个绕不过去的话题, 特别是与类和继承相关联的时候. 至少作者碰到的程序同学中能真正搞明白的还是比较少的.

那么什么是元表呢?

元表顾名思义, 首先是一个表, "元(meta)“作为前缀, 表示"xxxx of xxxx”, 即数据的数据.

以上的说明有点抽象, 作者尝试简单的解释:

meta data代表数据的数据, 即数据是用来代表一些事物, 而meta data是描述数据本身的结构等信息的数据.

比如一个对象存储了一些数据, 而类就是这个对象的meta data, 用来描述这个对象的结构, 有哪些属性等.

那Lua的元表其实就是用来描述一张表本身的结构和一些行为的表, 即表的结构信息表, 这个概念和类的概念相似, 所以我们可以利用元表来模拟类的机制.

元表的基本介绍

元表提供了一些基本的行为给我们使用, 我们可以通过重写这些行为来重新定义表的行为, 这些行为被称为元方法.

__index元方法

通过key值来访问表时, 如果表没有该key对应的值, 就用使用其元表的"__index"

  • 如果"__index"是一个表A
    • 那么直接在表A中索引
    • 如果还是没有索引到, 则继续在表A的元表的"__index"中索引
    • 继续下去直到找到或者没有元表为止
  • 如果"__index"是一个函数, 则使用其返回值
__newindex元方法

通过key来设置表时, 如果表没有该key对应的值, 就用使用其元表的"__newindex"

  • 如果"__newindex"是一个表A
    • 则该值设置到表A中
    • 如果表A中也没有该key对应的值, 则继续在表A的元表的"__newindex"中索引
    • 继续下去直到找到或者没有元表为止
  • 如果"__newindex"是一个函数, 则调用该函数, 我们一般会在该函数内部使用rawset函数来设置值
__call元方法

使用"()"将表当做函数调用时触发调用.

__tostring元方法

将表当做字符串使用时触发调用.

其它元方法用的不多, 这里不再介绍.

类机制的模拟

模拟的核心在于: 将元表定义为类, 具体的表定义为对象.

--- 类生成器
function Class(classname, super)
    if super and type(super) ~= "table" then 
        return assert(false, "[Class]: 当前只支持继承Lua类")
    end

    local cls = {}
    cls.GetClassName = function() return classname end
    cls.GetClass = function() return cls end

    cls._isClass = true

    -- (1)
    cls.__index = cls

    ------------------------------------------------------
    if not super then
        -- (2)
        cls.ctor = function() end
    else
        cls.super = super

        -- (3)
        setmetatable(cls, super)
    end
    ------------------------------------------------------

    function cls.new(...)
        -- (4)
        local instance = setmetatable({}, cls)
        instance:ctor(...)
        return instance
    end

    return cls
end

------------------------------------------------------
--- classA.lua
local _M = Class("classA")
-- (5)
_M.classVariable = "hello"

function _M:ctor()
    print("classA:ctor")
end

-- (6)
function _M:Func1()
    print("classA:Func1")
end

return _M
------------------------------------------------------
--- classB.lua
local _M = Class("classB", require("classA"))

function _M:ctor()
    --- (7)
    _M.super.ctor(self)

    print("classB:ctor")
    print("classB:" .. self.classVariable)
    self:Func1()
end

return _M
------------------------------------------------------
--- test.lua
local objectA = require("classA").new()
local objectB = require("classB").new()

--output:
-- classA:ctor
-- classA:ctor
-- classB:ctor
-- classB:hello
-- classA:hello
------------------------------------------------------

下面就几个关键节点进行说明:

  • (1)(2)(5)(6): cls模拟类的概念
    • 模块本身代表类cls
    • 对象的元表代表类cls
    • 其本身是对象的元表, 且__index索引指向自身
    • 方法和类变量定义在cls上, 对象本身不存在方法和类变量, 而是在对象上索引时, 去元表(即类)中索引
  • (3): 设置cls元表为其它类, 模拟类的继承
    • 对象上索引->类上索引(cls, 对象的元表的__index)->父类上索引(super, 类cls的元表的__index)
  • (4): 对象的实例化即是给一个新生成的空表设置元表cls
  • (7): 重写父类内部调用父类方法需要通过类(_M)->父类(_M.super)拿到方法(_M.super.ctor)后, 把执行对象(self)传递执行, 否则会递归调用自身

需要注意的是, 这里和C++或者C#不一样的地方在在于, 并不存在"对象内存 = 父类对象部分内存+本类对象部分内存"的概念.

类的模拟主要使用的"__index"元方法, 只要给对象设置任何key赋值, 对象身上就会存在该值(属性和方法), 而不会继续在类中索引, 在使用时请注意不要将给对象赋值方法, 因为这样会覆盖类的方法.

代理对象

代理对象顾名思义是userdata的代理者, 我们在业务代码中不直接操作userdata, 而是将其与代理对象绑定.

因为代理对象是Lua中的table, 所以可以对其附加变量和方法, 方便后续使用.

代理机制的实现和类的模拟大体类似, 也是通过元表实现.

核心的实现在于: 我们访问代理对象, 如果代理对象不存在该属性或者该方法, 则访问其绑定的userdata对象.

从userdata中获取其定义的属性和方法

我们有一个必须要解决的问题是: 如何知道userdata上有哪些属性和方法呢?

如果是方法还比较简单, 直接使用key对userdata对象进行索引即可.

如果是属性就比较复杂了, 因为属性的值可以为空, 如果我们无法判断某个属性是来自于代理对象还是userdata, 一旦设置了属性, 就可能造成一些错误.

XLua默认情况下无法获知一个userdata是拥有该属性, 但是属性为空, 还是不拥有该属性. 所以需要我们做一些修改.

在Xlua/Src/Utils.cs的EndObjectRegister方法末尾对导出的userdata附加其属性和方法等信息, 用于Lua层获取.

public static void EndObjectRegister(Type type, RealStatePtr L, ObjectTranslator translator, LuaCSFunction csIndexer,
			LuaCSFunction csNewIndexer, Type base_type, LuaCSFunction arrayIndexer, LuaCSFunction arrayNewIndexer)
#endif
{
    int top = LuaAPI.lua_gettop(L);
    int meta_idx = abs_idx(top, OBJ_META_IDX);
    int method_idx = abs_idx(top, METHOD_IDX);
    int getter_idx = abs_idx(top, GETTER_IDX);
    int setter_idx = abs_idx(top, SETTER_IDX);

    // ......

    // ----------------------------------------------
    // 将三个列表附加到元表中, 便于lua查看和索引
    LuaAPI.xlua_pushasciistring(L, "__getter");
    LuaAPI.lua_pushvalue(L, getter_idx);
    LuaAPI.lua_rawset(L, meta_idx);

    LuaAPI.xlua_pushasciistring(L, "__setter");
    LuaAPI.lua_pushvalue(L, setter_idx);
    LuaAPI.lua_rawset(L, meta_idx);

    LuaAPI.xlua_pushasciistring(L, "__method");
    LuaAPI.lua_pushvalue(L, method_idx);
    LuaAPI.lua_rawset(L, meta_idx);

    // 保存父类的元表以供lua查询
    if (type != null && type.BaseType != null)
    {
        LuaAPI.lua_pushstring(L, "__baseTypeMeta"); // [baseMeta], meta
        LuaAPI.luaL_getmetatable(L, type.BaseType.FullName); // baseMeta, [baseMeta], meta
        if (LuaAPI.lua_isnil(L, -1))
        {
            LuaAPI.lua_pop(L, 1);
            translator.GetTypeId(L, type.BaseType); // 触发父类的元表初始化
            LuaAPI.luaL_getmetatable(L, type.BaseType.FullName);
        }

        LuaAPI.lua_rawset(L, meta_idx);
    }
    // ----------------------------------------------


    //end new index gen
    LuaAPI.lua_pop(L, 4);
}

这样, 我们就可以通过userdata的元表访问相应的信息.

--- 检查指定Key是否存在于userdata上, 主要用于索引
function GetSomethingFromUGO(userdata, key)
    return userdata[key]
end

--- 检查指定Key是否存在于userdata上, 主要用于设置
function CheckKeyExistOnUserdata(userdata, key)
    --- 如果可以直接索引到就返回
    local selfLookup = userdata[key]
    if selfLookup then return true, selfLookup, userdata end

    --- 无法直接索引到, 则在其getter/setter/method和父类中索引
    local meta = ugo
    while(meta) do
        local result = meta.__getter[key]
        if result then return true, result, ugo end

        result = meta.__setter[key]
        if result then return true, result, ugo end

        result = meta.__method[key]
        if result then return true, result, ugo end

        meta = meta.__baseTypeMeta
    end

    return false
end

为代理对象附加代理行为

通过元表的元方法, 重定义代理对象的访问行为. 通过在对象和类之间插入一个新的元表来重定义行为.

下面的key皆可以指定属性和方法.

  • 索引:
    • 如果代理对象上或者其类或者其父类存在指定key则返回值或者调用方法
    • 如果代理对象上或者其类或者其父类不存在, 则在其绑定的userdata上查找, 如果查找到则返回, 否则抛出异常
    • 如果在userdata上找到的是方法, 则生成Lua的包装方法返回
  • 设置: 如果代理对象上存在指定key, 则使用rawset设置
    • 如果代理对象上或者其类或者其父类存在指定key则直接设置
    • 如果代理对象上或者其类或者其父类不存在, 则在其绑定的userdata上查找, 如果查找到则直接设置
    • 如果都没有, 直接设置在该对象上
-- UserdataProxy.lua
local _M = require("UserdataProxy")

function _M:ctor(userdata)
    self._userdata = userdata
    
    self:_ProxyUserdata()
end

function _M:_ProxyUserdata(userdata)
    local cls = _M
    local newMeta = {}
    setmetable(newMeta, cls)
    setmetable(self, newMeta)

    newMeta.__index = function(self, key)
        -- Lua上找, 类方法或属性
        local selfLookup = cls[key]
        if selfLookup ~= nil then return selfLookup end

        -- userdata上查找
        local userdataAttr = GetSomethingFromUGO(key)
        if userdataAttr then
            local result = userdataAttr
            if type(userdataAttr) == "function" then
                -- 如果是方法, 则构建lua方法缓存, 下次可直接访问
                result = function(_, ...)
                    --- 不使用upValue, 因为userdata可能发生改变
                    local userdata = rawget(self, "_userdata")
                    return userdataAttr(userdata, ...)
                end

                rawset(self, key, result)
            end

            return result
        end

        -- 都没找到
        return nil
    end

    newMeta.__newindex = function(self, key, value)
        -- Lua上找, 类方法或属性
        local selfLookup = cls[key]
        if selfLookup ~= nil then return rawset(self, key, value) end

        -- 过滤私有属性和方法, 减少userdata查找次数
        if string.sub(key, 1, 1) == "_" then
            return rawset(self, key, value)
        end

        if selfLookup == nil then
            -- userdata上查找
            local find, _, userdata = CheckKeyExistOnUserdata(key)
            if find then
                --- 除了userData之外, 向userdata的属性设置不能是lua table
                if key ~= "userData" and type(value) == "table" then
                    local userdataAttrValue = rawget(self, "_userdata")
                    if userdataAttrValue then
                        value = userdataAttrValue
                    end
                end

                userdata[key] = value
                return
            end
        end

        rawset(self, key, value)
    end
end

return _M

总结

整个思想和实现都是比较简单的, 相信感兴趣的同学都能看懂.

结合作者的另一篇文章Unity中配合EmmyLua的Lua使用方案, 可以分类给某些类附加快捷的方法, 比如给Text类附加设置text属性的方法:

--- TextProxy.lua
local _M = Class("TextProxy", require("UserdataProxy"))
return _M
----------------------------------------------------------------------------------------
--- TextShortcut.lua
local _M = require("TextProxy")

function _M:SetText(text)
    local component = self:GetComponent("Text", true)
    if component then
        component.text = text
        return self
    end
end
----------------------------------------------------------------------------------------
--- test.lua
local text = require("TextProxy").new(userdata_text)
text:SetText("test")

另外这里顺便提一下, 有些项目会预定义一些全局方法, 在使用时将userdata传入, 也可以达到附加方法的目的, 但是却无法附加属性.

本文分享的代理对象损失了一定的性能和内存, 但是提高了开发的效率, 如果游戏的某部分对性能和内存有极致的要求, 使用原始的userdata和Lua表会更好.

当然, 游戏的大部分内容对性能和内存的需求并没有那么强烈, 如何使用, 什么地方使用就由各位同学自己决定啦.

希望对大家有所帮助.

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
使用XLua可以在Unity实现Lua脚本的编写,同时也可以让Lua脚本与C#代码进行交互。 以下是基本的使用步骤: 1. 下载XLua插件并导入到Unity。 2. 在Unity创建一个新的Lua文件,编写Lua脚本代码。 3. 在C#代码使用XLua的API来加载并运行Lua脚本,例如: ``` using XLua; public class LuaManager : MonoBehaviour { private LuaEnv luaEnv; void Awake() { luaEnv = new LuaEnv(); luaEnv.DoString("require 'main'"); // 加载并执行Lua脚本 } void Update() { luaEnv.Tick(); // 更新Lua环境 } void OnDestroy() { luaEnv.Dispose(); // 释放Lua环境 } } ``` 在上面的例子,`Awake()`方法创建了一个Lua环境,然后通过`DoString()`方法加载并执行了Lua脚本。`Update()`方法每帧都调用了`Tick()`方法来更新Lua环境,`OnDestroy()`方法释放了Lua环境。需要注意的是,在实际的项目,可能需要更加复杂的逻辑和管理方式来处理Lua脚本的加载和运行。 4. 在Lua脚本使用XLua提供的API来调用C#代码或者导出Lua模块,例如: ``` -- 导出一个Lua模块 local module = {} module.foo = function() print("Hello from Lua!") end return module -- 调用C#代码 local gameObject = CS.UnityEngine.GameObject("LuaObject") local transform = gameObject.transform transform.position = CS.UnityEngine.Vector3(1, 2, 3) ``` 在上面的例子,导出了一个名为`module`的Lua模块,并定义了其的一个函数`foo()`。同时,也调用了C#代码的API来创建了一个新的GameObject,并修改了它的Transform组件的位置。 XLua还提供了其他的功能,例如自定义导出规则、GC优化等。在使用XLua之前,建议先了解一些基本的Lua语法和C#Lua交互的机制,以便更好地使用XLua
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

拂面清风三点水

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值