xLua是Unity3D下Lua編程解決方案,自2016年初推廣以來,已經應用於十多款騰訊自研游戲,因其良好性能、易用性、擴展性而廣受好評。現在,騰訊已經將xLua開源到GitHub。
2016年12月末,xLua剛剛實現新的突破:全平台支持用Lua修復C#代碼bug。
目前Unity下的Lua熱更新方案大多都是要求要熱更新的部分一開始就要用Lua語言實現,不足之處在於:
- 接入成本高,有的項目已經用C#寫完了,這時要接入需要把需要熱更的地方用Lua重新實現;
- 即使一開始就接入了,也存在同時用兩種語言開發難度較大的問題;
- Lua性能不如C#;
xLua熱補丁技術支持在運行時把一個C#實現(函數,操作符,屬性,事件,或者整個類)替換成Lua實現,意味着你可以:
- 平時用C#開發;
- 運行也是C#,性能秒殺Lua;
- 有bug的地方下發個Lua腳本fix了,下次整體更新時可以把Lua的實現換回正確的C#實現,更新時甚至可以做到不重啟游戲; 這個新特性iOS,Android,Window,Mac都測試通過了,目前在做一些易用性優化。
xLua插件下載地址:https://github.com/Tencent/xLua
xLua的使用
創建工程並導入xLua插件
通過xLua插件運行lua程序
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using XLua;
public class MyHelloWorld : MonoBehaviour {
void Start () {
// 創建lua環境
LuaEnv luaenv = new LuaEnv();
// 運行Lua代碼
luaenv.DoString("print('Hello World')");
// 關閉Lua環境
luaenv.Dispose();
}
}
可以看到,輸出了打印,前綴有Lua的標識表示這是由Lua中的方法執行的
反過來,也可以使用lua調用C#中的程序
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using XLua;
public class MyHelloWorld : MonoBehaviour {
void Start () {
// 創建lua環境
LuaEnv luaenv = new LuaEnv();
// 運行Lua代碼
//luaenv.DoString("print('Hello World')");
luaenv.DoString("CS.UnityEngine.Debug.Log('Hello World')");
// 關閉Lua環境
luaenv.Dispose();
}
}
這個時候,打印前就沒有Lua標識符了,表示這是由C#中代碼執行的
上面是C#和Lua之間的簡單調用,但是在實際工作中,我們不可能這么寫。我們的做法是寫好Lua文件后,在C#中加載這個文件,然后使用其中的函數功能。
首先我們創建好一個Lua文件,然后在C#中加載后使用
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using XLua;
public class MyHello : MonoBehaviour {
void Start () {
TextAsset t = Resources.Load<TextAsset>("helloworld.lua");
LuaEnv luaenv = new LuaEnv();
luaenv.DoString(t.ToString());
luaenv.Dispose();
}
}
注意:在加載的時候,我們使用的是TextAsset文本格式,它默認識別的后綴為.txt,所以我們上面創建的lua文件后綴不是.lua,但是為了讓我們方便的看出它是一個lua文件,所以取名的時候使用.lua.txt。
除了上面的加載方法外,更常用的方法是使用require加載
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using XLua;
public class MyHello : MonoBehaviour {
void Start () {
LuaEnv luaenv = new LuaEnv();
luaenv.DoString("require 'helloworld'");
luaenv.Dispose();
}
}
require實際上是調一個個的loader去加載,有一個成功則不再往下嘗試,全部失敗則報文件找不到。目前Lua除了原生的loader外,還添加了從Resources加載的loader,需要注意的是Resources只支持有限的后綴,放在Resources下的lua文件需要加上.txt后綴。
自定義loader
我們發現上面的lua文件都是放在Resources文件夾下,因為原生的loader會在這個下面去加載。在我們的項目中,可能我們的lua文件放在自定義的文件夾下,這個時候就需要我們自定義loader,在xLua加自定義loader是很簡單的,只涉及到一個接口:
public delegate byte[] CustomLoader(ref string filepath);
public void LuaEnv.AddLoader(CustomLoader loader)
通過AddLoader可以注冊個回調,該回調參數是字符串,lua代碼里頭調用require時,參數將會透傳給回調,回調中就可以根據這個參數去加載指定文件,如果需要支持調試,需要把filepath修改為真實路徑傳出。該回調返回值是一個byte數組,如果為空表示該loader找不到,否則則為lua文件的內容。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using XLua;
using System.IO;
public class CreateNewLoader : MonoBehaviour {
void Start () {
LuaEnv luaenv = new LuaEnv();
// 自定義loader
luaenv.AddLoader(MyLoader);
luaenv.DoString("require 'newloaderText'");
luaenv.Dispose();
}
private byte[] MyLoader(ref string filePath)
{
string absPath = Application.streamingAssetsPath + "/" + filePath + ".lua.txt";
return System.Text.Encoding.UTF8.GetBytes(File.ReadAllText(absPath));
}
}
上面代碼中我們定義的lua文件為“newloaderText.lua”,該文件位於“StreamingAssets”文件夾下,該文件夾與Assets文件夾同級,所以在后面設置路徑的時候使用系統自帶的函數“Application.streamingAssetsPath”可以找到該文件夾。當然,我們也可以自定義文件夾的位置,后面的路徑改一下就行。
上面的執行過程,注冊回調后,調用require的時候,將“newloaderText”傳遞給回調函數"MyLoader",在此回調函數中我們加載到指定文件然后傳回來使用。
C#訪問Lua 獲取全局變量
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using XLua;
public class CSharpCallLua : MonoBehaviour {
void Start () {
LuaEnv luaenv = new LuaEnv();
luaenv.DoString("require 'CSharpCallLua'");
// 獲取lua中的全局變量
int num = luaenv.Global.Get<int>("num");
string name = luaenv.Global.Get<string>("name");
bool isPause = luaenv.Global.Get<bool>("isPause");
Debug.Log("num:" + num);
Debug.Log("name:" + name);
Debug.Log("isPause:" + isPause);
luaenv.Dispose();
}
}
使用函數LuaEnv.Global就能訪問,其中,luaenv.Global.Get<int>("num")中,<int>指的是要轉換成的類型,"num"是在lua中定義的變量名
C#訪問Lua 獲取全局table
- 映射到普通class或struct:定義一個class或者struct,有對應於table的字段的public屬性,而且有無參數構造函數即可,比如對於{f1 = 100, f2 = 100}可以定義一個包含public int f1;public int f2;的class。這種方式下xLua會幫你new一個實例,並把對應的字段賦值過去。table的屬性可以多於或者少於class的屬性。可以嵌套其它復雜類型。
注意:lua的table中的字段名和C#的class中的字段名要一一對應(名字也要相同),否則取不到值。此種方式為值拷貝,修改class的字段值不會同步到table,反過來也不會。使用此種方式,不能訪問lua的函數。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using XLua;
public class CSharpCallLua : MonoBehaviour {
void Start () {
LuaEnv luaenv = new LuaEnv();
luaenv.DoString("require 'CSharpCallLua'");
// 獲取lua中的全局table
Person p = luaenv.Global.Get<Person>("Person");
Debug.Log("name:" + p.name);
Debug.Log("age:" + p.age);
luaenv.Dispose();
}
class Person
{
public string name;
public int age;
}
}
- 映射到interface:這種方式依賴於生成代碼(如果沒生成代碼會拋InvalidCastException異常),代碼生成器會生成這個interface的實例,如果get一個屬性,生成代碼會get對應的table字段,如果set屬性也會設置對應的字段。甚至可以通過interface的方法訪問lua的函數。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using XLua;
public class CSharpCallLua : MonoBehaviour {
void Start () {
LuaEnv luaenv = new LuaEnv();
luaenv.DoString("require 'CSharpCallLua'");
// 獲取lua中的全局table(映射到interface)
Person_1 p1 = luaenv.Global.Get<Person_1>("Person");
Debug.Log("name:" + p1.name);
Debug.Log("age:" + p1.age);
p1.eat("apple");
luaenv.Dispose();
}
[CSharpCallLua]
interface Person_1
{
string name { get;set;}
int age { get; set; }
void eat(string str);
}
}
注意:在lua中定義函數的時候,第一個參數是arg,需要寫上,名字隨意取都行,這里寫的self。在C#中定義接口的時候,要加上標簽[CSharpCallLua]
- 映射到Dictionary<>,List<>
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using XLua;
public class CSharpCallLua : MonoBehaviour {
void Start () {
LuaEnv luaenv = new LuaEnv();
luaenv.DoString("require 'CSharpCallLua'");
// 獲取lua中的全局table(通過Dictionary)
Dictionary<string, object> dict = luaenv.Global.Get<Dictionary<string, object>>("Person");
foreach(string key in dict.Keys)
{
print("key:" + key + " value:" + dict[key]);
}
luaenv.Dispose();
}
}
注意:映射到Dictionary<>的時候,只映射了Lua中鍵值對的形式,普通的值沒有映射過來
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using XLua;
public class CSharpCallLua : MonoBehaviour {
void Start () {
LuaEnv luaenv = new LuaEnv();
luaenv.DoString("require 'CSharpCallLua'");
// 獲取lua中的全局table(通過List)
List<object> list = luaenv.Global.Get<List<object>>("Person");
foreach(object o in list)
{
print(o);
}
luaenv.Dispose();
}
}
注意:映射到List<>的時候,只映射了Lua中值的形式,鍵值對的形式沒有映射過來
映射到LuaTable類:這種方式不常用,也不建議使用
C#訪問Lua 獲取全局函數
- 映射到delegate:這種是建議的方式,性能好很多,而且類型安全。缺點是要生成代碼(如果沒生成代碼會拋InvalidCastException異常)。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using XLua;
public class CSharpCallLua : MonoBehaviour {
void Start () {
LuaEnv luaenv = new LuaEnv();
luaenv.DoString("require 'CSharpCallLua'");
// 訪問lua中的全局函數(映射到delegate)
Add add = luaenv.Global.Get<Add>("add");
int res1 = 0; int res2 = 0;
int res = add(3, 4, out res1, out res2);
print("res:" + res);
print("res1:" + res1);
print("res2:" + res2);
add = null;
luaenv.Dispose();
}
[CSharpCallLua]
delegate int Add(int a, int b, out int res1, out int res2);
}
注意:使用delegate需要添加特性[CSharpCallLua],如果lua中函數返回多值,在C#中只能接收一個值,其它值從左往右映射到c#的輸出參數,輸出參數包括返回值,out參數,ref參數。
- 映射到LuaFunction:這個性能不好,不建議使用
Lua訪問C#
在C#這樣new一個對象:
var newGameObj = new UnityEngine.GameObject();
對應到Lua是這樣:
local newGameObj = CS.UnityEngine.GameObject()
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using XLua;
public class LuaCallCSharp : MonoBehaviour {
void Start () {
LuaEnv luaenv = new LuaEnv();
luaenv.DoString("require 'LuaCallCS'");
luaenv.Dispose();
}
}
Lua訪問C#靜態屬性和方法
如果需要經常訪問的類,可以先用局部變量引用后訪問,除了減少敲代碼的時間,還能提高性能
Lua訪問C#成員屬性和方法
讀成員屬性
testobj.DMF
寫成員屬性
testobj.DMF = 1024