Unity熱更新 xLua

xLua是Unity3D下Lua編程解決方案,自2016年初推廣以來,已經應用於十多款騰訊自研遊戲,因其良好性能、易用性、擴展性而廣受好評。如今,騰訊已經將xLua開源到GitHub。git

2016年12月末,xLua剛剛實現新的突破:全平臺支持用Lua修復C#代碼bug。github

目前Unity下的Lua熱更新方案大多都是要求要熱更新的部分一開始就要用Lua語言實現,不足之處在於:編程

  1. 接入成本高,有的項目已經用C#寫完了,這時要接入須要把須要熱更的地方用Lua從新實現;
  2. 即便一開始就接入了,也存在同時用兩種語言開發難度較大的問題;
  3. Lua性能不如C#;

xLua熱補丁技術支持在運行時把一個C#實現(函數,操做符,屬性,事件,或者整個類)替換成Lua實現,意味着你能夠:c#

  1. 平時用C#開發;
  2. 運行也是C#,性能秒殺Lua;
  3. 有bug的地方下發個Lua腳本fix了,下次總體更新時能夠把Lua的實現換回正確的C#實現,更新時甚至能夠作到不重啓遊戲; 這個新特性iOS,Android,Window,Mac都測試經過了,目前在作一些易用性優化。

xLua插件下載地址:https://github.com/Tencent/xLua數組

 

xLua的使用

建立工程並導入xLua插件安全

 

經過xLua插件運行lua程序app

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

相關文章
相關標籤/搜索