结合官方文档的 xLua 学习笔记,实现 Lua 和 c# 之间的相互调用
什么是热更新
简单来说就是当游戏某个功能出现 bug,或者修改了某个功能,或者增加了某个功能的时候,我们不需要重新下载安装安装包,就可以更新游戏内容。
热更新的好处:不用浪费流量重新下载,不用通过商店审核更加快速,不用重新安装玩家可以更快体验到更新的内容
热更新方案
这些热更新方案都是基于 Lua 语言的,也可以叫做 lua 插件。
- ulua(不再维护,转到主要维护 tolua)
- tolua(基于 tolua 开发了 luaframework)
- slua 代码质量好,性能比 tolua 低
xLua 简介
xLua 是 Unity3D 下的 Lua 编程解决方案,作者是腾讯的车雄生前辈,xLua 全平台支持用 Lua 修复 C# 代码的 bug,借助 xLua,这些 Lua 代码可以方便的和 C# 相互调用。
2016年12月末,xLua 刚刚实现新的突破:全平台支持用 Lua 修复 C# 代码 bug。
目前 Unity 下的 Lua 热更新方案大多都是要求要热更新的部分一开始就要用 Lua 语言实现,不足之处在于:
- 接入成本高,有的项目已经用 C# 写完了,这时要接入需要把需要热更的地方用 Lua 重新实现;
- 即使一开始就接入了,也存在同时用两种语言开发难度较大的问题;
- Lua 性能不如 C#;
xLua 热补丁技术支持在运行时把一个 C# 实现(函数,操作符,属性,事件,或者整个类)替换成 Lua 实现,意味着你可以:
- 平时用 C# 开发;
- 运行也是 C#,性能秒杀 Lua;
- 有 bug 的地方下发个 Lua 脚本 fix 了,下次整体更新时可以把 Lua 的实现换回正确的 C# 实现,更新时甚至可以做到不重启游戏;
导入 xLua
- 去 GitHub Cloen or download
- 解压后,将 Assets 文件夹下的所有文件复制到自己工程的 Assets 文件夹下
- XLua 里包含文档和教程,可自行选择是或否需要
HelloWorld - 执行字符串
using UnityEngine;
using XLua; // xLua
namespace XLuaTest {
public class Helloworld : MonoBehaviour {
private LuaEnv luaEnv; // 一个 LuaEnv 实例对应 Lua 虚拟机,出于开销的考虑,建议全局唯一。
private void Start() {
luaEnv = new LuaEnv(); // Lua 环境
luaEnv.DoString("print('hello world lua')"); // 运行 Lua 程序,String 内容需要符合 Lua 语法规则,输出带有 Lua 标识,LUA: hello world lua
luaEnv.DoString("CS.UnityEngine.Debug.Log('hello world')"); // 在 Lua 中调用 c# API
}
private void OnDestroy() {
luaEnv.Dispose(); // 释放环境
}
}
}
加载 Lua 文件
因为 Resource 只支持有限的后缀,Load<TextAsset>
会自动为读取的文件添加 txt 的后缀,但又为了区分是 lua 文件,所以将 lua 文件命名为 HelloWorld.lua.txt
HelloWorld.lua.txt
print('HelloWorld -- Lua')
利用 TextAsset 加载
using UnityEngine;
using XLua;
public class HelloWorldByFile : MonoBehaviour {
private void Start() {
// lua 文件名为: HelloWorld.lua.txt
TextAsset ta = Resources.Load<TextAsset>("HelloWorld.lua");
LuaEnv luaEnv = new LuaEnv();
luaEnv.DoString(ta.text); // 运行 Lua 程序
luaEnv.Dispose();
}
}
利用 Loader 加载
用 lua 的 require 函数,require 实际上是调一个个的 loader 去加载,有一个成功就不再往下尝试,全失败则报文件找不到。
建议的加载 Lua 脚本方式是:整个程序就一个 DoString(“require ‘main’”),然后在 main.lua 加载其它脚本(类似 lua脚本的命令行执行:lua main.lua)。
using UnityEngine;
using XLua;
public class HelloWorldByFile : MonoBehaviour {
private void Start() {
LuaEnv luaEnv = new LuaEnv();
luaEnv.DoString("require 'HelloWorld'"); // 利用 Loader 加载
luaEnv.Dispose();
}
}
自定义 Loader
因为系统自带的 Loader 需要 lua 文件放在 Resources 文件夹下,那要是我的 Lua 文件是下载回来的,或者放在自定义的目录里面,怎么办?问得好,xLua 的自定义 Loader 可以满足这些需求。
using UnityEngine;
using System.Collections;
using XLua;
using System.IO;
namespace Tutorial {
public class CustomLoader : MonoBehaviour {
LuaEnv luaenv = null;
void Start() {
luaenv = new LuaEnv();
// public void LuaEnv.AddLoader(CustomLoader loader)
luaenv.AddLoader(MyLoader);
luaenv.DoString("require 'HelloWorld'"); // 利用 Loader 加载
}
/// <summary>
/// 自定义 Loader
/// public delegate byte[] CustomLoader(ref string filepath);
/// </summary>
private byte[] MyLoader(ref string filePath) {
string absParh = "D:/" + filePath + ".lua.txt"; // lua 文件放在自定义目录 D 盘根目录下
return System.Text.Encoding.UTF8.GetBytes(File.ReadAllText(absParh)); // 将 Lua 程序转换为了字节数组
}
void OnDestroy() {
luaenv.Dispose();
}
}
}
C# 访问 Lua
这里指的是 C# 主动发起对 Lua 数据结构的访问。
-
访问 lua 全局数据,特别是 table 以及 function,代价比较大,建议尽量少做
- 比如在初始化时把要调用的 lua function 获取一次(映射到delegate)后,保存下来,后续直接调用该delegate 即可。table 也类似。
-
如果 lua 实现的部分都以 delegate 和 interface 的方式提供,使用方可以完全和 xLua 解耦:由一个专门的模块负责 xlua 的初始化以及 delegate、interface 的映射,然后把这些 delegate 和i nterface 设置到要用到它们的地方。
获取一个全局基本数据类型
因为 lua 中 的 number 包括 int float double,所以利用 C# 访问时,需自行选择对应的类型进行获取
a = 100
b = 'Hello World'
c = true
int a = luaenv.Global.Get<int>("a");
float a2 = luaenv.Global.Get<float>("a");
string b = luaenv.Global.Get<string>("b");
bool c = luaenv.Global.Get<bool>("c");
访问一个全局的 table
映射到 class
这种方式下 xLua 会帮你 new 一个实例,并把对应的字段赋值过去。table 的属性可以多于或者少于 class 的属性。可以嵌套其它复杂类型。要注意的是,这个过程是值拷贝,如果 class 比较复杂代价会比较大。而且修改 class 的字段值不会同步到 table,反过来也不会。
这个功能可以通过把类型加到 GCOptimize 生成降低开销。
person = {
name='cy',
age=18,
666, -- 无对应键,不会映射
}
public class Person {
public string name;
public int age;
}
// 访问一个全局的 table,,映射到 class
Person p = luaenv.Global.Get<Person>("person");
print(p.name + " - " + p.age);
// 修改 class 的字段值不会同步到 table
p.age = 23;
luaenv.DoString("print(person.age)"); // LUA: 18
映射到 interface(推荐)
这种方式是引用方式的映射,即在 c# 中修改,也会同步到 lua 的 table 中。
需要在接口上添加 [CSharpCallLua]
标签
在表中定义方法时需显示指定 self 参数,引号方式则不需要
person = {
name='cy',
age=18,
Study=function(self,hour) -- 需要显示指定 self 参数
return '已学习'..hour..'小时'
end
}
-- 默认带一个 self 的参数,代表当前 table
function person:Eat(food)
return '吃饭:'..food
end
[CSharpCallLua]
public interface IPerson {
string name { get; set; }
int age { get; set; }
string Study(int hour);
string Eat(string food);
}
// 访问一个全局的 table,映射到 interface
IPerson p2 = luaenv.Global.Get<IPerson>("person"); // 映射到有对应字段的class,by value
print(p2.name + " - " + p2.age);
p2.name = "interface cy";
luaenv.DoString("print(person.name)"); // 引用方式,会同步到 table 中,输出 LUA: interface cy
print(p2.Study(2)); // p2.Study(p2,2); xLua 调用时会自动将当前对象填充为第一个参数,所以需要在 Lua 中显示的定义第一个参数 self
print(p2.Eat("Apple"));
映射到 Dictionary<>,List<>
更加轻量级的方式,有很多的限制,如果不想定义 class 或者 interface 的话,可以考虑用这个,前提 table 下 key 和 value 的类型都是一致的。
Dictionary 无法映射没有键的情况,方法映射也无法达到预期效果。
List 只能映射值。
person = {
name='cy',
age=18,
66,'哈哈',
Study=function(self,hour) -- 需要显示指定 self 参数
return '已学习'..hour..'小时'
end
}
-- 默认带一个 self 的参数,代表当前 table
function person:Eat(food)
return '吃饭:'..food
end
// 映射到 Dictionary<>
Dictionary<string, object> dict = luaenv.Global.Get<Dictionary<string, object>>("person");
foreach (string key in dict.Keys) {
print(key + "-" + dict[key]); // age-18 Eat-function:11 Study-function:13 name-cy
}
// 映射到 List<>
List<object> list = luaenv.Global.Get<List<object>>("person");
foreach (object o in list) {
print(o); // 66 哈哈
}
映射到 LuaTable 类
LuaTable 类,是 xLua 定义好的一个 C# 类,这种方式好处是不需要生成代码,但也有一些问题,比如慢,比映射到 interface 要慢一个数量级,而且没有类型检查。
person = {
name='cy',
age=18
}
LuaTable tab = luaenv.Global.Get<LuaTable>("person");
print(tab.Get<string>("name") + tab.Get<int>("age"));
访问一个全局的 function
映射到 delegate(推荐)
-- 全局函数
function fun()
print('Hello World')
end
function fun2(a,b)
print(a+b)
return a+b
end
function fun3(a,b)
return a+b,a+1,b+2
end
[CSharpCallLua]
public delegate int Add(int a, int b);
// 多返回值,利用 out 参数
[CSharpCallLua]
public delegate int Fun3(int a, int b,out int resA,out int resB);
// ---------------------------------------
// 利用 ACtion
Action act = luaenv.Global.Get<Action>("fun"); // 映射到一个 delgate,要求 delegate 加到生成列表,否则返回 null,建议用法
act(); // LUA:Hello World
// 带参数
Add add = luaenv.Global.Get<Add>("fun2");
print(add(3, 4)); // LUA:7 7
// 多返回值
Fun3 fun3 = luaenv.Global.Get<Fun3>("fun3");
int resA, resB;
int res = fun3(3, 4, out resA, out resB); // 利用 out 参数接收多返回值
print(res + "-" + resA + "-" + resB); // 7-4-6
映射到 LuaFunction
存在性能问题
function fun3(a,b)
return a+b,a+1,b+2
end
LuaFunction lfun = luaenv.Global.Get<LuaFunction>("fun3");
object[] os = lfun.Call(3, 4);
foreach (object o in os) {
print(o); // 7 4 6
}
完整 Demo
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using XLua;
using System;
namespace Tutorial {
public class CSCallLua : MonoBehaviour {
LuaEnv luaenv = null;
#region Lua Code
string script = @"
a = 100
b = 'Hello World'
c = true
person = {
name='cy',
age=18,
66,'哈哈',
Study=function(self,hour) -- 需要显示指定 self 参数
return '已学习'..hour..'小时'
end
}
-- 默认带一个 self 的参数,代表当前 table
function person:Eat(food)
return '吃饭:'..food
end
-- 全局函数
function fun()
print('Hello World')
end
function fun2(a,b)
print(a+b)
return a+b
end
function fun3(a,b)
return a+b,a+1,b+2
end
";
#endregion
public class Person {
public string name;
public int age;
}
[CSharpCallLua]
public interface IPerson {
string name { get; set; }
int age { get; set; }
string Study(int hour);
string Eat(string food);
}
[CSharpCallLua]
public delegate int Add(int a, int b);
// 多返回值,利用 out 参数
[CSharpCallLua]
public delegate int Fun3(int a, int b,out int resA,out int resB);
void Start() {
luaenv = new LuaEnv();
luaenv.DoString(script);
// 1. 获取 Lua 里面的全局变量
int a = luaenv.Global.Get<int>("a"); // 因为 lua 中 的 number 包括 int float double,所以利用 C# 访问时,需自行选择对应的类型进行获取
string b = luaenv.Global.Get<string>("b");
bool c = luaenv.Global.Get<bool>("c");
Debug.Log("a = " + a + ", b = " + b + ", c = " + c);
// 2. 访问一个全局的 table,映射到 class
Person p = luaenv.Global.Get<Person>("person");
print(p.name + " - " + p.age);
p.age = 23;
luaenv.DoString("print(person.age)");
// 3. 访问一个全局的 table,映射到 interface
IPerson p2 = luaenv.Global.Get<IPerson>("person"); // 映射到有对应字段的class,by value
print(p2.name + " - " + p2.age);
p2.name = "interface cy";
luaenv.DoString("print(person.name)"); // LUA: interface cy
print(p2.Study(2)); // p2.Study(p2,2); xLua 调用时会自动将当前对象填充为第一个参数,所以需要在 Lua 中显示的定义第一个参数 self
print(p2.Eat("Apple"));
// 4. 映射到 Dictionary<>
Dictionary<string, object> dict = luaenv.Global.Get<Dictionary<string, object>>("person");
foreach (string key in dict.Keys) {
print(key + "-" + dict[key]); // age-18 Eat-function:11 Study-function:13 name-cy
}
// 5. 映射到 List<>
List<object> list = luaenv.Global.Get<List<object>>("person");
foreach (object o in list) {
print(o); // 66 哈哈
}
// 6. 映射到 LuaTable
LuaTable tab = luaenv.Global.Get<LuaTable>("person");
print(tab.Get<string>("name") + tab.Get<int>("age"));
// 7. 访问全局函数
Action act = luaenv.Global.Get<Action>("fun"); // 映射到一个 delgate,要求 delegate 加到生成列表,否则返回 null,建议用法
act(); // LUA:Hello World
// 带参数
Add add = luaenv.Global.Get<Add>("fun2");
print(add(3, 4)); // LUA:7 7
// 多返回值
Fun3 fun3 = luaenv.Global.Get<Fun3>("fun3");
int resA, resB;
int res = fun3(3, 4, out resA, out resB); // 利用 out 参数接收多返回值
print(res + "-" + resA + "-" + resB); // 7-4-6
// 8. 映射到 LuaFunction
LuaFunction lfun = luaenv.Global.Get<LuaFunction>("fun3");
object[] os = lfun.Call(3, 4);
foreach (object o in os) {
print(o);
}
}
void Update() {
if (luaenv != null) {
luaenv.Tick();
}
}
void OnDestroy() {
luaenv.Dispose();
}
}
}
Lua 调用 C#
所有 C# 相关的都放到 CS 下,包括构造函数,静态成员属性、方法。
new C# 对象
-- new C# 对象,构造游戏物体
local newGameObj = CS.UnityEngine.GameObject()
local newGameObj2 = CS.UnityEngine.GameObject('NewByLua')
访问 C# 静态属性,方法
如果需要经常访问的类,可以先用局部变量引用后访问,除了减少敲代码的时间,还能提高性能:
local GameObject = CS.UnityEngine.GameObject
GameObject.Find(“Main Camera”)
local GameObject = CS.UnityEngine.GameObject -- 性能优化
print('UnityEngine.Time.deltaTime:', CS.UnityEngine.Time.deltaTime) -- 读静态属性
CS.UnityEngine.Time.timeScale = 0.5 -- 写静态属性
local camera = GameObject.Find("Main Camera") -- 静态方法调用
camera.name = "update by lua" -- 修改摄像机的名字
访问 C# 成员属性,方法
local cameraCom= camera:GetComponent("Camera") -- 调用成员方法的时候,使用冒号,否则需要传递自身为第一个参数
-- local cameraCom= camera.GetComponent(camera,"Camera") -- 传递自身
GameObject.Destroy(cameraCom)
参数的输入输出属性(out,ref)
Lua 调用 c# 的参数处理规则:C# 的普通参数算一个输入形参,ref 修饰的算一个输入形参,out 不算,然后从左往右对应 lua 调用 C# 的实参列表;
Lua 调用 C# 的返回值处理规则:C# 函数的返回值(如果有的话)算一个返回值,out 算一个返回值,ref 算一个返回值,然后从左往右对应 lua 的多返回值。
lua
xxx(t1,t2,t3)
return re1,t3,t4
end
c#
xxx(type t1,type t2,ref type t3,put type t4){
return re1;
}
泛化(模版)方法
不直接支持,可以通过 Extension methods 功能进行封装后调用。