前言
在上一篇文章 Unity3D熱更新之LuaFramework篇[06]--Lua中是怎麼實現指令碼生命週期的 中,我分析了由LuaBehaviour來實現lua指令碼生命週期的方法。
但在實際使用中發現,只有一個這樣的指令碼還不夠。
LuaBehaviour驅動XxxPanel.lua指令碼的方法,只適用於介面相對簡潔的情況(介面上只有少量的Image、Text和其它UI元件),一但遇到稍微複雜一點的情況,就有點捉襟見肘了,比如一個包含多個子項的排行榜頁面。
現以一個排行榜的示例來說明。
一、建立一個排行榜頁面
1、建立一個大廳場景,相機及Canvas設定與之前的main場景相同,然後建立一個HallPanel皮膚。
同時建立HallPanel.lua和HallCtrl.lua指令碼並做相應註冊(新增到CtrlNames和PanelNames裡並做Require)。
皮膚上放兩個按鈕(排行榜、商城),且這個皮膚不做成由PanelMgr載入的預製體,就這麼掛在Canvas下好了。
2、建立一個排行榜RankingPanel,其結構主要是幾個垂直排序的RankItem,如下圖所示。
同時建立RankingPanel.lua和RankingCtrl.lua並做相應註冊。
這個皮膚也不做成由PanelMgr載入的那種,就放在Canvas下,通過SetActive來控制顯示與隱藏(開發中這種使用方式應該也很常見)。
3、功能需求:
1) 點選HallPanel上的排行榜按鈕,彈出排行榜皮膚;
2)點選排行榜上的子項,彈出各自的名字及順序;
難點分析:
難點1,怎麼實現HallPanel的點選事件
假如不是用的Lua,而是c#,實現這個功能也太簡單了,剛入門Unity的新手也知道怎麼做。
假如HallPanel是一個動態載入的,那實現排行榜按鈕的點選事件也好做,因為有LuaBehaviour以及之前我們自己實現的UIEventEx。 由於這個是非預製體載入的,所以這條路也走不通。
思路:手動給這個HallPanel掛載LuaBehaviour.cs指令碼試試?不行就自己寫個差不多的指令碼。
難點2,怎麼讓RankItem獨自產生行為
前言中有提到過LuaBehavoiur並不適用所有情況,這個就是一種。在一個設計良好的架構中,XxxPanel.lua最好只處理淺層佈局的元素,對於複雜的巢狀的UI或者元素較多的UI,最好讓它們自行處理自己的行為。
這個需求放在這裡就是,不在RankingCtrl.lua和RankingPanel.lua中處理RankItem的邏輯,而是交由RankItem自行處理。
思路:建立一個RankItem.lua指令碼(擁有事件處理功能以及其它生命週期能力),與RankItem物件繫結。
這兩個難點,其實反映的是一個問題,我有一個unity物件,又建立了一 個lua指令碼,怎麼讓它們產生繫結關係?
下面來嘗試解決問題。
二、處理HallPanel的UI事件
方法1:使用LuaBehaviour指令碼
1、直接給HallPall物件新增LuaBehaviour指令碼;
2、在Game.lua中把初始自動載入Panel的語句註釋掉。
CtrlManager.Init(); local ctrl = CtrlManager.GetCtrl(CtrlNames.Login); if ctrl ~= nil and AppConst.ExampleMode == 1 then -- ctrl:Awake(); --就是這一句決定首先載入什麼皮膚 end3、給HallPanel的InitPanel方法新增查詢按鈕控制元件的語句,並在HallCtrl中新增按鈕事件,具體修改見程式碼:
HallPanel.lualocal transform; local gameObject; HallPanel = {}; local this = HallPanel; --啟動事件-- function HallPanel.Awake(obj) gameObject = obj; transform = obj.transform; this.InitPanel(); logWarn("Awake lua--->>"..gameObject.name); end --初始化皮膚-- function HallPanel.InitPanel() logWarn("我是HallPanel,我被載入了."); --排行榜按鈕 HallPanel.rankingBtn = transform:FindChild("BtnRanking").gameObject; --呼叫Ctrl中panel建立完成時的方法 HallCtrl.OnCreate(gameObject); end function HallPanel.OnDestroy() logWarn("OnDestroy---->>>"); endHallCtrl.luaHallCtrl = {}; local this = HallCtrl; local behaviour; local transform; local gameObject; --構建函式-- function HallCtrl.New() logWarn("HallCtrl.New--->>"); return this; end function HallCtrl.Awake() logWarn("HallCtrl.Awake--->>"); logWarn("我是HallCtrl,我被載入了."); end --啟動事件-- function HallCtrl.OnCreate(obj) gameObject = obj; transform = obj.transform; UIEventEx.AddButtonClick(HallPanel.rankingBtn, function () log("你點選了排行榜按鈕"); end); end --單擊事件-- function HallCtrl.OnClick(go) destroy(gameObject); end --關閉事件-- function HallCtrl.Close() panelMgr:ClosePanel(CtrlNames.Hall); end有一點需要注意的是,之前UI事件處理的方法是在XxxCtrl中的OnCreate方法裡處理,這個方法在XxxPanel預製體載入後被回撥。
現在HallPanel沒有預製體載入的過程,所以要在InitPanel方法的末尾手動加一句對HallCtrl.OnCreate方法的呼叫。
4、執行遊戲
點選執行後,發現,InitPanel方法中的日誌語句沒有輸出,點選按鈕也沒有響應。
經跟蹤除錯發現,在處理HallPanel皮膚時,其身上的LuaBehaviour指令碼中Awake方法的執行時,Lua虛擬機器的初始化還沒完成,甚至是在執行Start方法時其初始化也沒初始化完成。
所以,從LuaBehaviour的Awake中呼叫HallPanel.lua指令碼的Awake是不可能成功的(Lua虛擬機器沒初始化完成,所有Lua指令碼也沒被載入)。
LuaBehaviour指令碼本身沒問題,這個問題的出現,是因為我們想繞過LuaFramework的載入流程引起的。
5、解決問題
想解決這個問題,就需要修改 Awake方法的呼叫時機。
為了不破壞原有的LuaBehaviour指令碼,我們複製一個LuaBehaviour指令碼並重新命名為"CustomBehaviour"。
並在CustomBehaviour的Awake的0.1秒之後,再呼叫HallPanel.lua的Awake方法,見下圖:
重新給HallPanel物件掛載CustomBehaviour指令碼後,再執行遊戲,
能看到InitPanel方法被正確執行了,按鈕事件也生效了。
說明:用延時的方法去執行Awake,雖然讓Lua中的方法執行了,但也破壞了Awake的原本執行順序。如果對框架了解不深或遊戲邏輯處理不夠嚴謹,則會引起問題。
這只是一個臨時方法,完善的解決方案可以看看PanelMgr的載入流程,應該能找到答案。
三、顯示RankingPanel皮膚並處理RankItem子項
1、顯示RankingPanel皮膚
在HallPanel.lua中引用RankingPanel皮膚,並在HallCtrl.lua中新增點選事件,見下圖:
如此,當點選排行榜按鈕時,就會顯示排行榜皮膚了(執行前要把RankingPanel禁掉)。
完整的HallPanel.lua
View Codelocal transform; local gameObject; HallPanel = {}; local this = HallPanel; --啟動事件-- function HallPanel.Awake(obj) gameObject = obj; transform = obj.transform; this.InitPanel(); logWarn("Awake lua--->>"..gameObject.name); end --初始化皮膚-- function HallPanel.InitPanel() logWarn("我是HallPanel,我被載入了."); --排行榜按鈕 HallPanel.rankingBtn = transform:FindChild("BtnRanking").gameObject; --排行榜皮膚 HallPanel.rankingPanel = transform.parent:Find("RankingPanel"); --呼叫Ctrl中panel建立完成時的方法 HallCtrl.OnCreate(gameObject); end function HallPanel.OnDestroy() logWarn("OnDestroy---->>>"); end完整的HallCtrl.lua
View CodeHallCtrl = {}; local this = HallCtrl; local behaviour; local transform; local gameObject; --構建函式-- function HallCtrl.New() logWarn("HallCtrl.New--->>"); return this; end function HallCtrl.Awake() logWarn("HallCtrl.Awake--->>"); logWarn("我是HallCtrl,我被載入了."); end --啟動事件-- function HallCtrl.OnCreate(obj) gameObject = obj; transform = obj.transform; UIEventEx.AddButtonClick(HallPanel.rankingBtn, function () log("你點選了排行榜按鈕"); HallPanel.rankingPanel.gameObject:SetActive (true); end); end --單擊事件-- function HallCtrl.OnClick(go) destroy(gameObject); end --關閉事件-- function HallCtrl.Close() panelMgr:ClosePanel(CtrlNames.Hall); end
2、處理RankItem
思路: 我們的目標是讓RankItem具有獨立處理邏輯的能力(包括生命週期函式的執行),想到的第一個辦法就是繼續使用上邊講到的CustomBehaviour指令碼。
CustomBehaviour適用於皮膚載入,且每個皮膚要對應一個XxxPanel.lua和XxxCtrl.lua,並且還要註冊,用起來有點不方便。所在決定重新建立一個C#指令碼,以處理各種Item型別的Unity物件(如RankItem,ShopItem等)與Lua的繫結關係。
考慮到RankItem可能是動態建立的,所以這個指令碼應該有繫結unity物件與Lua指令碼物件的能力。
步驟:
1)建立一個LuaComponent指令碼
將這個指令碼放在 “Assets\LuaFramework\Scripts\Utility”下,這個指令碼包含將GameObjet與LuaTable進行繫結的Add方法以及呼叫Lua指令碼生命週期函式的方法。見下圖
LuaCompnent.cs的完整程式碼:
View Code/* * 讓Lua指令碼也能掛載到遊戲物體上的元件 * * LuaComponent主要有Get和Add兩個靜態方法,其中Get相當於UnityEngine中的GetComponent方法,Add相當於AddComponent方法, * 只不過這裡新增的是lua元件不是c#元件。每個LuaComponent擁有一個LuaTable(lua表)型別的變數table,它既引用上述的Component表。 * Add方法使用AddComponent新增LuaComponent,呼叫引數中lua表的New方法,將其返回的表賦予table。 * Get方法使用GetComponents獲取遊戲物件上的所有LuaComponent(一個遊戲物件可能包含多個lua元件,由引數table決定需要獲取哪一個), * 通過元表地址找到對應的LuaComponent,返回lua表 * * Add by TYQ */ using UnityEngine; using System.Collections; using LuaInterface; using LuaFramework; public class LuaComponent : MonoBehaviour { //Lua表 public LuaTable table; //新增LUA元件 public static LuaTable Add(GameObject go, LuaTable tableClass) { LuaFunction fun = tableClass.GetLuaFunction("New"); if (fun == null) return null; /*object[] rets = fun.Call(tableClass); if (rets.Length != 1) return null; LuaComponent cmp = go.AddComponent(); cmp.table = (LuaTable)rets[0]; */ //lua升級後不,Call方法不再返回物件,因此改為Invoke方法實現 object rets = fun.Invoke<LuaTable, object>(tableClass); if (rets == null) { return null; } LuaComponent cmp = go.AddComponent<LuaComponent>(); cmp.table = (LuaTable)rets; cmp.CallAwake(); return cmp.table; } //新增LUA元件,允許攜帶額外一個引數(args) public static LuaTable Add(GameObject go, LuaTable tableClass, LuaTable args) { LuaFunction fun = tableClass.GetLuaFunction("New"); if (fun == null) return null; object rets = fun.Invoke<LuaTable, object>(tableClass); if (rets == null) { return null; } LuaComponent cmp = go.AddComponent<LuaComponent>(); cmp.table = (LuaTable)rets; cmp.CallAwake(args); return cmp.table; } //新增LUA元件 // isAllowOneComponent為true時,表示只新增一次元件,如果已存在,就不再新增 public static LuaTable Add(GameObject go, LuaTable tableClass, bool isAllowOneComponent) { //如果已存在,則不再新增 LuaComponent luaComponent = go.GetComponent<LuaComponent>(); if (luaComponent != null) { return null; } LuaFunction fun = tableClass.GetLuaFunction("New"); if (fun == null) return null; object rets = fun.Invoke<LuaTable, object>(tableClass); if (rets == null) { return null; } LuaComponent cmp = go.AddComponent<LuaComponent>(); cmp.table = (LuaTable)rets; cmp.CallAwake(); return cmp.table; } //獲取lua元件 public static LuaTable Get(GameObject go, LuaTable table) { /* LuaComponent[] cmps = go.GetComponents(); foreach (LuaComponent cmp in cmps) { string mat1 = table.ToString(); string mat2 = cmp.table.GetMetaTable().ToString(); if (mat1 == mat2) { return cmp.table; } } */ LuaComponent cmp = go.GetComponent<LuaComponent>(); string mat1 = table.ToString(); string mat2 = cmp.table.GetMetaTable().ToString(); if (mat1 == mat2) { return cmp.table; } return null; } //刪除LUA元件的方法略,呼叫Destory()即可 //呼叫lua表的Awake方法 void CallAwake() { LuaFunction fun = table.GetLuaFunction("Awake"); if (fun != null) fun.Call(table, gameObject); } //呼叫lua表的Awake方法(攜帶一個引數) void CallAwake(LuaTable args) { LuaFunction fun = table.GetLuaFunction("Awake"); if (fun != null) fun.Call(table, gameObject, args); } private void OnEnable() { // Debug.Log("================================================================================"); //Debug.Log(table); if (table == null) { //Debug.LogWarning("Table is Null---------------------"); return; } LuaFunction fun = table.GetLuaFunction("OnEnable"); if (fun != null) { fun.Call(table, gameObject); } } void Start() { LuaFunction fun = table.GetLuaFunction("Start"); if (fun != null) fun.Call(table, gameObject); } void Update() { //效率問題有待測試和優化 //可在lua中呼叫UpdateBeat替代 LuaFunction fun = table.GetLuaFunction("Update"); if (fun != null) fun.Call(table, gameObject); } private void FixedUpdate() { LuaFunction fun = table.GetLuaFunction("FixedUpdate"); if (fun != null) fun.Call(table, gameObject); } private void LateUpdate() { LuaFunction fun = table.GetLuaFunction("LateUpdate"); if (fun != null) fun.Call(table, gameObject); } void OnCollisionEnter(Collision collisionInfo) { //略 } //更多函式略 private void OnDisable() { if (table != null) { LuaFunction fun = table.GetLuaFunction("OnDisable"); if (fun != null) { fun.Call(table, gameObject); } } } private void OnDestroy() { if (table != null) { LuaFunction fun = table.GetLuaFunction("OnDestroy"); if (fun != null) { fun.Call(table, gameObject); } } } }這個指令碼的寫法參考了知乎上 羅培羽 大佬的一篇文章 :Unity3D熱更新LuaFramework入門實戰(4)——Lua元件
該文章裡有詳細的原理闡述,我這裡就不多解釋了。
LuaComponent.cs指令碼建立完畢後,需要新增到CustomSetting.cs檔案中並進行匯出操作(Generate All)。
2)建立一個RankItem.Lua的指令碼,並放在Controller/Hall目錄下。
RankItem的主要功能是在其Start方法中查詢子元件並賦值 以及 新增按鈕點選事件,見程式碼:
function RankItem:Start()-- 這裡的id, name, score來源於繫結時的賦值,見RankingPanel的 InitPanel方法
-- 設定Id
self.obj.transform:Find("TextOrder"):GetComponent("Text").text = self.id;
-- 設定name
self.obj.transform:Find("TextName"):GetComponent("Text").text = self.name;
-- 設定score
self.obj.transform:Find("TextScore"):GetComponent("Text").text = self.score;
UIEventEx.AddButtonClick(self.obj, function ()
log("你點選了RankItem " .. self.name);
end);
endRankItem.lua的完整程式碼在這裡:
View CodeRankItem = { --裡面可以放一些屬性 name = "RankItem", index = -1, --索引 obj = nil --指令碼關聯的物件 } function RankItem:Awake() --print("RankItem Awake name = "..self.name ); end function RankItem:Start() -- 設定Id self.obj.transform:Find("TextOrder"):GetComponent("Text").text = self.id; -- 設定name self.obj.transform:Find("TextName"):GetComponent("Text").text = self.name; -- 設定score self.obj.transform:Find("TextScore"):GetComponent("Text").text = self.score; UIEventEx.AddButtonClick(self.obj, function () log("你點選了RankItem " .. self.name); end); end --Item點選事件 function RankItem.OnItemClick (go, selfData) end function RankItem:Update() end --建立物件 function RankItem:New(obj) local o = {} setmetatable(o, self) self.__index = self return o end
3)在RankingPanel.lua中查詢RankItem的引用,並進行繫結操作
a.宣告rankitemData變數,這裡存放的是將要顯示在RankItem上的資料。
b.查詢rankItem子元件並用LuaComponent.Add方法執行繫結操作,程式碼如下:
--排行榜項資料 local rankItemData = { {id = 1, name = "張三1", score = 700}, {id = 2, name = "張三2", score = 500}, {id = 3, name = "張三3", score = 300}, {id = 4, name = "張三4", score = 200} } --初始化皮膚-- function RankingPanel.InitPanel() local rankList = transform:FindChild("RankList"); for i = 1, rankList.childCount do local go = rankList:GetChild(i - 1).gameObject; log(go.name); local item = LuaComponent.Add(go, RankItem); item.name = rankItemData[i].name; item.index = i; item.obj = go; item.id = rankItemData[i].id; item.score = rankItemData[i].score; end RankingCtrl.OnCreate(gameObject); end完整的RankingPanel.lua程式碼在這裡:
View Codelocal transform; local gameObject; require("Controller/Hall/RankItem") RankingPanel = {}; local this = RankingPanel; --啟動事件-- function RankingPanel.Awake(obj) gameObject = obj; transform = obj.transform; this.InitPanel(); logWarn("=========Awake lua--->>"..gameObject.name); end --排行榜項資料 local rankItemData = { {id = 1, name = "張三1", score = 700}, {id = 2, name = "張三2", score = 500}, {id = 3, name = "張三3", score = 300}, {id = 4, name = "張三4", score = 200} } --初始化皮膚-- function RankingPanel.InitPanel() local rankList = transform:FindChild("RankList"); for i = 1, rankList.childCount do local go = rankList:GetChild(i - 1).gameObject; log(go.name); local item = LuaComponent.Add(go, RankItem); item.name = rankItemData[i].name; item.index = i; item.obj = go; item.id = rankItemData[i].id; item.score = rankItemData[i].score; end RankingCtrl.OnCreate(gameObject); end --單擊事件-- function RankingPanel.OnDestroy() logWarn("OnDestroy---->>>"); end4)執行
執行Hall場景,點出排行榜皮膚。
能看到在lua指令碼給定的值(rankItemData )已經被正確顯示到RankItem上了。點選相應項,輸出的內容也符合預期。
總結
要用Lua做邏輯開發,怎麼讓unity物件繫結lua指令碼,是一個繞不過去的問題。由於網上相關資料比較少,這一篇講的都是自己摸出來的一點門道,不知道寫得是否對,但勉強還能用,僅供參考。
補充一個在LuaFramework中實現Update的簡單方法
要在XxxPane中實現Update等方法,直接在其Awake函式中寫 UpdateBeat:Add(Update, self) 就行,見程式碼
function XxxPanel.Awake(obj) gameObject = obj; transform = obj.transform; UpdateBeat:Add(Update, self); FixedUpdateBeat:Add(FixedUpdate, self); LateUpdateBeat:Add(LateUpdate, self); end
Add函式的第一個引數是一個function, 是這個指令碼中定義的函式。這個UpdaateBeat應該是框架實現的全域性函式。