IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 游戏开发 -> Unity中使用XLua时为C#对象绑定代理对象 -> 正文阅读

[游戏开发]Unity中使用XLua时为C#对象绑定代理对象

本文分享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表会更好.

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

希望对大家有所帮助.

  游戏开发 最新文章
6、英飞凌-AURIX-TC3XX: PWM实验之使用 GT
泛型自动装箱
CubeMax添加Rtthread操作系统 组件STM32F10
python多线程编程:如何优雅地关闭线程
数据类型隐式转换导致的阻塞
WebAPi实现多文件上传,并附带参数
from origin ‘null‘ has been blocked by
UE4 蓝图调用C++函数(附带项目工程)
Unity学习笔记(一)结构体的简单理解与应用
【Memory As a Programming Concept in C a
上一篇文章      下一篇文章      查看所有文章
加:2021-09-29 10:39:01  更:2021-09-29 10:41:08 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/28 3:27:16-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码