文章目录
0.参考文章
- 【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
- 浅探ToLua框架
- ToLua API使用
- tolua之wrap文件的原理与使用
推荐:
https://www.cnblogs.com/movin2333/p/14639142.html
1.框架基本原理
ToLua 框架主要是通过静态绑定来实现 C# 与 Lua 之间的交互的,基本原理是通过建立一个 Lua 虚拟机来映射 C# 脚本,然后再通过这个虚拟机来运行 Lua 脚本,Lua 脚本在运行时可以通过虚拟机反过来调用 C# 脚本里注册过的物体,虚拟机变相地实现了一个解释器的功能,这种方式的优势在于比起使用反射的 uLua 来说效率更高。
2.下载和安装
github地址:https://github.com/topameng/tolua
安装:
将以上三个文件夹保持目录结构导入到unity工程中
3.使用
可查看Assets\ToLua\Examples中的例子
3.1 C#调用 Lua 代码
3.1.1 创建虚拟机
创建虚拟机和启动虚拟机之后,就可以调用 Lua 代码
LuaState state = new LuaState(); // 创建虚拟机
state.Start(); // 启动虚拟机
3.1.2 执行一段 Lua 代码
state.DoString(string chunk, string chunkName = “LuaState.cs”)
:比较少用这种方式加载代码,无法避免代码重复加载覆盖等,需调用者自己保证。第二个参数用于调试信息,或者 error 消息,用于提示出错代码所在文件名称。state.DoFile(string fileName)
:加载一个 lua 文件,fileName 需要扩展名(.lua
),可反复执行,后面的变量会覆盖之前的 DoFile 加载的变量。state.Require(string fileName)
:同 lua require(modname),会判断是否文件已经加载避免重复加载同一文件- 注意: 使用 DoFile 和 Require 方法时,要用
state.AddSearchPath(string fullPath)
手动给目标文件添加一个文件搜索位置。
示例:
LuaState state = new LuaState();
state.Start();
string hello =
@"
print('hello tolua#')
";
state.DoString(hello, "HelloWorld.cs");
//如果移动了ToLua目录,需要自己手动,这里只是例子就不做配置了
string fullPath = Application.dataPath + "/ToLua/Examples/02_ScriptsFromFile";
state.AddSearchPath(fullPath);
state.DoFile("ScriptsFromFile.lua");
state.Require("ScriptsFromFile");
// 垃圾回收, 对于被自动 gc 的 LuaFunction, LuaTable, 以及委托减掉的 LuaFunction, 延迟删除的 Object 之类
// 等等需要延迟处理的回收, 都在这里自动执行
state.Collect();
// 检查是否堆栈是否平衡,一般放于 update 中,c# 中任何使用 lua 堆栈操作,
// 都需要调用者自己平衡堆栈(参考 LuaFunction 以及 LuaTable 代码),
// 当 CheckTop 出现警告时其实早已经离开了堆栈操作范围,这时需自行 review 代码
state.CheckTop();
// 释放LuaState 以及其资源
state.Dispose();
state= null;
3.1.3 调用 Lua 变量/函数
3.1.3.1 获取 Lua 变量/函数
state[string]
:通过 LuaState [string] 的形式就可以直接获取到,也可以通过这个表达式来直接赋值,取到的是 object 类型。
state[“变量名”]
:通过这种方式对 lua 中的变量进行访问state[“方法名”] as LuaFunction
:LuaState 对象 转 LuaFunction,调用 lua 中的函数GetFunction(“test.luaFunc”)
:获取lua中指定test表的luaFunc函数
3.1.3.2 调用 Lua 函数
state.Invoke()
:临时调用一个 lua function 并返回一个值,这个操作并不缓存 lua function,适合频率非常低的函数调用,可带参数。state.Call()
:看起来和 Invoke 类似,只是无返回值。state.GetFunction(string)
: 获取并缓存一个 lua 函数,string 支持串式操作。LuaFunction.Call()
:不需要返回值的函数调用操作。LuaFunction.Invoke()
:有一个返回值的函数调用操作。LuaFunction.BeginPCall()
:开始函数调用。LuaFunction.Push()
:压入函数调用需要的参数。LuaFunction.PCall()
:调用 lua 函数。LuaFunction.CheckNumber()
:提取函数返回值, 并检查返回值为 lua number 类型。
*LuaFunction.EndPCall()
:结束 lua 函数调用,清楚函数调用造成的堆栈变化。luaFunc.ToDelegate
:创建一个委托,后续直接调用委托即可。LuaFunction.Dispose()
:释放 LuaFunction,递减引用计数,如果引用计数为 0,则从 _R 表删除该函数。
private string script =
@" function luaFunc(num)
return num + 1
end
";
void Start ()
{
LuaState state = new LuaState();
state.Start();
DelegateFactory.Init();
state.DoString(script);
state.Call<int>("luaFunc", 123456, false);
Debugger.Log("state.Call");
int num = state.Invoke<int, int>("luaFunc", 123456, false);
Debugger.Log("state.Invoke: {0}", num); // 123457
LuaFunction luaFunc = state.GetFunction("luaFunc");
if (luaFunc != null)
{
luaFunc.Call<int>(123456);
Debugger.Log("LuaFunction.Call");
num = luaFunc.Invoke<int, int>(123456);
Debugger.Log("LuaFunction.Invoke: {0}", num); // 123457
luaFunc.BeginPCall();
luaFunc.Push(123456);
luaFunc.PCall();
num = (int)luaFunc.CheckNumber();
luaFunc.EndPCall();
Debugger.Log("LuaFunction.PCall: {0}", num); // 123457
Func<int, int> func = luaFunc.ToDelegate<Func<int, int>>();
num = func(123456);
Debugger.Log("LuaFunction.ToDelegate: {0}", num); // 123457
}
state.CheckTop();
luaFunc.Dispose();
luaFunc = null;
state.Dispose();
state = null;
}
3.1.4 ToLua table 方法
这里完全可以将 table 看一个 LuaState 来进行操作,ToLua 对 table 的数据结构进行了解析,实现了非常多的方法:
state[“变量名”]
:state通过重载this操作符,访问lua _G表中的变量state.GetTable(string)
:从 state 中获取一个 table, 可以串式访问,比如:state.GetTable(“varTable.map.name”) 等于 varTable->map->name- LuaTable 支持this操作符,但此this不支持串式访问。比如:table[“map.name”] ,“map.name” 只是一个key,不是table->map->name 。
table.GetMetaTable()
:可以获取当前 table 的 metatable。table.ToArray()
:获取数组表中的所有对象存入到object[]表中。table.AddTable(name)
:在当前的table表中添加一个名字为name的表。table.GetTable(key)
:获取 table[key] 值,类似于 lua_gettable。table.SetTable(key, value)
:等价于 table[k] = v,类似于lua_settable。table.RawGet(key)
:获取 table[key]值,类似于 lua_rawgettable.RawSet(key, value)
:等价于 table[k] = v,类似于lua_rawset。
private string script =
@"
print('Objs2Spawn is: '..Objs2Spawn)
var2read = 42
varTable = {1,2,3,4,5}
varTable.default = 1
varTable.map = {}
varTable.map.name = 'map'
meta = {name = 'meta'}
setmetatable(varTable, meta)
function TestFunc(strs)
print('get func by variable')
end
";
void Start ()
{
LuaState state = new LuaState();
state.Start();
state["Objs2Spawn"] = 5;
state.DoString(script);
//通过LuaState访问
Debugger.Log("Read var from lua: {0}", state["var2read"]); // 42
Debugger.Log("Read table var from lua: {0}", state["varTable.default"]); // 1
//cache成LuaTable进行访问
LuaTable table = state.GetTable("varTable");
// table["map.name"]:不支持串式访问
Debugger.Log("Read varTable from lua, default: {0} name: {1}", table["default"], table["map.name"]); // default: 1 name:
table["map.name"] = "new"; //table 字符串只能是key
Debugger.Log("Modify varTable name: {0}", table["map.name"]); // new
table.AddTable("newmap");
LuaTable table1 = (LuaTable)table["newmap"];
table1["name"] = "table1";
Debugger.Log("varTable.newmap name: {0}", table1["name"]); // table1
table1.Dispose();
table1 = table.GetMetaTable();
if (table1 != null)
{
Debugger.Log("varTable metatable name: {0}", table1["name"]); // meta
}
object[] list = table.ToArray();
for (int i = 0; i < list.Length; i++)
{
Debugger.Log("varTable[{0}], is {1}", i, list[i]); // 1, 2, 3, 4, 5
}
table.Dispose();
state.CheckTop();
state.Dispose();
}
3.2 Lua 调用C#代码
3.2.1 Lua 中使用 coroutine(携程)
使用Lua携程必须添加LuaLooper脚本
coroutine.start
:启动一个lua携程coroutine.wait
:携程中等待一段时间,单位:秒coroutine.step
:携程中等待一帧.coroutine.www
:等待一个WWW完成.tolua.tolstring
:转换byte数组为lua字符串缓冲coroutine.stop
:停止一个正在lua将要执行的携程
Lua脚本:
-------tolua官方案例:初始化时,C#调用此函数开启协程----------
function TestCortinue()
coroutine.start(CoFunc)
end
local coDelay = nil
------------------------每隔1秒打印一次输出------------------
function Delay()
local c = 1
while true do
coroutine.wait(1)
print("Count: "..c)
c = c + 1
end
end
----------------------------开启协程------------------------
function StartDelay()
coDelay=coroutine.start(Delay)
end
----------------------------停止协程------------------------
function StopDelay()
coroutine.stop(coDelay)
end
----------------------------每隔0.1秒计算一次------------------------
function fib(n)
local a, b = 0, 1
while n > 0 do
a, b = b, a + b
n = n - 1
end
return a
end
----------------------------协程运行函数------------------------
function CoFunc()
print('Coroutine started')
for i = 0, 10, 1 do
print(fib(i))
coroutine.wait(0.1)--等待0.1秒
end
print("current frameCount: "..Time.frameCount)
coroutine.step()--等待一帧
print("yield frameCount: "..Time.frameCount)
local www = UnityEngine.WWW("http://www.baidu.com")
coroutine.www(www)--开启www请求
local s = tolua.tolstring(www.bytes)
print(s:sub(1, 128))
print('Coroutine ended')
end
C#脚本:
new LuaResLoader();
lua = new LuaState();
lua.Start();
LuaBinder.Bind(lua); // 绑定lua对象
DelegateFactory.Init(); // 委托工厂初始化
looper = gameObject.AddComponent<LuaLooper>(); // 使用协程必须添加
looper.luaState = lua; // 指定对象
lua.DoString(luaFile.text, "TestLuaCoroutine.lua");
LuaFunction f = lua.GetFunction("TestCortinue");
f.Call();
f.Dispose();
f = null;
3.2.1 Lua 中使用数组
func.Push(arrayInts)
:将 数组 arrayInts 传至 Lua 中func.CheckNumber()
:得到 Lua 对应函数返回值中的:值类型func.CheckString()
:得到 Lua 对应函数返回值中的:字符串类型func.CheckBoolean()
:得到 Lua 对应函数返回值中的:布尔类型
注意: 书写顺序应该是与函数返回值先后顺序保持一致,否则会报错
Lua代码:
private string script =
@"
function TestArray(array)
----------------------------获取数组长度------------------------
local len = array.Length
----------------------------遍历数组输出------------------------
for i = 0, len - 1 do
print('数组: '..tostring(array[i]))
end
----------------------------获取枚举器--------------------------
local iter = array:GetEnumerator()
----------------------调用数组函数MoveNext()--------------------
while iter:MoveNext() do
print('依次取: '..iter.Current)
end
----------------------数组转表----------------------------------
local t = array:ToTable()
for i = 1, #t do
print('转表: ' .. tostring(t[i]))
end
----------------------调用数组函数BinarySearch(3)---------------
local pos = array:BinarySearch(3)
print('数组二进制 BinarySearch: 下标是: '..pos..' 值: '..array[pos])
----------------------调用数组函数IndexOf(4)--------------------
pos = array:IndexOf(4)
print('数组 indexof 4 下标是: '..pos)
----------------------返回值参数--------------------------------
return 5, 'Chinar', true
end
"
————————————————
C#代码:
LuaState lua = null;
LuaFunction func = null;
string tips = null;
void Start()
{
new LuaResLoader();
tips = "";
lua = new LuaState();
lua.Start();
lua.DoString(script, "ChinarTestArray.cs"); //运行 script 字符串中的lua脚本,指定C#脚本路径
int[] arrayInts = {
1, 2, 3, 4, 5};
func = lua.GetFunction("TestArray"); //取得 lua script 脚本中的 TestArray 函数
func.BeginPCall();
#region 第一种写法
func.Push(arrayInts);
func.PCall();
// 接受返回值时顺序要一一对应。 顺序写错,会报错
// 另外 int string float 这些会自动完成转换,不会报错
// 如: 10.3 —— func.CheckValua<int>(); 这里会自动 返回 int
// 如: 1 —— func.CheckNumber();
// 如: `123` —— func.CheckString();
// 如: true —— func.CheckBoolean();
double arg1 = func.CheckNumber();
string arg2 = func.CheckString();
bool arg3 = func.CheckBoolean();
Debugger.Log("返回值:{0}|{1}|{2}", arg1, arg2, arg3);
//-------------------------------------------
func.EndPCall();
#endregion
#region 第二种通用写法
//调用通用函数需要转换一下类型,避免可变参数拆成多个参数传递
//func.LazyCall((object) arrayInts);
//相当于 ↓↓
//func.Push(arrayInts);
//func.PCall();
object[] objects = func.LazyCall((object) arrayInts);
if (objects != null)
{
Debugger.Log("return is {0} {1} {2}", objects[0], objects[1], objects[2]);
}
func.EndPCall();
lua.CheckTop(); //检查返回值
#endregion
}
————————————————
3.2.2 Lua 中使用字典
map.Add(1, new TestAccount(1, “水水”, 0))
:字典中添加元素:利用构造函数,新建测试账号对象时,在对象中直接完成赋值
注意:
这对于初学者是一个老生常谈的问题了
Lua 中调用C#函数用“:”,字段用“.”
: —— map:GetEnumerator()/iter:MoveNext()
. ——iter.Current.name /map.Keys
C#脚本:
using UnityEngine;
using System.Collections.Generic;
using LuaInterface;
/// <summary>
/// Chinar解释 —— tolua官方案例9
/// Lua中使用字典
/// </summary>
public sealed class TestAccount
{
public int id;
public string name;
public int sex;
/// <summary>
/// 构造函数
/// </summary>
public TestAccount(int id, string name, int sex)
{
this.id = id;
this.name = name;
this.sex = sex;
}
}
public class UseDictionary : MonoBehaviour
{
private Dictionary<int, TestAccount> map = new Dictionary<int, TestAccount>();
string script =
@"
function TestDict(map)
------------调用字典函数GetEnumerator()---------------
local iter = map:GetEnumerator()
-------------调用字典函数MoveNext()-------------------
while iter:MoveNext() do
local v = iter.Current.Value
print('id: '..v.id .. ' name: '..v.name..' sex: '..v.sex)
end
-------------调用字典函数TryGetValue()-----------------
local flag, account = map:TryGetValue(1, nil)
if flag then
print('TryGetValue 返回结果正确时: '..account.name)
end
--------------------获取字典中的键---------------------
local keys = map.Keys
iter = keys:GetEnumerator()
print('------------打印输出字典中的键---------------')
while iter:MoveNext() do
print(iter.Current)
end
print('----------------------over----------------------')
local values = map.Values
iter = values:GetEnumerator()
print('------------打印输出字典中的值---------------')
while iter:MoveNext() do
print(iter.Current.name)
end
print('----------------------over----------------------')
-----------获取字典中2键对应的name值---------------------
print('kick '..map[2].name)
-----------移除map中下标2对应的元素---------------------
map:Remove(2)
iter = map:GetEnumerator()
-----------重新输出---------------------
while iter:MoveNext() do
local v = iter.Current.Value
print('id: '..v.id .. ' name: '..v.name..' sex: '..v.sex)
end
end
";
void Awake()
{
#if UNITY_5 || UNITY_2017 || UNITY_2018
Application.logMessageReceived += ShowTips;
#else
Application.RegisterLogCallback(ShowTips);
#endif
new LuaResLoader();
map.Add(1, new TestAccount(1, "水水", 0));
map.Add(2, new TestAccount(2, "王伟", 1));
map.Add(3, new TestAccount(3, "王芳", 0));
LuaState luaState = new LuaState();
luaState.Start();
BindMap(luaState);
luaState.DoString(script, "UseDictionary.cs");
LuaFunction func = luaState.GetFunction("TestDict");
func.BeginPCall();
func.Push(map);
func.PCall();
func.EndPCall();
func.Dispose();
func = null;
luaState.CheckTop();
luaState.Dispose();
luaState = null;
}
void OnApplicationQuit()
{
#if UNITY_5 || UNITY_2017 || UNITY_2018
Application.logMessageReceived -= ShowTips;
#else
Application.RegisterLogCallback(null);
#endif
}
string tips = "";
void ShowTips(string msg, string stackTrace, LogType type)
{
tips += msg;
tips += "\r\n";
}
void OnGUI()
{
GUI.Label(new Rect(Screen.width / 2 - 300, Screen.height / 2 - 200, 600, 400), tips);
}
//示例方式,方便删除,正常导出无需手写下面代码
//Chinar:这里不用管,这是tolua作者为方便我们删除示例预留的代码
//为了保证删除示例是,不影响我们的整体工程的其他 wrap 文件
void BindMap(LuaState L)
{
L.BeginModule(null);
TestAccountWrap.Register(L);
L.BeginModule("System");
L.BeginModule("Collections");
L.BeginModule("Generic");
System_Collections_Generic_Dictionary_int_TestAccountWrap.Register(L);
System_Collections_Generic_KeyValuePair_int_TestAccountWrap.Register(L);
L.BeginModule("Dictionary");
System_Collections_Generic_Dictionary_int_TestAccount_KeyCollectionWrap.Register(L);
System_Collections_Generic_Dictionary_int_TestAccount_ValueCollectionWrap.Register(L);
L.EndModule();
L.EndModule();
L.EndModule();
L.EndModule();
L.EndModule();
}
}
3.2.3 Lua 中使用枚举类型
LuaBinder.Bind(lua)
:首先这一步不能少
lua[“space”] = Space.World
:Lua 脚本中的 space 变量,进行枚举类型赋值
C#代码:
using UnityEngine;
using LuaInterface;
/// <summary>
/// Chinar解释 —— tolua官方案例 10
/// Lua中使用Unity枚举类型
/// </summary>
public class AccessingEnum : MonoBehaviour
{
string script =
@"
space = nil
function TestEnum(e)
print('枚举是:'..tostring(e))
----------------枚举类型转int----------------
if space:ToInt() == 0 then
print('枚举转Int成功')
end
-------------枚举类型int与0比较--------------
if not space:Equals(0) then
print('枚举类型(与int型)比较成功')
end
-------------枚举类型直接比较----------------
if space == e then
print('枚举类型比较成功')
end
-------调用Unity中的Space枚举值转枚举--------
local s = UnityEngine.Space.IntToEnum(0)
if space == s then
print('int转枚举,比较成功')
end
end
----------------改变Unity光照类型---------------
function ChangeLightType(light, type)
print('改变光照类型为: '..tostring(type))
light.type = type
end
";
LuaState state = null;
string tips = "";
int count = 1;
/// <summary>
/// 格式化打印输出
/// </summary>
void ShowTips(string msg, string stackTrace, LogType type)
{
tips += msg;
tips += "\r\n";
}
/// <summary>
/// 初始化函数
/// </summary>
void Start()
{
#if UNITY_5 || UNITY_2017 || UNITY_2018
Application.logMessageReceived += ShowTips;
#else
Application.RegisterLogCallback(ShowTips);
#endif
new LuaResLoader();
state = new LuaState();
state.Start();
LuaBinder.Bind(state); //绑定,必须要写
state.DoString(script);
state["space"] = Space.World; //Lua脚本中的space变量,进行枚举类型赋值
LuaFunction func = state.GetFunction("TestEnum");
func.BeginPCall();
func.Push(Space.World);
func.PCall();
func.EndPCall();
func.Dispose();
func = null;
}
/// <summary>
/// 绘制函数
/// </summary>
void OnGUI()
{
GUI.Label(new Rect(Screen.width / 2 - 300, Screen.height / 2 - 200, 600, 400), tips);
if (GUI.Button(new Rect(0, 60, 120, 50), "ChangeType"))
{
Light light = GameObject.Find("/Light").GetComponent<Light>(); //获取层次列表中对象的Light组件
LuaFunction func = state.GetFunction("ChangeLightType");
func.BeginPCall();
func.Push(light); //将light传入ChangeLightType函数
LightType type = (LightType) (count++ % 4); //简单计算得到类型
func.Push(type); //将type类型传入ChangeLightType函数
func.PCall(); //调用ChangeLightType函数
func.EndPCall();
func.Dispose();
}
}
/// <summary>
/// 应用退出时执行函数
/// </summary>
void OnApplicationQuit()
{
state.CheckTop();
state.Dispose();
state = null;
#if UNITY_5 || UNITY_2017 || UNITY_2018
Application.logMessageReceived -= ShowTips;
#else
Application.RegisterLogCallback(null);
#endif
}
}
3.3 Lua 调用自定义C#类中方法
推荐文章
:https://www.cnblogs.com/movin2333/p/14639142.html
- 新建供Lua调用的C#类和Mono类
C#类:
public class CsScript
{
public int idx;
// int Idx { get => idx; set => idx = value; }
public CsScript() {
}
public CsScript(int idx)
{
this.idx = idx;
}
public int Sum(int a, int b)
{
return a + b;
}
}
Mono类:
public class MomoCsScript : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
Debug.Log("Statr_ming");
}
// Update is called once per frame
void Update()
{
}
}
- 在CustomSettings中注册
- 生成Warp文件
- 编写主入口代码
public class Enter : MonoBehaviour
{
LuaState lua = null;
LuaFunction luaFunc = null;
// Start is called before the first frame update
void Awake()
{
new LuaResLoader();
lua = new LuaState();
lua.Start();
LuaBinder.Bind(lua);
string luaPath = Application.dataPath + "/Scripts";//注意这里的文件位置
lua.AddSearchPath(luaPath);
lua.DoFile("TestLuaCallCS.lua");
CallFunc("TestLuaCallCS.Awake", this.gameObject);
}
private void OnApplicationQuit()
{
lua.Dispose();
lua = null;
}
void CallFunc(string func, GameObject obj)
{
luaFunc = lua.GetFunction(func);
luaFunc.Call(obj);
luaFunc.Dispose();
luaFunc = null;
}
}
- 编写Lua代码
TestLuaCallCS = {
};
local this = TestLuaCallCS;
local GameObject = UnityEngine.GameObject
local AudioSource = UnityEngine.AudioSource
local test = CsScript(18)
function this.Awake(object)
local go = GameObject.Find('GameObject')
go:AddComponent(typeof(AudioSource));
go:AddComponent(typeof(MomoCsScript));
print(test:Sum(1, 2))
print(test.idx)
print(go.name)
end
- 结果