lua課程學習筆記

GavinZ233發表於2024-07-09

Learning-Lua

lua課程學習筆記

整體結構

節點 內容 難點
AB包 AB包瀏覽器下載,AB打包,AB載入,ABMgr
Lua語法 lua邏輯,表,方法,物件導向模擬,協程 物件導向模擬需要熟悉表、元表、全域性表
xLua lua與C#互相呼叫,lua使用C#資料結構,lua無法直接呼叫的類需要標記特性
Hotfix 標記補丁指令碼,使用lua對C#替換
xLua的揹包系統 拼UI,使用Lua載入資源,lua指令碼監聽並控制UI 透過物件導向的思維,用lua模擬C#指令碼的操作

1. AB包

1.1 瞭解AB包

Resources Ab包
全部打包,只讀 儲存位置、壓縮方式自定義,可動態更新

熱更流程:客戶端向服務端對比版本號獲取資源伺服器地址,客戶端向資源伺服器比對資源,檢測需要更新的內容,下載對應AB包

匯入AssetBundles-Browser:開啟UnityPackageMgr,點+號選擇URL匯入: https://github.com/Unity-Technologies/AssetBundles-Browser.git

1.2 生成AB包

1.2.1 建立ab包

  1. 資原始檔右下角的AssetBundle皮膚有兩個下拉框,第一個選擇資源要打包的ab包,第二個是在ab包內的分組

  2. 檢視ab包可以到Window-AssetBundles-Browser,點開的彈窗裡看,每次新修改ab包需要重新整理一下

1.2.2 ab包打包設定

Build引數 含義
Build Target 打包的目標平臺
Ouput Path ab包輸出路徑
Clear Folders 打包前清空路徑,可以清除一些不需要的包
Copy to StreamingAssets 輸出的ab包複製到StreamingAssets一份
Compression 壓縮方式:1.不壓縮 2.LZMA壓縮最小,但解壓時全部解壓 3. LZ4壓縮包體比LAZMA略大,但可以單獨解壓
Exclude Type Information 在資源包中不包含資源的型別資訊
Force Rebuild 重新打包時重新構建所有包,不會刪除多餘的包
Ignore Type Tree Changes 增量構建檢查時,忽略型別樹的更改
Append Hash 將檔案的雜湊值附加到資源包名上
Strict Mode 嚴格模式,如果打包報錯則打包失敗
Dry Run Build 執行時構建

1.2.3 ab包檔案

檔名 作用
包名.分組名 ab包本體
manifest ab包的關聯資訊,資源資訊,版本資訊等
和匯出資料夾同名 ab包的主包,記錄依賴關係

1.2.4 UnityLearn對於AB包的介紹

(暫時空著,回頭看看文章對於資源引用的介紹)
https://learn.unity.com/tutorial/assets-resources-and-assetbundles#5c7f8528edbc2a002053b5a6

1.3 使用AB包資源

1.3.1 同步載入AB包

  1. 型別載入

     //載入AB包,ab包不能重複載入
     AssetBundle bundle = AssetBundle.LoadFromFile(Application.streamingAssetsPath + "/" + "model");
     //載入ab包中的資源
     GameObject cube = bundle.LoadAsset("Cube", typeof(GameObject)) as GameObject;
     //例項化
     Instantiate(cube);
    
  2. 泛型載入

     //泛型載入
     GameObject cap = bundle.LoadAsset<GameObject>("Capsule");
     //例項化
     Instantiate(cap, Vector3.one, Quaternion.identity);
    

1.3.2 非同步載入AB包

    IEnumerator LoadABRes(string abName, string resName)
    {
      AssetBundleCreateRequest abcr = AssetBundle.LoadFromFileAsync(Application.streamingAssetsPath + "/" + abName);
      yield return abcr;
      AssetBundleRequest abq = abcr.assetBundle.LoadAssetAsync(resName, typeof(Sprite));
      yield return abq;

      img.sprite = abq.asset as Sprite;
      img.rectTransform.localScale = Vector3.one * 5;
    }

注意 : 此處的 AssetBundleCreateRequestAssetBundleRequest 類都是繼承自 AsyncOperation,屬於非同步操作協同程式。
AssetBundleCreateRequest是建立請求,建立成功後,返回AssetBundleRequest載入請求,可以從AssetBundleRequest中得到載入成功的AB包

1.3.3 解除安裝AB包

        //解除安裝所有AB包,入參為是否解除安裝已經載入的AB包資源
        AssetBundle.UnloadAllAssetBundles(false);

        //單個ab包解除安裝,入參為是否解除安裝已經載入的AB包資源
        bundle.Unload(true);

實用場景中,大部分情況不會入參true

1.3.4 依賴包

如果某模型的材質與模型分別打包,只載入模型的AB包,例項化出來的模型是無材質的,需要將材質所在的AB包也一併載入。

包與包之間的依賴被記錄在主包中,但不會記錄資源對包的依賴,只會記錄包內部所有資源對外部哪些包有依賴。
舉個例子:model包中有模型(坦克和炮彈),tanktt包中有坦克車身的貼圖,tracktt包中有履帶貼圖,shelltt包中有炮彈的貼圖。此時從model包的依賴包有tanktt、tarcktt、shelltt包,哪怕只是想載入model包中的炮彈,tanktt和tarcktt包也會因為依賴關係被一起載入。

獲取依賴資訊:

    //載入主包
    AssetBundle abMain=AssetBundle.LoadFromFile(Application.streamingAssetsPath+"/" + "StandaloneWindows");
    //從主包載入依賴資訊檔案
    AssetBundleManifest abManifest = abMain.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
    //從依賴檔案中獲取model的依賴包資訊
    string[] strs=abManifest.GetAllDependencies("model");
    foreach (string str in strs)
    {
        print("model依賴包:"+str);
    }

1.4 AB包資源載入Mgr

ABMgr類

名稱 作用 操作
+ ABMgr Instance 單例 訪問本類時,如果為空例項化gameobject並附加本類,返回instance
- AssetBundle mainAB 記錄主包
- AssetBundleManifest mainifest 包依賴資訊
- Dictionary<string,AssetBundle> abDic 記錄載入的AB包
- string PathUrl ab包路徑地址
- string MainABName 主包名稱
+ object LoadRes(string abName,string resName) 載入資源 LoadAB(abName),載入對應名稱資源
+ object LoadRes(string abName, string resName,System.Type type) 載入資源 LoadAB(abName),載入對應名稱對應type的資源
+ T LoadRes< T >(string abName, string resName)where T : Object 載入資源 (同上)泛型過載
- LoadAB(string abName) 載入ab包 非同步載入ab包
+ LoadResAsync(string abName, string resName, UnityAction< Object > callback) 對外公開非同步載入方法 啟動ReallyLoadResAsync(abName,resName,callback)
- IEnumerator ReallyLoadResAsync(string abName, string resName, UnityAction< Object > callback) 非同步載入資源 LoadABAsync(abName),載入resName資源
+ LoadResAsync(string abName, string resName,System.Type type, UnityAction< Object > callback) 對外公開非同步載入方法 啟動ReallyLoadResAsync(abName,resName,type,callback)
- IEnumerator ReallyLoadResAsync(string abName, string resName, System.Type type, UnityAction 非同步載入type型別資源 LoadABAsync(abName),載入resName資源
+ LoadResAsync< T >(string abName, string resName, UnityAction< T > callback)where T : Object 對外公開非同步載入方法 啟動ReallyLoadResAsync< T >(abName,resName,callback)
- IEnumerator ReallyLoadResAsync< T >(string abName, string resName, UnityAction< T > callback) where T : Object 非同步載入T類資源 LoadABAsync(abName),載入resName資源
- IEnumerator LoadABAsync(string abName) 非同步載入ab包 檢查主包,依賴包,目標包是否已經載入,沒有載入時進行非同步ab包載入,並記錄到abDic
+ UnLoad(string abName) 解除安裝ab包 解除安裝目標包並從abDic中移出
+ ClearAB() 解除安裝所有ab包 解除安裝所有ab包並清空abDic和mainAB與manifest資訊

此類主要對外提供:

  1. 同步載入資源方法(三種過載)
  2. 非同步載入資源方法(三種過載)
  3. 解除安裝AB包方法

ab包的載入中,如果沒有指定型別,會載入第一個對應名稱的資源,這就需要給定型別才能避免偏差,而lua是不支援泛型的,在lua熱更中就需要在提供resName後面再提供type,保證準確性

2. Lua語法

Lua 5.3 參考手冊

2.1 lua環境

luaforwindows

2.2 註釋

--單行註釋
--[[多行
註釋]]

2.3 資料型別

簡單資料型別:nil,number,string,boolean
複雜資料型別:function,table,userdata,thread

資料型別 描述
nil 表示一個無效值,空
boolean false或者true
number 表示雙精度型別的實浮點數
string 字串,雙引號和單引號都是string
funciton 編寫的函式
userdata 表示任意儲存在變數中的C資料結構
thread 表示執行的獨立線路,協同程式
table Lua 中的表(table)其實是一個"關聯陣列"(associative arrays),陣列的索引可以是數字、字串或表型別。在 Lua 裡,table 的建立是透過"構造表示式"來完成,最簡單構造表示式是{},用來建立一個空表。

lua中使用未宣告的變數,預設為nil
print(a)
pring(type(a))
列印:nil nil

2.4 字串操作

  1. 字串長度

    print(#str)
    
  2. 字串多行列印

     print("使用轉義字\n符換行")
     s=[[在字串
     裡
     換行]]
     print(s)
    
  3. 字元拼接
    print("字串".."拼接")

     s1="拼接數字" s2=321
     print(s1..s2)
    
     print(string.format("使用format拼接數字%d。",213))
     print(string.format("使用format拼接數字%s。","asdaf"))
    
     --%d:數字拼接
     --%s:字元配對
    
  4. 其他型別轉字串

     a = true
     print(tostring(a))
     --print會自動把目標變數tostring
    
  5. 字串的公共方法

     string.upper(str)
     string.lower(str)
     string.reverse(str)
     string.find(str,"as")
     --lua的索引從1開始
     --find返回的索引結果是兩個,第一個是起點,第二個是結尾
     string.sub(str,2,4)
     --sub,傳一個引數是從該起點到結尾擷取,傳兩個引數就是擷取的起點和終點
     string.rep(str,2)
     --重複字串指定次數
     string.gsub(str,"SA","*……*")
     --替換字串,返回替換次數
    
     --字元轉ASCII碼
     a=string.byte("lua",1)--字串指定位置(轉碼
    

2.5 運算子

  1. 算術運算子

+-*/ 取餘% 冪運算^
沒有自增自減
沒有複合運算 += -= /=等
print("321"+2) --字串可以算數操作,自動轉成number

  1. 條件運算子

> < <= >= == ~=(不等於)

  1. 邏輯運算子
邏輯 C# lua
&& and
|| or
! not

2.6 條件分支

    if a>5 then
            print(a.."大於"..5)
    elseif a==3 then
            print("a等於"..3)
    elseif a==2 then
            print("a等於"..2)
    elseif a==1 then
            print("a等於"..1)
    else print("idontknow")
    end

Lua不持支switch

2.7 迴圈

  1. while

     num=0
     while num<5 do
             print(num)
             num=num+1
     end
    
  2. do while

     num=0
     repeat
             print(num)
             num=num+1
     until num>5  --直到滿足,退出迴圈
    
  3. for

     for i=1,10 do -- 預設+1
             print(i)
     end
    
     for i=1,10,2 do  --設定+2
             print(i)
     end
    

2.8 函式

lua的函式不支援過載,新申明的函式會覆蓋老的

  1. 函式結構

     function  FunName()
             -- body
     end
    
     FunName = function()
             -- body
     end
    
  2. 反參

     function F4(a)
             return a
     end
     temp=F4("213")
     print(temp)
    
  3. 變長引數

     function F7(...)
             arg={...}
             for i=1 ,#arg do
                     print(arg[i])
             end
     end
     F7(3,1,4,76,"weq32")
    
  4. 函式巢狀

     function F8()
             F9 =function() --內部宣告一個函式
                     print(123);
             end
             return F9  --返回函式
     end
     f9=F8()
     f9()
    
     function F9(x)
             return function(y) --在return後直接申明函式
                     return x+y 
             end
     end
     print(F9(10)(1)) --呼叫外部函式後面加一個括號表示呼叫返回的函式
    

2.9 Table

表是一切複雜資料的基礎:陣列,二維陣列,字典,類等

1. 陣列

索引從1開始
長度獲取:#Nums
長度計算時,自動捨去末尾的nil
當自定義索引為數字時,長度忽略小於0的,且索引間隔大於1時長度預設為斷開處的索引值

自定義索引被預設索引覆蓋:

t={[1]=1,[2]=2,[3]=3,4}
t={4,[1]=2,[3]=3,4}
列印 t[1] 都是4

2. 遍歷方法

    --ipairs 類似#獲取到長度根據長度遍歷
    for i,v in ipairs(t) do
            print(i,v)
    end
    --pairs 將table所有鍵都找到
    for k,v in pairs(t) do
            print(k,v)
    end

    for k,v in pairs(c) do
            --可以傳多個引數,一樣可以列印
            print(k,v,3,4)
    end
    --宣告_下劃線代替key的引數獲取,只列印value
    for _,v in pairs(c) do
            print(v)
    end

表的增刪改

    c={["name"]="吳彥祖",["age"]=26}

    print(c.name)
    c["name"]="曾志偉"--直接賦值就是修改
    print(c.name)

    c["money"]=3--直接宣告就是新增
    print(c.money)

    c["money"]=nil--置空就相當於刪除
    print(c.money)

3. 表模擬類

在表內宣告成員屬性和方法

    Student ={
            age=1,
            sex=true,
            Up =function()
                    print("up了")
            end	
    }

但在外部也能新增

    Student.name="吳彥祖"
    --第一種宣告方式
    function Student.Speak()
            print("speak"..Student.age)
    end
    --第二種宣告方式
    Student.Speak=function()
             print("speak"..Student.age)
    end

特殊的,方法呼叫方式有兩種
第一種預設方法

    Student.Speak()

直接點呼叫,如果需要傳參,就在()內寫入參

此方法呼叫:宣告的方法時,第一個入參會作為self傳入

第二種把呼叫者作為隱藏入參

    Student:Speak()

對應的方法內部會使用self關鍵字代表隱藏入參

    function Student:Learn()
            print("Learn"..self.name)
    end

4. 表的公共方法

  1. 插入
    table.insert(被插表,插入索引,插入元素)
    無插入索引時,預設插入到 len+1處

     table.insert(t1,t2);
     table.insert(t1,1,t2)
    
  2. 移除
    table.remove(要操作的表,移除索引)
    無索引時,預設移除最後一位

     table.remove(t1,1)
     table.remove(t1)
    
  3. 排序
    table.sort(排序的表,排序函式)
    無排序函式時,預設升序

     table.sort(t3)
     table.sort(t3, function(a,b)
             if a>b then
                     return true
             end
     end)
    
  4. 拼接
    table.concat(操作表,中間字元,拼接起點,拼接終點)
    無拼接起點終點時,預設從頭到尾
    只能拼接字串與數字

     str=table.concat(tb, " ",2,3)
     str=table.concat(t2,"")
    

2.10 指令碼相關

1.指令碼執行

  1. 指令碼執行
    會按順序執行指令碼邏輯,如果最後有return會返回反參

     require('指令碼名稱')
     returnValue=require('指令碼名稱')
     --得到指令碼反參
    

指令碼只會執行一次,再次執行需要先解除安裝再執行

  1. 檢視指令碼執行
    返回boolean表示是否執行

     print(package.loaded['指令碼名稱'])
    
  2. 指令碼解除安裝
    將該loaded置空

     package.loaded["指令碼名稱"]=nil
    

2. 本地變數

在方法或指令碼內的變數前加 local 該變數變為本地變數
如果沒有 local 則屬於全域性變數記錄在_G表中,在方法體或指令碼外也可以訪問

_G表,儲存了lua所用的所有全域性函式和全域性變數

2.11 特殊用法

  1. 多變數賦值
    多變數賦值秉承了一貫作風,多給的引數捨去,少給的引數nil補上

     a,b,c=1,false,"ghj"
    
  2. 多返回值
    同上

     function Test()
             return 1,2,3,"ghj"
     end
     a,b,c,d,e=Test()
    
  3. and or

     print(1 and false)
     --短路原理,先判斷and前是否為真,1為真,返回and後的false
    
     print(1 or 2)
     --短路原理,先判斷1是否為真,1為真,返回1不需要執行or後的2
     print(false or nil)
     --false為假,返回nil
    

lua中,只有nilfalse才是假

  1. 三目運算
    C#中的三目運算? :是封裝好的語法糖,lua中沒有實現可以自己透過and or手動實現

     x=3
     y=1
     
     res= (x>y) and x or y 
     --x大於y,為true
     --=> true and x or y
     --true and x 返回x,
     --=> x or y
     --x 為真,返回x
     --達到了()內為真,返回x的效果
    
     res= (x<y) and x or y 
     --x不小於y,返回false,
     --=> false and x or y
     --flase and x 返回false
     --=> false or y
     --false跳過,返回y
     --達到了()內為假,返回y的效果
    

2. 12 協程

就像C#中的協程,是協同程式,不是執行緒,分段執行不產生資源搶佔

方法 使用場景
coroutine.create(方法) 建立協程,返回一個thread 建立一個需要監測狀態的協程
coroutine.wrap(方法) 建立協程,返回一個function 快速建立一個不需要監測狀態的協程,像funciton一樣使用
coroutine.resume(thread,傳參) 執行thread協程 啟動thread協程(多次執行都按照第一次的傳參執行,後續的引數無法使用,有待深入瞭解原理)
function() 執行協程方法 啟動協程方法
coroutine.yield(可傳參) 在協程內宣告掛起當前協程,就像Unity中協程的yieldreturn,不過不會自動執行下一步 掛起協程返回當前資料
coroutine.status(thread) 獲取協程的狀態(dead,suspended,running) 監控協程的狀態,決定是否關閉或開啟
coroutine.running() 獲取當前執行的協程號

2.13 元表

任何表變數都可以作為另一個表變數的元表
任何表變數都可以有自己的元表
當子表進行特定 操作時,會執行元表的內容

元表與元方法(lua參考手冊)

1. 設定元表

設定元表

    meta={}
    myTable={}
    --設定元表
    --(子表,元表)
    setmetatable(myTable,meta)

獲取元表

    getmetatable(表名)

2. 運算子過載

在元表宣告對應的funciton,示例程式碼:

            --運算子+
            __add =function(t1,t2)
                    return t1.age+t2.age
            end,

部分對應運算子表格

名稱 運算子 方法名 備註
加 (add) + __add
減 (subtract) - __sub
乘 (multiply) * __mul
除 (divide) / __div
取餘 (modulo) % __mod
冪 (power) ^ __pow
等於 (equal) == __eq
小於 (less than) < __lt 過載了<方法,lua遇到>會自動調換入參位置,所有不需要>的方法
小於等於 (less equal) <= __le 同上
連線 (connect) .. __concat

所有條件運算子過載都需要兩個入參的元表一致

3. __tostring 方法

    meta2={
            __tostring = function(t)
                    return t.name
            end
    }
    myTable2={
            name="吳彥祖"
    }
    --設定元表
    setmetatable(myTable2,meta2)

    print(myTable2)

3. __call 方法

    meta3={
            __tostring = function(t)
                    return t.name
            end,
            __call = function(a,b)
                    print("表name:"..a.name.."   傳入引數:"..b) 
            end

    }
    myTable3 = {
            name="表格名稱table3"
    }
    --設定元表
    --(子表,元表)
    setmetatable(myTable3,meta3)
    print(myTable3)
    myTable3(3)

call方法就是給予了該表一個可以呼叫自己執行的方法,call 的第一個引數是呼叫者,第二個是外部入參

4. __index __newIndex

"index": 索引 table[key]。 當 table 不是表或是表 table 中不存在 key 這個鍵時,這個事件被觸發。 此時,會讀出 table 相應的元方法。
儘管名字取成這樣, 這個事件的元方法其實可以是一個函式也可以是一張表。 如果它是一個函式,則以 table 和 key 作為引數呼叫它。 如果它是一張表,最終的結果就是以 key 取索引這張表的結果。 (這個索引過程是走常規的流程,而不是直接索引, 所以這次索引有可能引發另一次元方法。)

"newindex": 索引賦值 table[key] = value 。 和索引事件類似,它發生在 table 不是表或是表 table 中不存在 key 這個鍵的時候。 此時,會讀出 table 相應的元方法。
同索引過程那樣, 這個事件的元方法即可以是函式,也可以是一張表。 如果是一個函式, 則以 table、 key、以及 value 為引數傳入。 如果是一張表, Lua 對這張表做索引賦值操作。 (這個索引過程是走常規的流程,而不是直接索引賦值, 所以這次索引賦值有可能引發另一次元方法。)

一旦有了 "newindex" 元方法, Lua 就不再做最初的賦值操作。 (如果有必要,在元方法內部可以呼叫 rawset 來做賦值。)

以上是lua參考手冊內容,等學完課程重新梳理此處概念。

自己簡言之:
當子表找不到某屬性時,會到元表的__index指定的表找索引,利用index可以向上巢狀,類似於多重繼承的效果。
當賦值時,預設屬於index,宣告瞭newindex時,預設屬於newindex,再訪問這些就需要註明newindex,否則在index找不到newindex的變數。

5. rawget rawset

rawget會忽略index,在自身尋找變數

    print(rawget(myTable6,"age"))

rawset會忽略newindex,寫入到自己

    rawset(myTable7,"age",2)

2.14 模擬物件導向

  1. 宣告一個Object表作為基類

     Object={}
    
  2. Object的例項化方法
    建立一個新表作為例項化類的子表,並返回

     function Object:new()
             local obj={}
             --給空物件設定元表,和__index
             self.__index=self
             setmetatable(obj,self)
             return obj
     end
    
  3. Object的繼承方法
    在G表建立記錄子類,並設定子類的元表為父類

     function Object:subClass(className)
             --根據名字生成一個表,記錄在全域性_G表
             _G[className] ={}
             local obj =_G[className]
             --給子表記錄父表,命名為base
             obj.base=self
    
             --給子類設定元表,和__index
             setmetatable(obj,self)
             self.__index=self
    
     end
    
  4. GameObject繼承Object

     Object:subClass("GameObject")
    
  5. GameObjcet例項化

     local obj=GameObject:new()
    
  6. 子類重寫方法

     --GameObjcet成員方法
     function GameObject:Move()
             self.posX=self.posX+1
             self.posY=self.posY+1
     end
     --Player繼承Gameobjcet
     GameObject:subClass("Player")
     --Player重寫Move
     function Player:Move()
             --呼叫父方法,傳入自身
             self.base.Move(self)
             --再寫新的PlayerMove邏輯
     end
    

上面呼叫父Move時,用 self.base.Move(self) 而不是 self.base:Move()
使用.呼叫方法時,無預設傳參,使用:呼叫方法時,會將呼叫者傳入作為預設引數,對應self
此處如果使用:呼叫方法,傳入的只會是父類GameObjcet,那麼每次不同的Player都在共同操作GameObjcet

2.15 函式庫

1. 時間

    --系統時間(單位s)
    print(os.time())
    --傳入日期得到s
    print(os.time({year=2014,month=2,day=3}))
    local nowTime=os.date("*t")
    print(nowTime.min)

2. 數學

    --絕對值
    print(math.abs(-11))
    --弧度轉角度
    print(math.deg(math.pi))
    --三角函式,傳弧度
    print(math.cos(math.pi))
    --向下取整
    print(math.floor(2.6))
    --向上取整
    print(math.ceil(5.1))
  
    print(math.max(1,2))

    print(math.min(4,5))
    --分離小數,返回整數部分與小數部分
    print(math.modf(1.2))
    --冪運算
    print(math.pow(2,5))

    --隨機數,先設定種子
    math.randomseed(os.time())
    --第一個是根據種子本身生成,種子改變了才會改變
    print(math.random(300))
    --第二個是根據種子資訊生成,種子資訊改變就會改變
    print(math.random(300))
    --開方
    print(math.sqrt(9))

3. 路徑

    print(package.path)

並不常用,單獨用來呼叫lua時使用

4. 垃圾回收

    --獲取當前lua佔用記憶體數 k位元組 用返回值*1024 得到記憶體佔用位元組數
    print(collectgarbage("count"))
    --垃圾回收
    collectgarbage("collect")

貼一個剖析XluaGC的部落格,抽空再看
Unity下XLua方案的各值型別GC最佳化深度剖析

3. xLua

3.1 準備階段

  1. xLua框架
    只取用專案中的 Assets/Plugins Assets/Xlua 資料夾
    xLua專案地址

  2. AB包工具
    見專題一1.1部分

  3. 單例基類(非必須)
    泛型單例基類,簡化單例類的重複宣告。
    參考
    SimpleFrameWork
    專題一《單例模式基類》

  4. AB包管理器(非必須)
    封裝AB包同步非同步載入操作,可根據專案重寫
    見專題一1.4部分

3.2 C#呼叫Lua

1. Lua解析器

  1. 引用名稱空間

     using XLua;
    
  2. 建立Lua解析器

     LuaEnv luaEnv = new LuaEnv();
    
  3. 執行Lua語言

     //DoString(執行內容,報錯資訊)
     luaEnv.DoString("print('嗨,你好')", "報錯內容:"+this.name);
    
  4. 執行Lua指令碼
    require預設尋找指令碼的路徑是在Resources下
    大致是透過Resources.Load載入txt等,而無法讀取.lua
    因此Lua指令碼字尾要加txt
    檔名:Main.lua.txt

     luaEnv.DoString("require('Main')");
    
  5. 垃圾回收
    清理沒有手動釋放的物件

     luaEnv.Tick();
    
  6. 關閉Lua解析器
    不常用,解析器一般貫穿專案始終,不會關閉

     luaEnv.Dispose();
    

2. 檔案載入重定向

  1. AddLoader
    其中自定義方法被記錄到委託List,如果自定義方法返回空,會一直執行到預設方法

     //xlua提供的路徑重定向的方法
     //允許自定義載入Lua檔案
     //當我們執行Lua語言require時,相當於執行一個lua指令碼
     //他會執行我們自定義傳入的函式
     env.AddLoader(MyCustomLoader);
    
  2. 自定義載入lua指令碼方法

     private byte[] MyCustomLoader(ref string filePath)
     {
             //自定義路徑,訪問lua檔案
             string path=Application.dataPath+"/Lua/"+filePath+".lua";
             Debug.Log(path);
             if (File.Exists(path))
             {
             //讀取位元流返回
             return  File.ReadAllBytes(path);
             }
             else
             {
             Debug.Log("MyCustomLoader重定向路徑失敗,檔名:" + filePath);
             };
             return null;
     }
    

3. LuaMgr

名稱 作用 操作
+ LuaTable Global 提供lua的_G 返回luaEnv.Gloabal
- LuaEnv luaEnv 記錄lua解析器
+ Init() 初始化方法 初始化lua解析器,為解析器加入lua指令碼載入方法
+ DoLuaFile(string fileName) 透過指令碼名稱執行lua指令碼,避免了每次都require 拼接require與fileName,執行DoString()方法
+ DoString(string str) 執行lua語言
+ Tick() 執行lua垃圾回收
+ Dispose() 關閉解析器 dispose解析器,並將本地解析器記錄置空
- byte[] CustomLoader(ref string filePath) 自定義lua檔案讀取,從Asset下讀取 拼接Asset下的lua指令碼路徑,讀取位元流並返回
- byte[] CustomABLoader(ref string filePath) 定義lua檔案讀取,從AB包中讀取 讀取lua檔案的AB包,從包中獲取TextAsset,返回其中bytes

AB包中的Lua指令碼也需要將字尾名改為txt,鑑於修改字尾與打AB包過程繁瑣,日常開發都讀取Asset路徑下的Lua指令碼,測試階段再統一改字尾打包。
修改字尾後,第一個CustomLoader讀取不到字尾為".lua"的檔案,返空,解析器會繼續執行CustomABLoader找AB包中的".lua.txt"檔案

4. 全域性變數的獲取

    //初始化解析器
    LuaMgr.GetInstance().Init();
    //執行主指令碼
    LuaMgr.GetInstance().DoLuaFile("LuaMain");
    //獲取number儲存為int
    int i = LuaMgr.GetInstance().Global.Get<int>("testNumber");
    //獲取number儲存為double
    double d = LuaMgr.GetInstance().Global.Get<double>("testNumber");
    //獲取string
    string s = LuaMgr.GetInstance().Global.Get<string>("testString");
    //修改資料
    LuaMgr.GetInstance().Global.Set("testNumber", 66);
    LuaMgr.GetInstance().Global.Set("testString", "修改了string");

其中資料獲取為值複製,string也是

5. 全域性函式的獲取

1. 無參無返回方法

委託部分:

    public delegate void CustomCall();

執行部分:

    CustomCall call = LuaMgr.GetInstance().Global.Get<CustomCall>("testNoINAndOUT");
    call();
    //使用UnityAction
    UnityAction callUA = LuaMgr.GetInstance().Global.Get<UnityAction>("testNoINAndOUT");
    callUA();
    //使用Action
    Action callA = LuaMgr.GetInstance().Global.Get<Action>("testNoINAndOUT");
    callA();
    //Xlua提供的獲取方式
    LuaFunction lf = LuaMgr.GetInstance().Global.Get<LuaFunction>("testNoINAndOUT");
    lf.Call();

其中有四種呼叫方式,使用Action比較方便

2. 有參有返回方法

委託部分:

    [CSharpCallLua]
    public delegate int CustomCall2(int i);

執行部分:

    CustomCall2 call2 = LuaMgr.GetInstance().Global.Get<CustomCall2>("testHasINAndOUT");
     int returnNum = call2(3);
    print(returnNum);


    Func<int,int> sFunc=LuaMgr.GetInstance().Global.Get<Func<int,int>>("testHasINAndOUT");
    int returnsFunc = call2(88);
    print(returnsFunc);

自定義委託時,需要新增特性[CSharpCallLua],Xlua生成程式碼向xlua直譯器註冊委託才能使用,Func是系統提供的委託XLua已經處理過

3. 多返回值方法

委託部分:

    [CSharpCallLua]
    public delegate void CustomCall3(int ina, out int a, out string b, out bool c);
    [CSharpCallLua]
    public delegate int CustomCall4(int ina, ref string b, ref bool c);

執行部分:

    CustomCall3 customCall3 = LuaMgr.GetInstance().Global.Get<CustomCall3>("testMultipleOUT");
    int a;
    string b;
    bool c;
    customCall3(1,out a,out b,out c);
    Debug.Log(a+"  "+b+"   "+c);

    CustomCall4 customCall4 = LuaMgr.GetInstance().Global.Get<CustomCall4>("testMultipleOUT");
    int a1=0;
    string b1=null;
    bool c1=false;
    a1 = customCall4(1,  ref b1, ref c1);
    Debug.Log(a1 + "  " + b1 + "   " + c1);

    //Xlua
    LuaFunction lf3 = LuaMgr.GetInstance().Global.Get<LuaFunction>("testMultipleOUT");
    object[] objs=lf3.Call(5);
    for (int i = 0; i < objs.Length; i++)
    {
        Debug.Log(string.Format("第{0}個返回值:" + objs[i], i));
    }

當定義了委託的返回值時,lua的function第一個返回值就是委託的返回值,從第二個開始才是後續反參

4. 變長入參方法

委託部分:

    [CSharpCallLua]
    public delegate void CustomCall5(params string[] args);

執行方法:

    CustomCall5 customCall5 = LuaMgr.GetInstance().Global.Get<CustomCall5>("testMultipleIn");
    customCall5("asfa", "dasd", "abc");

    LuaFunction lf4 = LuaMgr.GetInstance().Global.Get<LuaFunction>("testMultipleIn");
    lf4.Call("wqe", 321, true);
思考

LuaFunction每種情況都適用,因為該方法的入參和反參都是object[],就像C#中用arraylist裝東西一樣,萬能但浪費,頻繁拆裝箱。
自定義委託需要新增特性之後在視窗XLua=>Generate Code生成程式碼,讓Xlua記錄該委託,如果新增新委託,重新生成即可,修改委託則需要先Clear Generate Code再生成。

6. list和dictionary對映table

1. 陣列

lua程式碼:

    testList={1,2,3,4,5}
    testArray={"wqe",321,true}

C#程式碼:

    List<int> list = LuaMgr.GetInstance().Global.Get<List<int>>("testList");
    List<object> list3 = LuaMgr.GetInstance().Global.Get<List<object>>("testArray");
2. 字典

lua程式碼:

    testDic={
            ["1"]=1,
            ["2"]=14,
            ["3"]=21,
            ["4"]=45
    }
    testDic2={
            ["1"]=true,
            [true]=1,
            [false]=0,
            ["a"]="abcd"

    }

C#程式碼:

    Dictionary<string, int> dic = LuaMgr.GetInstance().Global.Get<Dictionary<string, int>>("testDic");
    Dictionary<object, object> dic2 = LuaMgr.GetInstance().Global.Get<Dictionary<object, object>>("testDic2");
思考

與獲取值相同,都是使用Get方法

從C#讀取lua資料都是深複製,在C#建立一份全新的資料,修改C#部分不影響lua部分的資料

7. 類對映table

用lua 的table模擬C#的類
lua類:

    testClass={
        testInt=3,
        testBool=true,
        testFloat=3.2,
        testString="qwe",
        testFun=function()
	        print("testClass列印")
        end,
        testInClass={
	        testInInt=123
        }
    }

C#類:

    public class CallLuaClass
    {
            public int testInt;
            public bool testBool; 
            public float testFloat;
            public string testString;
            public UnityAction testFun;

            public CallLuaInClass testInClass;
    }

    public class CallLuaInClass
    {
            public int testInInt;
    }

C#呼叫程式碼:

    CallLuaClass clc = LuaMgr.GetInstance().Global.Get<CallLuaClass>("testClass");
    clc.testFun();

C#的成員變數需要與Lua表中的名稱一致,名稱無法吻合的成員會被忽略
成員屬性中的類也會被一起例項化

8. 介面對映table

lua程式碼同7
C#程式碼:

    [CSharpCallLua]
    public interface ICSharpCallInterface
    {
    public int testInt
    {
            get;
            set;
    }

    public bool testBool
    {
            get;
            set;
    }

    public float testFloat
    {
            get;
            set;
    }
    
    public string testString
    {
            get;
            set;
    }

    public UnityAction testFun
    {
            get;
            set;
    }
    }

介面內部是屬性不是欄位,需要加上特性[CSharpCallLua],並且是淺複製,透過C#部分修改資料lua部分也會被修改

9. C#呼叫LuaTable

呼叫程式碼:

    //LuTable裝table
    LuaTable table = LuaMgr.GetInstance().Global.Get<LuaTable>("testClass");
    //get方法直接執行
    table.Get<LuaFunction>("testFun").Call();
    //使用完釋放LuaTable
    table.Dispose();

xLua提供的LuaTable類,對獲取到的資料淺複製,C#端修改lua端也會被影響。
並且呼叫繁瑣,不常用。

在自定義委託和介面需要新增CSharpCallLua特性

3.3 lua調C#

1. 類

  1. 準備
    因為主體是Unity,所以Lua呼叫C#的前提是,C#先開啟Lua
    由此,先建立一個C#的Main指令碼,負責呼叫Lua的主指令碼LuaMain
    後續再用LuaMain執行lua邏輯指令碼

  2. 呼叫類
    呼叫類需要寫出路徑,CS是基礎,UnityEngine是名稱空間,GameObject是類名
    例項化方法就是該類同名方法,所以直接呼叫類名就是一次例項化

     --有名稱空間
     local obj1=CS.UnityEngine.GameObject("新物體")
     --無名稱空間
     local obj2=CS.Test()
    
  3. 靜態方法靜態變數
    直接在類後面.即可

     local findObj =CS.UnityEngine.GameObject.Find("新物體")
    
  4. 成員方法成員變數
    在例項化的類後.出成員變數
    在例項化的類後:出成員方法,不用.是因為成員方法往往需要操作自身,:是會傳入自身的呼叫方法

     print(newObj.transform.position)
     findObj.transform:Translate(Vector3.right)
    
  5. 類記錄到Global
    可以在_G表記錄常用的類,避免每次都要寫長串的路徑

     Debug=CS.UnityEngine.Debug
     Vector3=CS.UnityEngine.Vector3
     GameObject =CS.UnityEngine.GameObject
     --使用
     local obj=GameObject()
     Debug.Log(obj.transform.position)
    

2. 列舉

  1. 記錄
    像類一樣,可以記錄到Global

     --記錄列舉的路徑
     PrimitiveType=CS.UnityEngine.PrimitiveType
     MyEnum=CS.E_MyEnum
    
  2. 使用
    直接點出來內容,就像C#中一樣使用

     local obj=GameObject.CreatePrimitive(PrimitiveType.Cube)
     local idle=MyEnum.Idle
     print(idle)
    
  3. 轉換
    可以由數字和字串轉換到目標列舉
    不同的是,如果找不到目標字串會報錯,找不到目標數字會自動返回一個臨時的數字列舉(並不會記錄到列舉中,無用的知識增加了)

     local intEnum=MyEnum.__CastFrom(2)
     print(intEnum)
    
     local stringEnum = MyEnum.__CastFrom("Move")
     print(stringEnum)
    

3. 陣列列表字典

以上資料都是C#的資料結構,被lua讀取時,以userdata儲存不是table,操作方式需按照C#的方式執行

  1. 陣列

     --使用基類Array例項化陣列
     local array2=CS.System.Array.CreateInstance(typeof(CS.System.Int32),5)
     print("陣列長度: "..array2.Length )
     print("陣列內容:  ".. array2[4])
     --遍歷,按照C#的邏輯,從0開始,到Length-1
     for i=0,array2.Length-1 do
             print(array2[i])
     end
    
  2. 列表
    特別的當xlua版本低於 v2.1.12時,需要下面方法例項化
    local list3=CS.System.Collections.Generic"List`1[System.String]"

     --list是泛型,先記錄String型別List
     local List_String=CS.System.Collections.Generic.List(CS.System.String)
     --再例項化
     local list3=List_String() 
     list3:Add("新增元素3")
     --遍歷
     for i=0,list3.Count-1 do
             print(list3[i])
     end
    
  3. 字典
    當xlua版本低於 v2.1.12時,也需要和列表類似的例項化方法

     --記錄字典類
     local Dic_String_Vector3=CS.System.Collections.Generic.Dictionary(CS.System.String,CS.UnityEngine.Vector3)
     --例項化字典
     local dic2=Dic_String_Vector3()
     --呼叫成員方法
     dic2:Add("right",CS.UnityEngine.Vector3.right)
     --遍歷,使用pairs
     for k,v in pairs(dic2) do
             print(k,v)
     end
     --需要透過get_Item方法獲得
     print(dic2:get_Item("right"))
     --修改透過set_Item
     dic2:set_Item("right",CS.UnityEngine.Vector3.up)
     print(dic2:get_Item("right"))
     --TryGetValue有兩個返回值,一個返回是否成功,一個返回值
     print(dic2:TryGetValue("right3"))
    

4. 擴充方法

C#程式碼:

    [LuaCallCSharp]
    public static class Tools
    {
            //擴充方法需要傳入目標類
            public static void Move(this Lesson4 obj)
            {
                    Debug.Log(obj.name + "移動前: "+obj.step);
                    obj.step += 1;
                    Debug.Log(obj.name + "移動後:" +obj.step);
            }
    }

    public class Lesson4
    {
            public string name = "吳彥祖";
            public int step = 0;
            public void Speak(string str)
            {
                    Debug.Log(str);
            }

            public static void Eat()
            {
                    Debug.Log("吃東西");
            }
    }

lua呼叫:

    --記錄類
    Lesson4=CS.Lesson4
    --靜態方法,類名.靜態方法()
    Lesson4.Eat()
    --例項化
    local obj=Lesson4()
    --執行成員方法
    obj:Speak("開始說話")
    --使用擴充方法和成員方法一致
    obj:Move()

儘量對Lua中要使用的類新增[LuaCallCSharp]特性,Xlua會記錄該類,避免使用預設的反射機制,反射效率較低

此處引申出Lua與C#呼叫的深坑,目前蒐羅到的部落格如下
如何實現兩門語言互相呼叫

5. ref和out

在lua中也要遵循C#中的ref和out規則,ref需要傳值,out不用,二者都會返回值

C#程式碼:

public int RefFun(int a,ref int b,ref int c,int d)
{
    b = a + b;
    c = c+d;
    return 100;
}
public int OutFun(int a, out int b, out int c, int d)
{
    b = a + d;
    c = a - d;
    return 200;
}
public int RefOutFun(int a, out int b, ref int c)
{
    b = a *10;
    c = a *20;
    return 300;
}

lua程式碼:

    local a,b,c=obj:RefFun(1,1,2,2)
    local a,b,c=obj:OutFun(20,30)
    local a,b,c=obj:RefOutFun(2,1)

lua調C#時,函式多返回值,第一個值是方法返回值,後面的才是ref與out的返回值

6. 過載函式

lua支援呼叫C#過載函式,但是當引數個數相同精度不同時,會分不清引數精度

C#程式碼:

    public class Lesson6
    {
            public int Calc()
            {
                    return 100;
            }
            public int Calc(int a)
            {
                    return a;
            }
            public int Calc(int a,int b)
            {
                    return a + b;
            }
            public float Calc(float a)
            {
                    return a;
            }
    }

lua呼叫:

    local obj=CS.Lesson6()

    print(obj:Calc())
    print(obj:Calc(2))
    print(obj:Calc(3,1))
    print(obj:Calc(3.3))

入參3.3時會出問題,因為lua中只有number一種數值型別,而C#有多種,lua分不清Calc(int a)Calc(float a)

有解決過載函式含糊的方法,但不建議使用,實際使用應該避免以上情況。

Xlua提供的反射解決方案:

    --得到指定函式的相關資訊
    local m1=typeof(CS.Lesson6):GetMethod("Calc",{typeof(CS.System.Int32)})
    local m2=typeof(CS.Lesson6):GetMethod("Calc",{typeof(CS.System.Single)}) --Float的類名
    --透過xlua提供的方法,轉成lua函式使用
    --轉一次,重複使用
    local  f1=xlua.tofunction(m1)
    local  f2=xlua.tofunction(m2)
    print(f1(obj,99))
    print(f2(obj,9.9))

7. 委託與事件

7.1 委託
  1. 新增委託

     --lua中不能對nil的委託 + 第一次需要先等於
     obj.del=fun
     obj.del=obj.del+function()
             print("類似的匿名函式")
     end
    
  2. 執行委託

     obj.del()
    
  3. 移除委託

     obj.del=obj.del-fun
     --委託置空
     obj.del=nil
    
7.2 事件
  1. 新增事件

     obj:eventAction("+",fun2)
     obj:eventAction("+",function()
             print("事件的匿名函式")
     end)
    
  2. 執行事件
    事件不能給外部執行,所以只能執行類內部提供的執行方法

     obj:DoEvent()
    
  3. 移除事件
    事件不能給外部直接賦值,所以也需要執行類內部提供的置空方法

     obj:eventAction("-",fun2)
     obj:Clear()
    

8. 特殊問題

8.1 二維陣列遍歷
  1. 陣列的長度獲取

     print("行:"..obj.array:GetLength(0))
     print("列:"..obj.array:GetLength(1))
    
  2. 陣列的值讀取

     print(obj.array:GetValue(0,0))
    
8.2 null和nil比較

nilnull 無法==比較

但是可以使用以下三種方法比較

  1. lua的方法
    該方法只能判斷繼承C#的System.Objcet的類

     local rig  = obj1:GetComponent(typeof(Rigidbody))
     if IsNull(rig) then
     	print("無")
     end
    
  2. 自定義一個lua全域性方法

     function IsNull(obj)
             if obj==nil or obj:Equals(nil) then
                     return true
             end
             return false
     end
    
     if rig:Equals(nil) then
             print("無")
     end
    
  3. C#提供Object的擴充方法
    該方法只能判斷繼承UnityEngine.Objcet的類

     [LuaCallCSharp]
     public static class Lesson9
     {
             public static bool IsNull(this Object obj)
             {
                     return obj == null;
             }
     }
    
     --lua呼叫
     if rig:IsNull() then
             print("無")
     end
    
8.3 lua訪問系統型別

lua無法直接使用系統型別float,int等,提示需要新增call的特性,而系統型別無法修改。
由此引出新的方法:
將需要兩個語言互相呼叫的型別全部新增到對應的list當中,給list新增特性,整個list就會被lua記錄下來

    public static class Lesson10
    {
            [CSharpCallLua]
            public static List<Type> csharpCallLuaList = new List<Type>()
            {
                    typeof(UnityAction<float>)
                    //自定義委託也可以載入列表中,都會被特性標註記錄到xlua中
            };

            [LuaCallCSharp]
            public static List<Type> luaCallCSharpList = new List<Type>()
            {
                    typeof(GameObject),
                    typeof(Rigidbody)
            };
    }

xlua參考文件:
xLua的配置

9. 協程

lua的function無法直接傳入Unity的協程執行,需要使用xlua提供的工具util轉換

前置準備:

    --記錄GameObject
    GameObject =CS.UnityEngine.GameObject
    --Unity的yield類
    WaitForSeconds=CS.UnityEngine.WaitForSeconds
    --xlua提供給的工具表
    util=require("xlua.util")

    --建立一個物體
    local obj=GameObject("Coroutine")
    --新增Mono指令碼
    local mono=obj:AddComponent(typeof(CS.LuaCallCSharp))

宣告協程方法:

    fun=function()
            local i=1
            while i<11 do
                    --yield返回Unity的類WaitForSeconds
                    coroutine.yield(WaitForSeconds(1))
                    print(i)
                    i=i+1
                    if i>3 then
                            --停止本協程
                            mono:StopCoroutine(co)
                    end
            end
    end

呼叫協程:
執行Unity協程方法時,需要使用util的方法cs_generator轉換fun

    co =mono:StartCoroutine(util.cs_generator(fun))

10. 泛型函式

預設情況下lua僅支援呼叫C#有參有約束的函式,且約束只能是Class

呼叫:

    --有約束有參,直接傳入引數
    obj:TestFun1(child,father)
    obj:TestFun1(father,child)

當需要使用其他情況的泛型時,需要在新版xlua下使用
xlua.get_generic_method(目標類,泛型方法名稱)

    --記錄泛型方法
    local testFun2 =xlua.get_generic_method(Lesson,"TestFun2")
    --設定泛型型別
    local testFun2_int =testFun2(CS.System.Int32)

    --成員方法第一個引數傳物件,靜態方法不用傳
    --執行泛型方法
    testFun2_int(obj,3)

需要注意這種方法在Mono打包時,可以正常使用。
Il2cpp時,要考慮泛型是否被剔除的問題,引用型別正常使用,值型別需要C#呼叫過同型別的泛型引數,lua才能使用,不然就會被il2cpp自動剔除,導致lua無法訪問

此處需要複習il2cpp打包的知識,自動剔除的知識有些模糊
C#補充課程-IL2Cpp問題處理

4. Hotfix

適用於已有的完全C#專案進行lua熱補丁
可以使用lua邏輯頂替以往的C#邏輯
xlua熱補丁操作指南

4.1 準備工作

  1. Tools工具資料夾
    路徑:
    xLua-master==>Tools
    複製到Unity專案父資料夾下即可:
    xxProject/Tools

  2. 加入熱修復宏
    路徑:
    File>Build Settings>Project Settings>Player>Other Settings==>Scripting Define Symbols
    新增 HOTFIX_ENABLE 並應用
    官方提示
    建議平時開發業務程式碼不開啟HOTFIX_ENABLE
    編輯器、各手機平臺這個宏要分別設定!
    自動化打包在程式碼用API設定宏不生效,需要在編輯器設定。

4.2 第一個熱補丁

  1. 特性
    C#被熱修復的指令碼需要新增 [Hotfix] 特性

  2. 編寫lua熱補丁程式碼
    熱補丁固定寫法xlua.hotfix(被補丁類,被補丁函式名,lua補丁函式)
    成員方法要增加self入參,靜態方法不需要
    示例:

     --宣告補丁方法
     hf_GetAge=function (self)
             return self.age
     end
     --註冊補丁
     xlua.hotfix(CS.HotfixTest,"GetAge",hf_GetAge)
    
  3. 生成程式碼
    Xlua==>GenerateCode
    每當新 新增 修復類或者方法時,都需要 重新生成程式碼

  4. Hotfix注入
    路徑:
    Xlua==>Hotfix Inject In Editor
    注入時如果提示沒有Tools那就是忘記做4.1準備工作的1.Tools工具資料夾步驟或者檔案路徑放錯了。
    每當被修復類內容 修改 時,都需要 重新注入

4.3 多函式替換

多個函式一起替換時,呼叫:
xlua.hotfix(被修復類,{被替換方法1=lua方法1,被替換方法2=lua方法2}
程式碼示例:

    xlua.hotfix(CS.HotfixTest2,{

            --建構函式熱補丁,固定寫法 [".ctor"]
            [".ctor"]=function ()
                    print("熱補丁後的建構函式")
            end,
            Speak=function(self,a)
                    print("熱補丁後的Speak:"..a)
            end,
            --解構函式固定寫法 Finalize
            Finalize=function ()
                    -- body
            end
    })

以上,建構函式固定寫法[".ctor"],構析函式固定寫法Finalize
構造、構析兩個函式的熱補丁只能在原有邏輯之後執行,不能替換原有邏輯,
其他函式都是替換原有邏輯

4.4 協程函式替換

  1. 引用xlua 的轉換工具類
    用來轉換lua協程給C#

     util=require("xlua.util")
    
  2. 定義補丁協程函式
    此函式的邏輯替換原有協程的邏輯

     hf_Coroutine=function ()
     	while true do
     		coroutine.yield(CS.UnityEngine.WaitForSeconds(1))
     		print("lua補丁後的協程列印")
     	end
     end
    
  3. 定義協程轉換函式
    透過util轉換lua函式返回給C#

    hf_TransformCo=function (self)          
    	--返回一個xlua處理過的lua協程函式
            return util.cs_generator(hf_Coroutine)
    end
    
  4. 替換協程函式
    3的轉換函式替換原有的協程

     xlua.hotfix(CS.HotfixMain,{
             TestCoroutine=hf_TransformCo
     })
    

實際也可以將以上三個函式巢狀在一起寫,不過有些混亂。
建議將轉換函式與補丁函式合併,寫成下面樣子:

    hf_Coroutine=function (self)          
            --返回一個xlua處理過的lua協程函式
            --此匿名函式用來頂替原有C#協程
            return util.cs_generator(function()
                    while true do
                            coroutine.yield(CS.UnityEngine.WaitForSeconds(1))
                            print("lua補丁後的協程列印")
                    end
            end)
    end


    xlua.hotfix(CS.HotfixMain,{
            TestCoroutine=hf_Coroutine
    })

4.5 訪問器索引器替換

本章節在原有的知識基礎上,只需要注意名稱。
訪問器set方法set_屬性名,get方法get_屬性名
索引器set方法set_Item,get方法get_Item
並且都是成員方法,記得加入引數self

1. 訪問器替換

原有C#訪問器:

public int Age
{
    get
    {return 0;}
    set
    {Debug.Log(value);}
}

lua補丁訪問器:

--set_屬性名  set方法
--get_屬性名  get方法
set_Age=function (self,v)
	print("lua重定向的set")
end,
get_Age=function (self,v)
	return 99
end,

2. 索引器替換

原有C#索引器:

public int this[int index]
{
    get
    {
        if (index < 0||index>=array.Length)
        {
            Debug.LogWarning("索引不正確");
            return 0;
        }
        return array[index];
    }
    set
    {
        if (index < 0 || index >= array.Length)
        {
            Debug.LogWarning("索引不正確");
            return;
        }
         array[index]= value;

    }
}

lua補丁索引器:

--set_Item  索引器設定
--get_Item  索引器獲取
set_Item=function (self,index,v)
	print("lua重定向set"..index.."值:"..v)
end,
get_Item=function (self,index)
	return 99
end

4.6 事件加減

使用場景較少

新增方法add_事件名稱
移除方法remove_事件名稱

需要注意:新增方法被重定向後,就無法正常新增了,不能繼續呼叫C#的新增方法,會造成死迴圈,一般是將函式存在lua中

lua補丁程式碼:

    add_myEvent=function (self,del)
            print(del)
            print("新增事件函式")
    end,
    remove_myEvent=function (self,del)
            print(del)
            print("移除事件函式")
    end

4.7 泛型類替換

泛型類T是不確定的,lua中要將每個型別都替換

    xlua.hotfix(CS.HotfixGenericity(CS.System.String),{
            Test=function (self,t)
                    print("lua補丁列印:"..t)
            end
    })

    xlua.hotfix(CS.HotfixGenericity(CS.System.Int32),{
            Test=function (self,t)
                    print("lua補丁列印:"..t)
            end
    })

5. xLua的揹包系統

1. 準備工作

1. Asset Bundle Browser

2020版本以下直接在UnityPackageMgr搜尋,
2020版本以上開啟UnityPackageMgr,點+號選擇URL匯入: https://github.com/Unity-Technologies/AssetBundles-Browser.git

2. xlua

以下資料夾複製到專案根目錄
Plugins,Xlua

3. C#基礎程式碼(非必須)

單例基類,AB包管理器,Lua解析器管理器

4. lua基礎程式碼(非必須)

lua物件導向邏輯,lua json工具指令碼,字串拆分指令碼

5. C#主指令碼

掛載在場景上,負責呼叫lua主指令碼即可

傳送到地方

6. VSCode外掛

  1. 美化
    Material Icon Theme
    Guides
    background

    "background.fullscreen": {

     "images": ["https://file.moyublog.com/d/file/2022-10-10/06eb592b69286f9dc9cbb50d3c320357.jpg"],
     "opacity": 0.85,
     "size": "cover",
     "position": "center",
     "interval": 0
    

    }

  2. 功能
    Chinese
    C#
    C# XML Documentation Comments ///快速註釋
    Unity Snippets
    Auto-Using for C#
    Debugger for Unity(已經棄用改用Unity)

  3. Unity除錯配置
    Debugger for Unity已經被棄用,導致只能用Unity,且只支援2019以上版本Unity。
    如果依然選擇老版本Unity可能需要參考該方法,自己沒嘗試
    VS Code裡使用Debugger for Unity外掛進行除錯(2023最新版)
    新版本也需要進入Package Manager升級Visual Studio Editor到最新版,然後去首選項選擇VSCode。
    Unity除錯前需要建立配置檔案launch.json,在檔案內新增:

     {
         "name": "Unity除錯",
         "type": "vstuc",
         "request": "attach"
     }
    
  4. Lua除錯配置
    vscode外掛EmmyLua,作用是除錯lua,但需要java的jdk。
    課程中的jdk是jdk-1.8-64版本。
    然後配置java環境:
    進入系統屬性,環境變數
    系統變數新增
    變數名:JAVA_HOME
    變數值:C:\Program Files\Java\jdk1.8.0_191
    變數名:CLASSPATH
    變數值:.;%Java_Home%\bin;%Java_Home%\lib\dt.jar;%Java_Home%\lib\tools.jar
    環境變數的Path新建%JAVA_HOME%\bin
    最後在配置檔案launch.json新增:

     {
         "type": "emmylua_attach",
         "request": "attach",
         "name": "Lua附加Unity",
         "pid": 0,
         "processName": ""
     }
    

2.UI準備

拼UI太基礎,就不做操作記錄了。

  1. 主皮膚
    拼接出一個主皮膚MainPanel
    有兩個按鈕btnSkill btnRole

  2. 揹包皮膚
    揹包皮膚BagPanel
    內容:背景圖bg,關閉按鈕btnClose
    Toggle組toggleGroup,包含:togEquip togItem togGem
    還有一個物品的scrollview,記得在content上加GridLayoutGroupContentSizeFitter

  3. 物品格子
    物品格子ItemGrid
    自上向下:圖片bg物品圖示icon數量文字num

3. lua基礎類

  1. 物件導向的模擬類

  2. 字串根據分隔符拆分的指令碼

  3. JsonUtility.lua

  4. 初始化指令碼
    負責呼叫以上三個指令碼,和提前記錄所有常用的C#類

4. 資料準備

1. 編寫一些json資料,提供給揹包item

2. 將icon打包成圖集

3. 將預製體、json、圖集打AB包

打AB如果報lua相關的錯,記得先清空xlua程式碼再打包

4. lua讀json

首先構建了兩個指令碼:

  1. ItemData
    呼叫ABMgr載入json的AB包得到TextAsset,再透過lua這邊預先呼叫的JsonUtilitydecode成表,
    將表的內容按照id:item資訊表的格式存在全域性表ItemData
  2. PlayerData
    玩家資訊表PlayerData,內部儲存三個小表:equips items gems,每個表都記錄相關的id與數量。
    初始化方法Init用來獲取玩家資料資訊,將資訊填入到玩家資訊表中,目前資料是在方法裡直接寫死的,後面可以換成從外部讀取。

5. 主皮膚MainPanel

名稱 型別 作用
MainPanel 當作本皮膚的類,記錄皮膚與按鈕
panelObj 記錄本皮膚GameObject
btnRole 記錄角色皮膚按鈕
btnSkill 記錄技能皮膚按鈕
MainPanel:Init 方法 初始化皮膚,例項化皮膚與按鈕,並傳入點選事件
MainPanel:ShowMe 方法 初始化皮膚,顯示皮膚
MainPanel:HideM 方法 隱藏皮膚
MainPanel:BtnRoleClic 方法 點選事件
MainPanel:BtnSkillClick 方法 點選事件

注意:
在呼叫按鈕的onClick:AddListener()時,傳入的是方法,只能如下:

    self.btnRole.onClick:AddListener(self.BtnRoleClick)

此時面對一個問題:方法BtnRoleClick內部沒有傳入self,也即在外部呼叫時,訪問不到主皮膚的成員。
保持該思路,就需要在方法BtnRoleClick內部修改:

    function  MainPanel:BtnRoleClick()
            print(MainPanel.panelObj)
    end

這樣可以透過全域性變數MainPanel訪問主皮膚的成員,但有些繞圈子,也違背了物件導向的思想。

解決方法:
可以在呼叫按鈕的onClick:AddListener()時,將傳入的方法套在匿名函式內,保持方法可以傳入self

self.btnRole.onClick:AddListener(function ()
    self:BtnRoleClick()
end)

6. 揹包皮膚BagPanel

名稱 型別 作用
BagPanel 當作本皮膚的類,記錄皮膚與按鈕
panelObj 記錄本皮膚GameObject
btnClose 記錄關閉按鈕
togEquip 記錄裝備核取方塊
togItem 記錄物品核取方塊
togGem 記錄寶石核取方塊
svBag 記錄滾動框
Content 記錄物品容器Transform
BagPanel:Init 方法 初始化皮膚,例項化皮膚與按鈕,並傳入點選事件
BagPanel:ShowMe 方法 初始化皮膚,顯示皮膚
BagPanel:HideM 方法 隱藏皮膚
BagPanel:ChangeType(type) 方法 根據點選的核取方塊切換下方滾動框內容

注意:

此處的Toggle事件onValueChanged:AddListener(value)有引數,需要標註[CSharpCallLua]特性,但礙於無法新增,使用一個靜態類建立靜態列表記錄型別,幫助xlua生成對應的解釋程式碼

    public static class CSharpCallLuaList
    {
            [CSharpCallLua]
            public static List<Type> cSharpCallLuaList=new List<Type>()
            { 
                    typeof(UnityAction<bool>)
            };
    }

7. 物品格子ItemGrid

依靠已有的物件導向模擬指令碼,實現一個ItemGrid

    Object:subClass("ItemGrid")
名稱 型別 作用
ItemGrid.obj 記錄場景中的物品格子
ItemGrid.icon 記錄格子的圖示
ItemGrid.num 記錄格子的數量文字
ItemGrid:Init(father,posX,posY) 方法 例項化物品格子,記錄控制元件,設定父物體與位置
ItemGrid:InitData(data) 方法 讀取資料,更新格子的圖示與數量
ItemGrid:Destroy() 方法 刪除對應的物品格子,並置空引用

此揹包只是跟隨唐老師的思路製作,後續自行製作時應該注意:其中的資料名稱命名方式可以寫得詳細一些,避免認知混亂,預製體也可以更加豐富,滿足多種定製化,比如背景圖和環繞特效等。

8. 皮膚基類

名稱 型別 作用
BasePanel.panelObj 記錄皮膚物件
BasePanel.controls 記錄所有控制元件的鍵值對錶
BasePanel.isInitEvent Boolean 監聽事件是否寫入的標記,防止多次寫入
BasePanel:Init(name) 方法 初始化,從AB包讀取目標皮膚掛載Canvas下,Get所有UIBehaviour,得到該皮膚下所有子物體的UI類並記錄到字典
BasePanel:GetControl(name,typeName) 方法 從控制元件表中找到該控制元件並返回
BasePanel:ShowMe() 方法 顯示皮膚物體
BasePanel:HideMe() 方法 隱藏皮膚物體

其中初始化方法中,控制元件表只記錄需要的控制元件,如:toggle,button等,這些控制元件在Unity中的命名按照"togRole,btnRole"的統一規則,在遍歷時只需要辨認該控制元件的名稱中是否包含所需要的關鍵字,即可排除掉無關的UI元件。
記錄時,按照[控制元件名稱]:[UI元件1,UI元件2]的形式記錄所有需要記錄的Component。

9. 擴充工具

該指令碼繼承Editor,本篇屬於編輯器擴充的知識。

1. 擴充方法的宣告

方法需要為static靜態方法,畢竟擴充指令碼一般不會進行例項化,靜態很好理解。
方法需要加特性 [MenuItem("一級選單/當前功能的按鈕名稱")] ,加上特性後可以在Unity左上角的按鈕中找到該方法。

順便一提:在右鍵選單欄建立目標資源的特性如下
[CreateAssetMenu(fileName ="檔名稱",menuName ="一級選單/檔名稱")]

2. Directory檔案操作

  1. 檢驗路徑是否有效

     boolean hasPath =Directory.Exists(path)
    
  2. 獲取搜尋檔案的路徑
    其中GetFiles(搜尋的路徑,要搜尋的檔名資訊)*佔位符可以忽略檔名稱,直接比較檔案字尾名。

     string[] paths=Directory.GetFiles(path,"*.lua");
    
  3. 建立路徑

     Directory.CreateDirectory(newPath);
    

3. File操作

  1. 檔案刪除

     File.Delete(要刪除的檔案路徑);
    
  2. 檔案copy

     File.Copy(來源路徑,copy路徑);
    

4. Unity操作

  1. 重新整理資源
    如果不重新整理,無法修改剛生成的資源

     AssetDatabase.Refresh();
    
  2. 修改AB包歸屬
    此處的importerPath是相對於Assets的,如:Assets/LuaTxt/Main.lua.txt
    此操作為建立一個記錄AB包的資源匯入器類,在 importerPath 處檢索資源的資源匯入器,併為該資源選擇AB包

     AssetImporter importer=AssetImporter.GetAtPath(importerPath);
     importer.assetBundleName="lua";
    

5. 工具程式碼

    public class LuaCopyEditor:Editor  
    {
            [MenuItem("XLua/lua檔案加txt")]
            public static void CopyLuaToTxt(){
                    //找到所有Lua檔案
                    string path=Application.dataPath+"/Lua/";
                    if(!Directory.Exists(path)) 
                            return;

                    //得到每一個lua檔案的路徑才能遷移複製
                    string[] strs=Directory.GetFiles(path,"*.lua");

                    //檔案複製到新資料夾
                    string newPath=Application.dataPath+"/LuaTxt/";

                    //判斷新路徑資料夾是否存在
                    if(!Directory.Exists(newPath))
                            Directory.CreateDirectory(newPath);
                    else
                    {
                            //得到該路徑所有txt檔案,刪除
                            string[] oldFileStrs=Directory.GetFiles(newPath,"*.txt");
                            for (int i = 0; i < oldFileStrs.Length; i++)
                            {
                                    File.Delete(oldFileStrs[i]);
                            }
                    }
                    string fileName;//要儲存的新路徑名
                    List<string> newFileNames=new List<string>();
                    for (int i = 0; i < strs.Length; i++)
                    {
                            fileName=newPath+strs[i].Substring(strs[i].LastIndexOf("/")+1)+".txt";
                            File.Copy(strs[i],fileName);
                            newFileNames.Add(fileName);
                    }
                    AssetDatabase.Refresh();
                    //如果不重新整理,無法修改剛生成的資源

                    for (int i = 0; i < newFileNames.Count; i++)
                    {
                            //傳入路徑是相對Assets的 
                            string importerPath=newFileNames[i].Substring(newFileNames[i].IndexOf("Assets"));
                            Debug.Log("AssetImporter"+importerPath);
                            AssetImporter importer=AssetImporter.GetAtPath(importerPath);
                            if(importer!=null)
                            importer.assetBundleName="lua";
                    }
            }
    }

6. 擴充知識

僅記錄以下類的常用方法

6.1 Directory

名稱空間:System.IO

  1. 檢查路徑

     bool hasPath =Directory.Exists(path)
    
  2. 建立路徑

     Directory.CreateDirectory(path)
    
  3. 刪除路徑
    值得注意的:使用本方法刪除路徑和子檔案時,需要額外操作刪除該資料夾的meta檔案,否則Unity會報警

     從指定路徑刪除空目錄
     Directory.Delete(path)
     刪除指定的目錄,並刪除該目錄中的所有子目錄和檔案
     Directory.Delete(path, true)
    
  4. 查詢路徑
    其中的萬用字元作用 *:代替0個或多個字元 ?:代替該位置的一個字元

     string[] allDirs = Directory.GetDirectories(path);
     string[] searchDirs = Directory.GetDirectories(path, "201?");
    
  5. 查詢檔案

     string[] allFilePath = Directory.GetFiles(path);
     string[] searchFilePath = Directory.GetFiles(path, "*.txt");
    
  6. 查詢路徑下的所有檔案和路徑

     string[] allPath =Directory.GetFileSystemEntries(path);
     string[] searchPath =Directory.GetFileSystemEntries(path,"*.txt");
    
  7. 移動目錄及子檔案

     Directory.Move(sourceDirectory, destinationDirectory);
    
6.2 File

File類靜態方法

6.3 AssetDatabase

Unity提供的API,提供了訪問操作資源的方法
由於 Unity 需要跟蹤專案資料夾的所有更改,因此如果要訪問或修改資源資料,則應始終使用 AssetDatabase API 而不是檔案系統。(因為直接使用File刪除檔案會導致原本的meta檔案沒有更新,Unity跟蹤失敗)
AssetDatabase 介面僅在編輯器中可用,並且在構建的播放器中沒有任何函式。與所有其他編輯器類一樣,它僅適用於放置在 Editor 資料夾中的指令碼
所有路徑均是相對於專案資料夾的路徑,例如:"Assets/MyTextures/hello.png"

AssetDatabase靜態方法

  1. 檢視

     //是否為Assets資料夾內的資源?
     bool isAsset =AssetDatabase.Contains(obj);
     //資料夾的路徑是否存在
     bool hasPath =AssetDatabase.IsValidFolder(path);
     //是否為Project 視窗中的主資源,如模型資源的跟物件
     bool isMainAsset=AssetDatabase.IsMainAsset(obj);
    
  2. GUID相關
    其中:searchStr可以是名稱的一部分 btn 標籤 l:UI 型別 t:GameObject 組合 btn l:UI t:GameObject ,且搜尋內容不分大小寫
    返回結果不限於資源也包含資料夾和指令碼類

     string[] results;
     searchStr="Players";
     results = AssetDatabase.FindAssets(searchStr);
     //加入規定搜尋範圍的資料夾路徑,不加入預設就是在Assets下搜尋
     results = AssetDatabase.FindAssets(searchStr,string[] searchInFolders);
    
     //GUId字串轉path
     string path = AssetDatabase.GUIDToAssetPath(results[0]);
     //path轉GUID
     GUID guid = AssetDatabase.GUIDFromAssetPath(path);
     //path轉GUID字串
     string guidStr = AssetDatabase.AssetPathToGUID(path);
    
  3. 載入資源

     //返回 assetPath 下所有的子資源。此函式僅返回Project檢視中可見的子資源
     Sprite[] sprites = AssetDatabase.LoadAllAssetRepresentationsAtPath("Assets/MySpriteTexture.png");
     //返回 assetPath 下所有資源。包含Project不可見的資源,順序不確定
     Object[] data = AssetDatabase.LoadAllAssetsAtPath("Assets/MySpriteTexture.png");
     //返回路徑下的主資源物件
     Object obj =AssetDatabase.LoadMainAssetAtPath("Assets/MySpriteTexture.png");
     //返回路徑下的資源物件
     Object obj =AssetDatabase.LoadAssetAtPath("Assets/MySpriteTexture.png",typeof(Texture2D));
    
  4. 建立資源

     //建立資料夾
     string guid = AssetDatabase.CreateFolder("Assets", "My Folder");
     //建立資源
     AssetDatabase.CreateAsset(material, "Assets/MyMaterial.mat");
     //儲存所有資源
     AssetDatabase.SaveAssets();
     //重新整理
     AssetDatabase.Refresh();
    
  5. 獲取path

     string path = AssetDatabase.GetAssetPath(obj)
     //
     string path = AssetDatabase.GetAssetPathsFromAssetBundle(assetBundleName)
     //獲取該AB包內所有符合名稱的資源,不限制型別
     string[] assetPaths = AssetDatabase.GetAssetPathsFromAssetBundleAndAssetName(assetBundleName, assetName);
    

未完待續

相關文章