本文分享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
cls.__index = cls
if not super then
cls.ctor = function() end
else
cls.super = super
setmetatable(cls, super)
end
function cls.new(...)
local instance = setmetatable({}, cls)
instance:ctor(...)
return instance
end
return cls
end
local _M = Class("classA")
_M.classVariable = "hello"
function _M:ctor()
print("classA:ctor")
end
function _M:Func1()
print("classA:Func1")
end
return _M
local _M = Class("classB", require("classA"))
function _M:ctor()
_M.super.ctor(self)
print("classB:ctor")
print("classB:" .. self.classVariable)
self:Func1()
end
return _M
local objectA = require("classA").new()
local objectB = require("classB").new()
下面就几个关键节点进行说明:
- (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的元表访问相应的信息.
function GetSomethingFromUGO(userdata, key)
return userdata[key]
end
function CheckKeyExistOnUserdata(userdata, key)
local selfLookup = userdata[key]
if selfLookup then return true, selfLookup, userdata end
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上查找, 如果查找到则直接设置
- 如果都没有, 直接设置在该对象上
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)
local selfLookup = cls[key]
if selfLookup ~= nil then return selfLookup end
local userdataAttr = GetSomethingFromUGO(key)
if userdataAttr then
local result = userdataAttr
if type(userdataAttr) == "function" then
result = function(_, ...)
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)
local selfLookup = cls[key]
if selfLookup ~= nil then return rawset(self, key, value) end
if string.sub(key, 1, 1) == "_" then
return rawset(self, key, value)
end
if selfLookup == nil then
local find, _, userdata = CheckKeyExistOnUserdata(key)
if find then
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属性的方法:
local _M = Class("TextProxy", require("UserdataProxy"))
return _M
local _M = require("TextProxy")
function _M:SetText(text)
local component = self:GetComponent("Text", true)
if component then
component.text = text
return self
end
end
local text = require("TextProxy").new(userdata_text)
text:SetText("test")
另外这里顺便提一下, 有些项目会预定义一些全局方法, 在使用时将userdata传入, 也可以达到附加方法的目的, 但是却无法附加属性.
本文分享的代理对象损失了一定的性能和内存, 但是提高了开发的效率, 如果游戏的某部分对性能和内存有极致的要求, 使用原始的userdata和Lua表会更好.
当然, 游戏的大部分内容对性能和内存的需求并没有那么强烈, 如何使用, 什么地方使用就由各位同学自己决定啦.
希望对大家有所帮助.
|