XLua-Unity框架初探

文章目录

      • 前言
      • 应用场景
      • XLua 常用 API
      • 框架实例搭建
        • 需求
        • 实现
      • 实例,实现一朵云的运动
      • 其他

前言

Lua由于其简单易用,方便热更等性质,一直是游戏行业的首推脚本语言。Unity引擎也诞生了很多款为其适配的Lua虚拟机运行环境,主要有XLua,SLua,ToLua和ULua,本文不会着力比较这几种框架的实现差异,只讨论其中背靠大厂腾讯的XLua框架。

应用场景

  • UI逻辑代码
  • Hotfix
  • 网络层
  • 除计算以外的常规逻辑(此处的计算指涉及矩阵等运算的场合)

XLua 常用 API

此处只会介绍常用的,合乎规范的内容,拓展阅读请访问XLua github。

  • Lua虚拟机

XLua中对Lua虚拟机进行了封装,建立和移除一个全局环境很简单:

_luaEnv = new XLua.LuaEnv();
_luaEnv.Dispose();

在大致查看XLua C#部分封装中,可以看到,它将Lua的c通过dll形式引入 LuaAPI,然后对线程安全,异常等做了特殊处理。

  • require

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

使用 DoString 来实现Lua载入字符的功能,这块也是热更的重要功能。

_luaEnv.DoString("require('main')");
  • c# call lua

使用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是很难限定为一种值的,所以此处不多介绍,有兴趣可以访问上面官方库。

  • lua call c#

通过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#侧只能通过反射等方案实现赋值行为,效率存疑,此处不推荐使用。

框架实例搭建

需求
  • 像cs脚本一样调用lua
  • 执行Mono的生命周期函数
  • 属性在inspector可编辑
实现

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
XLua-Unity框架初探_第1张图片
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进行复制重命名。

你可能感兴趣的:(Unity3d)