前言
在上一篇文章 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---->>>"); end
HallCtrl.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應該是框架實現的全局函數。
2019-07-28更新 :
已找到新的啟動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,不存在問題,依然用之前介紹的使用方式。