Unity熱更新 xLua


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

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

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

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

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

  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程序

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

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM