一、tolua下載
tolua
的GitHub
下載地址:https://github.com/topameng/tolua
假設我們下載的是LuaFramework_UGUI,它是基於Unity 5.0 + UGUI + tolua
構建的工程
下載下來得到一個LuaFramework_UGUI-master.zip
二、運行Demo
1、生成註冊文件
解壓之後就是一個Unity
的工程,直接用Unity
打開,首次打開工程會詢問生成註冊文件,點擊確定即可
2、將lua打成AssetBundle
首先要執行lua
資源的生成(打AssetBundle
),點擊菜單【LuaFramework】-【Build Windows Resource】
會把lua
代碼打成AssetBundle
放在StreamingAssets
中。
3、解決報錯
如果你用的不是Unity5.x
,而是Unity2020
,那麼可能會報錯:
這是因爲新版本的Unity有些屬性和接口已經廢棄了的原因,我們需要特殊處理一下
一個是Light類,一個是QualitySettings類,這兩個類我們一般不需要在lua中使用,所以我們不對他們生產Wrap即可:
1、打開CustomSettings.cs,把 _GT(typeof(Light)),和 _GT(typeof(QualitySettings)),這兩行註釋掉
2、然後單擊菜單【Lua】-【Clear wrap files】清理掉Wrap
3、然後再單擊菜單【Lua】-【Generate All】重新生成Wrap,
4、然後再重新點擊菜單【LuaFramework】-【Build Windows Resource】生成lua資源。
執行【Lua】-【Generate All】菜單的時候,你可能會報錯
定位到報錯的位置
添加判空
重新執行【Lua】-【Generate All】
菜單
生成後應該還有報錯
這是因爲新版的ParticleSystem類新增了一些接口,我們可以定位到對應報錯的地方,把報錯的地方註釋掉。
不過爲了防止下次執行【Lua】-【Generate All】菜單時又被覆蓋導致報錯,我們可以把UnityEngine_ParticleSystemWrap.cs移動到BaseType目錄中
並把CustomSettings.cs中的_GT(typeof(ParticleSystem)),註釋掉。
並在LuaState.cs註冊ParticleSystemWrap類,要注意調用點要放在對應的BeginModul和EndModule之間,是什麼命名空間下的,就放在什麼Modul之下,如果是多級命名空間,則是嵌套多個BeginModul和EndModule。
// LuaState.cs void OpenBaseLibs() { // ... BeginModul("UnityEngine"); // ... UnityEngine_ParticleSystemWrap.Register(this); EndModule(); //end UnityEngine }
同理,UnityEngine_MeshRendererWrap.cs可能也會報錯,按上面的處理方式處理。
4、爲何一些沒有在CustomSettings.cs註冊的類也會生成Wrap類
假設我們把某個Wrap類手動移動到BaseType目錄中,並在CustomSettings.cs中註釋掉對應的_GT(typeof(xxx)),理論上應該不會生成對應的Wrap類,但事實上可能還是生成了,爲什麼?
這是因爲ToLua會將在CustomSettings.cs中註冊的類的父類進行遞歸生成。
舉個例子,CustomSettings.cs中把_GT(typeof(Component))註釋掉,執行【Lua】-【Generate All】菜單,依然會生成UnityEngine_ComponentWrap.cs,爲什麼?
因爲在CustomSettings.cs中有_GT(typeof(Transform)),而Transform的父類是Component,所以依然會生成UnityEngine_ComponentWrap.cs。
具體邏輯可以看ToLuaMenu.cs的AutoAddBaseType函數,它裏面就是進行遞歸生成父類的Wrap類的。
如果你將UnityEngine_ComponentWrap.cs移動到BaseType目錄中,並且不想重新生成UnityEngine_ComponentWrap.cs,可以在ToLuaMenu.cs的dropType數組中添加typeof(UnityEngine.Component)即可,不過不建議這麼做,因爲這裏有個坑!
這個坑就是Component的子類生成Wrap類是錯誤的。舉個例子,Transform是繼承Component,生成的UnityEngine_TransformWrap代碼是這樣的:
public class UnityEngine_TransformWrap { public static void Register(LuaState L) { L.BeginClass(typeof(UnityEngine.Transform), typeof(UnityEngine.Component)); // ... } }
當你在dropType
數組中添加typeof(UnityEngine.Component)
,那麼生成出來的UnityEngine_RendererWrap
是這樣的:
public class UnityEngine_TransformWrap { public static void Register(LuaState L) { L.BeginClass(typeof(UnityEngine.Transform), typeof(UnityEngine.Object)); // ... } }
發現沒有,會認爲Transform是繼承Object,而事實上,Transform是繼承Component的,這樣會導致你在lua中對於Component子類的對象無法訪問Component的public成員、屬性和方法。
比如下面這個會報錯,提示不存在gameObject成員或屬性。
-- 假設r是Transform對象
print(t.gameObject)
解決辦法就是不要在dropType
數組中添加過濾類,而是在ToLuaExport.cs
類的Generate
方法中進行過濾,例:
// ToLuaExport.cs public static void Generate(string dir) { // ... if(type(Component) == type) { return; } // ... }
5、順利生成AssetBundle
最後,【LuaFramework】-【Build Windows Resource】
成功生成AssetBundle
,我們可以在StreamingAssets
中看到很多AssetBundle
文件。
6、運行Demo場景
接下來,我們就可以運行Demo
場景了。打開main
場景
運行效果
7、Unity2020無報錯版LuaFramework-UGUI
如果你不想手動修復上報的報錯,我將修復好的版本上傳到了GitHub,使用Unity2020可以直接運行。
GitHub工程地址:https://github.com/linxinfa/Unity2020-LuaFramework-UGUI
三、開發環境IDE
可以使用subline
,也可以使用visual studio
,個人偏好使用visual studio
,配合插件BabeLua
Unity寫lua代碼的vs插件:BabeLua: https://blog.csdn.net/linxinfa/article/details/88191485
四、接口講解
1、MVC框架
上面這個Lua
動態創建出來的面板的控制邏輯在PromptCtrl.lua
腳本中,我們可以看到lua
工程中使用了經典的MVC
框架。
MVC全名是Model View Controller,是模型(model)-視圖(view)-控制器(controller)的縮寫,一種軟件設計典範,用一種業務邏輯、數據、界面顯示分離的方法組織代碼,
將業務邏輯聚集到一個部件裏面,在改進和個性化定製界面及用戶交互的同時,不需要重新編寫業務邏輯。
所有的controler
在CtrlManager
中註冊
-- CtrlManager.lua function CtrlManager.Init() logWarn("CtrlManager.Init----->>>"); ctrlList[CtrlNames.Prompt] = PromptCtrl.New(); ctrlList[CtrlNames.Message] = MessageCtrl.New(); return this; end
通過CtrlManager
獲取對應的controler
對象,調用Awake()
方法
-- CtrlManager.lua local ctrl = CtrlManager.GetCtrl(CtrlNames.Prompt); if ctrl ~= nil then ctrl:Awake(); end
controler
類中,Awake()
方法中調用C#
的PanelManager
的CreatePanel
方法
-- PromptCtrl.lua function PromptCtrl.Awake() logWarn("PromptCtrl.Awake--->>"); panelMgr:CreatePanel('Prompt', this.OnCreate); end
C#
的PanelManager
的CreatePanel
方法去加載界面預設,並掛上LuaBehaviour
腳本
這個LuaBehaviour
腳本,主要是管理panel
的生命週期,調用lua
中panel
的Awake
,獲取UI
元素對象
-- PromptPanel.lua local transform; local gameObject; PromptPanel = {}; local this = PromptPanel; --啓動事件-- function PromptPanel.Awake(obj) gameObject = obj; transform = obj.transform; this.InitPanel(); logWarn("Awake lua--->>"..gameObject.name); end --初始化面板-- function PromptPanel.InitPanel() this.btnOpen = transform:Find("Open").gameObject; this.gridParent = transform:Find('ScrollView/Grid'); end --單擊事件-- function PromptPanel.OnDestroy() logWarn("OnDestroy---->>>"); end
panel
的Awake
執行完畢後,就會執行controler
的OnCreate()
,在controler
中對UI
元素對象添加一些事件和控制
-- PromptCtrl.lua --啓動事件-- function PromptCtrl.OnCreate(obj) gameObject = obj; transform = obj.transform; panel = transform:GetComponent('UIPanel'); prompt = transform:GetComponent('LuaBehaviour'); logWarn("Start lua--->>"..gameObject.name); prompt:AddClick(PromptPanel.btnOpen, this.OnClick); resMgr:LoadPrefab('prompt', { 'PromptItem' }, this.InitPanel); end
2、StartUp啓動框架
AppFacade.Instance.StartUp(); //啓動遊戲
這個接口會拋出一個NotiConst.START_UP事件,對應的響應類是StartUpCommand
using UnityEngine; using System.Collections; using LuaFramework; public class StartUpCommand : ControllerCommand { public override void Execute(IMessage message) { if (!Util.CheckEnvironment()) return; GameObject gameMgr = GameObject.Find("GlobalGenerator"); if (gameMgr != null) { AppView appView = gameMgr.AddComponent<AppView>(); } //-----------------關聯命令----------------------- AppFacade.Instance.RegisterCommand(NotiConst.DISPATCH_MESSAGE, typeof(SocketCommand)); //-----------------初始化管理器----------------------- AppFacade.Instance.AddManager<LuaManager>(ManagerName.Lua); AppFacade.Instance.AddManager<PanelManager>(ManagerName.Panel); AppFacade.Instance.AddManager<SoundManager>(ManagerName.Sound); AppFacade.Instance.AddManager<TimerManager>(ManagerName.Timer); AppFacade.Instance.AddManager<NetworkManager>(ManagerName.Network); AppFacade.Instance.AddManager<ResourceManager>(ManagerName.Resource); AppFacade.Instance.AddManager<ThreadManager>(ManagerName.Thread); AppFacade.Instance.AddManager<ObjectPoolManager>(ManagerName.ObjectPool); AppFacade.Instance.AddManager<GameManager>(ManagerName.Game); } }
這裏初始化了各種管理器,我們可以根據具體需求進行改造和自定義。
3、LuaManager核心管理器
LuaManager
這個管理器是必須的,掌管整個lua
虛擬機的生命週期。它主要是加載lua
庫,加載lua
腳本,啓動lua
虛擬機,執行Main.lua
。
4、AppConst常量定義
AppConst定義了一些常量。
其中AppConst.LuaBundleMode是lua代碼AssetBundle模式。它會被賦值給LuaLoader的beZip變量,在加載lua代碼的時候,會根據beZip的值去讀取lua文件,false則去search path中讀取lua文件,否則從外部設置過來的bundle文件中讀取lua文件。默認爲true。在Editor環境下,建議把AppConst.LuaBundleMode設爲false,這樣方便運行,否則寫完lua代碼需要生成AssetBundle纔可以運行到。
#if UNITY_EDITOR public const bool LuaBundleMode = false; //Lua代碼AssetBundle模式 #else public const bool LuaBundleMode = true; //Lua代碼AssetBundle模式 #endif
5、Lua代碼的讀取
LuaLoader和LuaResLoader都繼承LuaFileUtils。lua代碼會先從LuaFramework.Util.AppContentPath目錄解壓到LuaFramework.Util.DataPath目錄中,lua文件列表信息記錄在files.txt中,此文件也會拷貝過去。然後從LuaFramework.Util.DataPath目錄中讀取lua代碼。
/// LuaFramework.Util.DataPath /// <summary> /// 應用程序內容路徑 /// AppConst.AssetDir = "StreamingAssets" /// </summary> public static string AppContentPath() { string path = string.Empty; switch (Application.platform) { case RuntimePlatform.Android: path = "jar:file://" + Application.dataPath + "!/assets/"; break; case RuntimePlatform.IPhonePlayer: path = Application.dataPath + "/Raw/"; break; default: path = Application.dataPath + "/" + AppConst.AssetDir + "/"; break; } return path; } /// <summary> /// 取得數據存放目錄 /// </summary> public static string DataPath { get { string game = AppConst.AppName.ToLower(); if (Application.isMobilePlatform) { return Application.persistentDataPath + "/" + game + "/"; } if (AppConst.DebugMode) { return Application.dataPath + "/" + AppConst.AssetDir + "/"; } if (Application.platform == RuntimePlatform.OSXEditor) { int i = Application.dataPath.LastIndexOf('/'); return Application.dataPath.Substring(0, i + 1) + game + "/"; } return "c:/" + game + "/"; } }
完了之後,再進行遠程的更新檢測,看看用不用熱更lua
代碼,遠程url
就是AppConst.WebUrl,先下載files.txt
,然後再讀取lua
文件列表進行下載。
6、GameManager遊戲管理器
啓動框架後,會創建GameManager
遊戲管理器,它負責檢測lua
邏輯代碼的更新檢測和加載(Main.lua
是在LuaManager
中執行的),我們可以在GameManager
中DoFile
我們自定義的lua
腳本,比如Game.lua
腳本。
7、C#中如何直接調用lua的某個方法
GameManager
可以獲取到LuaManager
對象,通過LuaManager.CallFunction接口調用。
也可以用Util.CallMethod接口調用,兩個接口的參數有差異,需要注意。
/// LuaManager.CallFunction接口 public object[] CallFunction(string funcName, params object[] args) { LuaFunction func = lua.GetFunction(funcName); if (func != null) { return func.LazyCall(args); } return null; } /// Util.CallMethod接口 public static object[] CallMethod(string module, string func, params object[] args) { LuaManager luaMgr = AppFacade.Instance.GetManager<LuaManager>(ManagerName.Lua); if (luaMgr == null) return null; return luaMgr.CallFunction(module + "." + func, args); }
8、lua中如何調用C#的方法
假設現在我們有一個C#
類
using UnityEngine; public class MyTest : MonoBehaviour { public int myNum; public void SayHello() { Debug.Log("Hello,I am MyTest,myNum: " + myNum); } public static void StaticFuncTest() { Debug.Log("I am StaticFuncTest"); } }
我們想在lua中訪問這個MyTest類的函數。首先,我們需要在CustomSettings.cs中的customTypeList數組中添加類的註冊:
_GT(typeof(MyTest)),
然後然後再單擊菜單【Lua】-【Generate All】生成Wrap,生成完我們會看到一個MyTestWrap類
接下來就可以在lua中訪問了。(注意AppConst.LuaBundleMode的值要設爲false,方便Editor環境下運行lua代碼,否則需要先生成AssetBundle才能運行)
function Game.TestFunc() -- 靜態方法訪問 MyTest.StaticFuncTest() local go = UnityEngine.GameObject("go") local myTest = go:AddComponent(typeof(MyTest)) -- 成員變量 myTest.myNum = 5 -- 成員方法 myTest:SayHello() end
調用Game.TestFunc()
注意,靜態方法、靜態變量、成員變量、成員屬性使用 “.” 來訪問,比如上面的 myTest.myNum,成員函數使用 “:” 來訪問,比如上面的 myTest:SayHello()
9、lua中如何使用協程
function fib(n) local a, b = 0, 1 while n > 0 do a, b = b, a + b n = n - 1 end return a end function CoFunc() print('Coroutine started') for i = 0, 10, 1 do print(fib(i)) coroutine.wait(0.1) end print("current frameCount: "..Time.frameCount) coroutine.step() print("yield frameCount: "..Time.frameCount) local www = UnityEngine.WWW("http://www.baidu.com") coroutine.www(www) local s = tolua.tolstring(www.bytes) print(s:sub(1, 128)) print('Coroutine ended') end
調用
coroutine.start(CoFunc)
如果要stop
協程,則需要這樣
local co = coroutine.start(CoFunc)
coroutine.stop(co)
10、lua解析json
假設現在有這麼一份json文件
{ "glossary": { "title": "example glossary", "GlossDiv": { "title": "S", "GlossList": { "GlossEntry": { "ID": "SGML", "SortAs": "SGML", "GlossTerm": "Standard Generalized Mark up Language", "Acronym": "SGML", "Abbrev": "ISO 8879:1986", "GlossDef": { "para": "A meta-markup language, used to create markup languages such as DocBook.", "GlossSeeAlso": ["GML", "XML"] }, "GlossSee": "markup" } } } } }
假設我們已經把上面的json文件的內容保存到變量jsonStr字符串中,現在在lua中要解析它
local json = require 'cjson' function Test(str) local data = json.decode(str) print(data.glossary.title) s = json.encode(data) print(s) end
調用Test(jsonStr)
11、lua調用C#的託管
// c#傳託管給lua System.Action<string> cb = (s) => { Debug.Log(s); }; Util.CallMethod("Game", "TestCallBackFunc", cb);
-- lua調用C#的託管 function Game.TestCallBackFunc(cb) if nil ~= cb then System.Delegate.DynamicInvoke(cb,"Hello, I am lua, I call Delegate") end end
12、lua通過反射調用C#
有時候,我們沒有把我們的C#
類生成Wrap
,但是又需要在lua
中調用,這個時候,可以通過反射來調用。
假設我們有一個C#
類:MyClass
// MyClass.cs public sealed class MyClass { //字段 public string myName; //屬性 public int myAge { get; set; } //靜態方法 public static void SayHello() { Debug.Log("Hello, I am MyClass's static func: SayHello"); } public void SayNum(int n) { Debug.Log("SayNum: " + n); } public void SayInfo() { Debug.Log("SayInfo, myName: " + myName + ",myAge: " + myAge); } }
在lua
中
-- Game.lua function Game.TestReflection() require 'tolua.reflection' tolua.loadassembly('Assembly-CSharp') local BindingFlags = require 'System.Reflection.BindingFlags' local t = typeof('MyClass') -- 調用靜態方法 local func = tolua.getmethod(t, 'SayHello') func:Call() func:Destroy() func = nil -- 實例化 local obj = tolua.createinstance(t) -- 字段 local field = tolua.getfield(t, 'myName') -- 字段Set field:Set(obj, "linxinfa") -- 字段Get print('myName: ' .. field:Get(obj)) field:Destroy() -- 屬性 local property = tolua.getproperty(t, 'myAge') -- 屬性Set property:Set(obj, 29, null) -- 屬性Get print('myAge: ' .. property:Get(obj, null)) property:Destroy() --public成員方法SayNum func = tolua.getmethod(t, 'SayNum', typeof('System.Int32')) func:Call(obj, 666) func:Destroy() --public成員方法SayInfo func = tolua.getmethod(t, 'SayInfo') func:Call(obj) func:Destroy() end
調用Game.TestReflection()
13、nil和null
nil是lua對象的空,null表示c#對象的空。假設我們在c#中有一個GameObject對象傳遞給了lua的對象a,接下來我們把這個GameObject對象Destroy了,並在c#中把這個GameObject對象賦值爲null,此時lua中的對象a並不會等於nil
如果要在lua中判斷一個對象是否爲空,安全的做法是同時判斷nil和null
-- lua中對象判空 function IsNilOrNull(o) return nil == o or null == o end
14、獲取今天是星期幾
-- 1是週日,2是週一,以此類推 function GetTodayWeek() local t = os.date("*t", math.floor(os.time())) return t.wday end
15、獲取今天的年月日
方法一
function GetTodayYMD() local t = os.date("*t", math.floor(os.time())) return t.year .. "/" .. t.month .. "/" .. t.day end
方法二
function GetTodayYMD() -- 如果要顯示時分秒,則用"%H:%M:%S" return os.date("%Y/%m%d", math.floor(os.time())) end
16、字符串分割
-- 參數str是你的字符串,比如"小明|小紅|小剛" -- 參數sep是分隔符,比如"|" -- 返回值爲{"小明","小紅","小剛"} function SplitString(str, sep) local sep = sep or " " local result = {} local pattern = string.format("([^%s]+)", sep) string.gsub(s, pattern, function(c) result[#result + 1] = c end) return result end
17、大數字加逗號分割(數字會轉成字符串)
-- 參數num是數字,如3428439,轉換結果"3,428,439" function FormatNumStrWithComma(num) local numstr = tostring(num) local strlen = string.len(numstr) local splitStrArr = {} for i = strlen, 1, -3 do local beginIndex = (i - 2 >= 1) and (i - 2) or 1 table.insert(splitStrArr, string.sub(numstr, beginIndex, i)) end local cnt = #splitStrArr local result = "" for i = cnt, 1, -1 do if i == cnt then result = result .. splitStrArr[i] else result = result .. "," .. splitStrArr[i] end end return result end
18、通過組件名字添加組件
-- 緩存 local name2Type = {} -- 參數gameObject物體對象 -- 參數componentName,組件名字,字符串 function AddComponent(gameObject, componentName) local component = gameObject:GetComponent(componentName) if nil ~= component then return component end local componentType = name2Type[componentName] if nil == componentType then componentType = System.Type.GetType(componentName) if nil == componentType then print("AddComponent Error: " .. componentName) return nil else name2Type[componentName] = componentType end end return gameObject:AddComponent(componentType) end
19、深拷貝
lua中的table是引用類型,有時候我們爲了不破壞原有的table,可能要用到深拷貝
function DeepCopy(t) if nil == t then return nil end local result = () for k, v in pairs(t) do if "table" == type(v) then result[k] = DeepCopy(v) else result[k] = v end end return result end
20、四捨五入
function Round(fnum) return math.floor(fnum + 0.5) end
21、檢測字符串是否含有中文
-- 需要把C#的System.Text.RegularExpressions.Regex生成Wrap類 function CheckIfStrContainChinese(str) return System.Text.RegularExpressions.Regex.IsMatch(str, "[\\u4e00-\\u9fa5]") end
22、數字的位操作get、set
-- 通過索引獲取數字的某一位,index從1開始 function GetBitByIndex(num, index) if nil == index then print("LuaUtil.GetBitByIndex Error, nil == index") return 0 end local b = bit32.lshift(1,(index - 1)) if nil == b then print("LuaUtil.GetBitByIndex Error, nil == b") return 0 end return bit32.band(num, b) end -- 設置數字的某個位爲某個值,num:目標數字,index:第幾位,從1開始,v:要設置成的值,0或1 function SetBitByIndex(num, index, v) local b = bit32.lshift(1,(index - 1)) if v > 0 then num = bit32.bor(num, b) else b = bit32.bnot(b) num = bit32.band(num, b) end return num end
23、限制字符長度,超過進行截斷
有時候,字符串過長需要截斷顯示,比如有一個暱稱叫“我的名字特別長一行顯示不下”,需求上限制最多顯示5個字,超過的部分以…替代,即"我的名字特…"。首先要計算含有中文的字符串長度,然後再進行截斷
-- 含有中文的字符串長度 function StrRealLen(str) if str == nil then return 0 end local count = 0 local i = 1 while (i < #str) do local curByte = string.byte(str, i) local byteCount = 1 if curByte >= 0 and curByte <= 127 then byteCount = 1 elseif curByte >= 192 and curByte <= 223 then byteCount = 2 elseif curByte >= 224 and curByte <= 239 then byteCount = 3 elseif curByte >= 240 and curByte <= 247 then byteCount = 4 end local char = string.sub(str, i, i + byteCount - 1) i = i + byteCount count = count + 1 end return count end -- 限制字符長度(多少個字) -- 參數str,爲字符串 -- 參數limit爲限制的字數,如8 -- 參數extra爲當超過字數時,在尾部顯示的字符串,比如"..." function LimitedStr(str, limit, extra) limit = limit or 8 extra = extra or "" local text = "" -- 含有中文的字符串長度 if StrRealLen(str) > limit then text = LuaUtil.sub_chars(str, limit) .. "..." .. extra else text = str .. extra end return text end
24、判斷字符串A是否已某個字符串B開頭
-- 判斷字符串str是否是以某個字符串start開頭 function StringStartsWith(str, start) return string.sub(str, 1, string.len(start)) == start end
五、熱更lua與資源
1、熱更lua
打app整包的時候,備份一份lua全量文件,後面打lua增量包的時候,根據文件差異進行比對,新增和差異的lua文件打成一個lua_update.bundle,放在一個update文件夾中,並壓縮成zip,放到服務器端,客戶端通過https下載增量包並解壓到Application.persistentDataPath目錄。遊戲加載lua文件的時候,優先從update文件夾中的lua_update.bundle中查找lua腳本。
2、熱更資源熱更資源
做個編輯器工具,指定某個或某些資源文件(預設、音頻、動畫、材質等),打成多個assetbundle,放在一個update文件夾中,並壓縮成一個zip,放到服務器端,客戶端通過https下載增量包並解壓到Application.persistentDataPath目錄。
遊戲加載資源文件的時候,優先從update文件夾中查找對應的資源文件。
3、真機熱更資源存放路徑
persistentDataPath/res/ ├──/update/ │ ├──/lua/ │ │ └──lua_update.bundle #lua增量bundle │ ├──/res/ │ │ ├──aaa.bundle #預設aaa的bundle │ │ ├──bbb.bundle #音頻bbb的bundle │ │ └──... #其他各種格式的資源bundle │ └──/cfg/ │ ├──cfg.bundle #配置增量bundle │ └──... #其他文本或二進制文件增量bundle ├──out_put.log #遊戲日誌 └──...
關於persistentDataPath
,可以參見我這篇博客:https://blog.csdn.net/linxinfa/article/details/51679528
轉載鏈接:https://blog.csdn.net/linxinfa/article/details/88246345