在上一篇文章 Unity3D熱更新之LuaFramework篇[06]--Lua中是怎麼實現腳本生命週期的 中,我分析了由LuaBehaviour來實現lua腳本生命週期的方法。html
但在實際使用中發現,只有一個這樣的腳本還不夠。c#
LuaBehaviour驅動XxxPanel.lua腳本的方法,只適用於界面相對簡潔的狀況(界面上只有少許的Image、Text和其它UI組件),一但遇到稍微複雜一點的狀況,就有點捉襟見肘了,好比一個包含多個子項的排行榜頁面。架構
現以一個排行榜的示例來講明。 併發
一、建立一個大廳場景,相機及Canvas設置與以前的main場景相同,而後建立一個HallPanel面板。框架
同時建立HallPanel.lua和HallCtrl.lua腳本並作相應註冊(添加到CtrlNames和PanelNames裏並作Require)。ide
面板上放兩個按鈕(排行榜、商城),且這個面板不作成由PanelMgr加載的預製體,就這麼掛在Canvas下好了。函數
二、建立一個排行榜RankingPanel,其結構主要是幾個垂直排序的RankItem,以下圖所示。佈局
同時建立RankingPanel.lua和RankingCtrl.lua並作相應註冊。post
這個面板也不作成由PanelMgr加載的那種,就放在Canvas下,經過SetActive來控制顯示與隱藏(開發中這種使用方式應該也很常見)。測試
三、功能需求:
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腳本,怎麼讓它們產生綁定關係?
下面來嘗試解決問題。
方法1:使用LuaBehaviour腳本
一、直接給HallPall對象添加LuaBehaviour腳本;
二、在Game.lua中把初始自動加載Panel的語句註釋掉。
CtrlManager.Init(); local ctrl = CtrlManager.GetCtrl(CtrlNames.Login); if ctrl ~= nil and AppConst.ExampleMode == 1 then -- ctrl:Awake(); --就是這一句決定首先加載什麼面板 end三、給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方法的調用。
四、運行遊戲
點擊運行後,發現,InitPanel方法中的日誌語句沒有輸出,點擊按鈕也沒有響應。
經跟蹤調試發現,在處理HallPanel面板時,其身上的LuaBehaviour腳本中Awake方法的執行時,Lua虛擬機的初始化還沒完成,甚至是在執行Start方法時其初始化也沒初始化完成。
因此,從LuaBehaviour的Awake中調用HallPanel.lua腳本的Awake是不可能成功的(Lua虛擬機沒初始化完成,全部Lua腳本也沒被加載)。
LuaBehaviour腳本自己沒問題,這個問題的出現,是由於咱們想繞過LuaFramework的加載流程引發的。
五、解決問題
想解決這個問題,就須要修改 Awake方法的調用時機。
爲了避免破壞原有的LuaBehaviour腳本,咱們複製一個LuaBehaviour腳本並重命名爲"CustomBehaviour"。
並在CustomBehaviour的Awake的0.1秒以後,再調用HallPanel.lua的Awake方法,見下圖:
從新給HallPanel對象掛載CustomBehaviour腳本後,再運行遊戲,
能看到InitPanel方法被正確執行了,按鈕事件也生效了。
說明:用延時的方法去執行Awake,雖然讓Lua中的方法執行了,但也破壞了Awake的本來執行順序。若是對框架了解不深或遊戲邏輯處理不夠嚴謹,則會引發問題。
這只是一個臨時方法,完善的解決方案能夠看看PanelMgr的加載流程,應該能找到答案。
一、顯示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
二、處理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應該是框架實現的全局函數。
已找到新的啓動HallPanel的方式,放棄使用CustomBehaviour並延遲調用Awake的方法,操做以下:
a)移除HallPanel身上的CustomBehaviour;
b)在Game.lua的OnInitOK方法末尾添加以下語句
--查找HallPanel對象,併發起對HallPanel.Awake的調用 local objHallPanel = UnityEngine.GameObject.Find("Canvas").transform:GetChild(0).gameObject; HallPanel.Awake(objHallPanel);代碼位置見下圖:
c)從新運行unity,點擊排行榜按鈕,效果如前。
至於RankItem.lua和LuaComponent.cs,不存在問題,依然用以前介紹的使用方式。