Lua由于其简单易用,方便热更等性质,一直是游戏行业的首推脚本语言。Unity引擎也诞生了很多款为其适配的Lua虚拟机运行环境,主要有XLua,SLua,ToLua和ULua,本文不会着力比较这几种框架的实现差异,只讨论其中背靠大厂腾讯的XLua框架。
此处只会介绍常用的,合乎规范的内容,拓展阅读请访问XLua github。
XLua中对Lua虚拟机进行了封装,建立和移除一个全局环境很简单:
_luaEnv = new XLua.LuaEnv();
_luaEnv.Dispose();
在大致查看XLua C#部分封装中,可以看到,它将Lua的c通过dll形式引入 LuaAPI,然后对线程安全,异常等做了特殊处理。
XLua为了方便使用require,添加了 AddLoader 方法:
_luaEnv.AddLoader(LoadLua);
static byte[] LoadLua(ref string luaname)
{
string realPath = @"LuaScripts/" + luaname + ".lua";
TextAsset luaContent = Resources.Load(realPath) as TextAsset;
return luaContent.bytes;
}
此处LoadLua是一个委托,使用Lambda函数亦可。
XLua支持多个Loader,会去遍历调用,直到读出数据为止。如果不添加任何Loader,它支持默认从Resources根目录读取后缀为“.txt”的lua文件。
使用 DoString 来实现Lua载入字符的功能,这块也是热更的重要功能。
_luaEnv.DoString("require('main')");
使用LuaTable来映射Lua表:
LuaTable luaMonoMap = _luaEnv.Global.Get<LuaTable>("luaMonoMap");
LuaTable是XLua的重要基类,其中封装了对Lua键值对的调用,Global是_G表,同样是一个LuaTable,通过Get来访问各个键值。
使用[CSharpCallLua]声明一个映射的interface
[CSharpCallLua]
public interface LuaContent
{
void this_print();
}
_luaEnv.Global.Get<LuaContent>("testContent");
我们在指明了该attribute后,再generate后,会产生一个名为ClassNameBridge的新类(此例中为LuaContentBridge),该类会继承这个interface和LuaBase,LuaBase主要是处理gc等内容,后面的文章会详细研究。在重写的函数实现中,会对LuaState直接进行操作,将函数和参数入栈,进行调用。
注意: 该映射并非真正的一一对应,只需要在运行时,lua函数能正常被访问到即可,也就是说,Lua侧可以远远多于该interface的内容。
还有两种方式,自定义类访问 和 Dictionary或List访问,但是由于这两种类型都是值类型访问,上面的是引用类型访问,而且只能访问同样的类型,就是说限定Lua表中都是同一种值,或者被转化为同一种值,但是Lua的TValue是很难限定为一种值的,所以此处不多介绍,有兴趣可以访问上面官方库。
通过CS.namespace.class.func_or_property来访问c#内容。
UnityEngine的类在虚拟机构造时已经将大部分类和类指针注入到 fix_cs_functions 中,所以可以访问到这些函数。
自定义类需要添加标签[LuaCallCSharp]:
[LuaCallCSharp]
public class LuaCall{}
构造函数直接赋值类名即可:
local lc = CS.LuaCall
为了支持lua的多返回值,c#侧使用ref,out等关键字来实现。
参数处理规则:Lua调用C#方法的时候,C#方法中的参数,从左到右,普通参数算一个,ref修饰的算一个,out修饰的不算。
返回值处理规则:Lua接受C#方法返回值的时候,C#方法的返回值(如果有)算一个返回值,参数ref算一个返回值,out算一个返回值。
注意: 重载函数,XLua支持多参数重载,意思是重载函数的参数不同,能够正常支持,lua默认类型,number内部不支持重载,number和string支持重载,否则调用生成代码的第一个函数副本,如果此时不能实现强制转换,则会抛出异常,比如:
trans.LookAt(Vector(1, 1, 1)) -- 报错,只支持GameObject作为参数
调用delegate时重载了+,-运算符,和c#一样。
注意:XLua中还有强制转化table为c#类型的方案,但是由于lua层传递的仅仅是一个table,又没有生成wrapper的中间代码,所以在c#侧只能通过反射等方案实现赋值行为,效率存疑,此处不推荐使用。
1、 Env Mng
实现一个单例,将 _luaEnv 等属性挂在上面,在 GameInit 时初始化虚拟机,加载所有的lua文件到_G表,这一块比较简单,代码就不放了。
2、Lua到Mono的接口映射
先看c#侧的实现
[CSharpCallLua]
public interface LuaMonoReflection
{
void Awake();
void OnEnable();
void Start();
void FixedUpdate();
void Update();
void LateUpdate();
void OnDisable();
void OnDistroy();
string CreateInst(string className);
void DestroyInst(string instName);
void LinkValue(string k, float v);
MonoBehaviour owner { set; get; }
}
此处只绑定逻辑函数,其他渲染等一般不放在lua侧处理。
再来看看lua侧的实现:
-- abstract
_G.LuaMonoBase = _G.LuaMonoBase or {
owner = nil,
_instNum = 0,
}
...
-- 省略生命周期函数
...
-- 继承方法
function LuaMonoBase:extends()
local obj = {}
obj._instNum = 0
self.__index = self
setmetatable(obj, self)
return obj
end
-- for gc
_G.luaMonoMap = _G.luaMonoMap or {}
-- 生成实例
function LuaMonoBase:CreateInst(originClassName)
self._instNum = self._instNum + 1
local instName = string.format("%s_INST_%d", originClassName, self._instNum)
local obj = self:extends()
_G.luaMonoMap[originClassName] = _G.luaMonoMap[originClassName] or {}
_G.luaMonoMap[originClassName][instName] = obj
return instName
end
function LuaMonoBase:DestroyInst(instName)
if instName == "" then
return
end
local nameArr = string.split(instName, "_")
local className = nameArr[1]
local num = nameArr[3]
_G.luaMonoMap[className][instName] = null
end
function LuaMonoBase:LinkValue(key, value)
self[key] = value
end
此处注意,在lua侧我们必须使用面向对象的方案。试想一下,如果我们调用全局table,那么所有的lua脚本都是索引到同一个table,除非在UI层和使用单例的mng层,否则都会要求生成table的实例。
我们这里提供了CreateInst方法来实现实例化,并将实例对象存放在一个全局管理的表里面,方便我们做gc和分析,甚至某些时候我们能重写该函数,使用对象池的调用,而不是将其直接进行垃圾回收。
3、MonoBehavior的脚本绑定
public class LuaBridge : MonoBehaviour
{
public string ScriptName = "DefaultName";
public LuaKeyValue[] LuaKV = { };
private string _instName = "";
private LuaMonoReflection _luaObj = null;
// Start is called before the first frame update
void Awake()
{
if (_luaObj == null)
{
var ClassObj = GameInit.LUA_ENV.Global.Get<LuaMonoReflection>(ScriptName);
_instName = ClassObj.CreateInst(ScriptName);
_luaObj = GetInst(ScriptName, _instName);
_luaObj.owner = this;
int len = LuaKV.Length;
for (int i = 0; i < len; ++i)
{
_luaObj.LinkValue(LuaKV[i].Key, LuaKV[i].Value);
}
}
if (_luaObj != null)
_luaObj.Awake();
}
...
void OnDistroy()
{
if (_luaObj != null)
_luaObj.OnDistroy();
_luaObj.DestroyInst(_instName);
_instName = "";
}
LuaMonoReflection GetInst(string className, string instName)
{
return GameInit.LUA_ENV.Global.Get<LuaTable>("luaMonoMap").Get<LuaTable>(className).Get<LuaMonoReflection>(instName);
}
}
这是直接绑定到gameobject上的脚本,注意我们需要将this传递到lua层。
此处lua层的属性可以通过LuaKeyValue来进行编辑,会在运行时对table进行赋值。
此处实例为了此框架的运用范围,并未进行性能测试,后续完善后会进行性能测试并制定使用的规范。
1、首先添加C#脚本 LuaBridge, lua脚本命名为CloudBehaviour
2、CloudBehaviour实现
local GameObject = CS.UnityEngine.GameObject
local Transform = CS.UnityEngine.Transform
local Vector3 = CS.UnityEngine.Vector3
local Time = CS.UnityEngine.Time
local Quaternion = CS.UnityEngine.Quaternion
CloudBehaviour = LuaMonoBase:extends()
function CloudBehaviour:Start()
self._center = Vector3(70, 254, 45)
local pos = self.owner:GetComponent(typeof(Transform)).position
local x = pos.x - self._center.x
local z = pos.z - self._center.z
self._radius = math.sqrt(x * x + z * z)
local arc = math.asin(z / self._radius)
self._curRotation = x < 0 and math.pi - arc or arc
end
function CloudBehaviour:FixedUpdate()
local trans = self.owner:GetComponent(typeof(Transform))
local pos = trans.position
self._curRotation = math.lerp(self._curRotation, self._curRotation + self._rSpeed, Time.deltaTime)
local x = self._center.x + math.cos(self._curRotation) * self._radius
local z = self._center.z + math.sin(self._curRotation) * self._radius
local y = pos.y + math.sin(Time.realtimeSinceStartup) * self._floatSpeed
trans.position = Vector3(x, y, z)
pos = trans.position
trans.rotation = Quaternion.LookRotation(Vector3(self._center.x, pos.y, self._center.z) - pos)
end
该类是对LuaMonoBase的派生。
我们在文件开头对常用类型进行了索引,避免去c#侧多次索引,可以建立一个表将常用类全部索引一次。不过XLua对这些行为是进行了优化的,在CS表的__index里面有很多优化,后面的文章会详细测试。
注意,每次写完带有XLua属性的代码时,需要重新generate,generate时注意先clear。某些ide禁止外部读写,所以有时会生成代码失败,需要关闭ide。
Resources目前不能加载.lua类型的文件,所以要添加后缀.txt,此处为了方便编辑,可以写一个py进行复制重命名。