Unity3D热更新之LuaFramework篇[07]--怎么让unity对象绑定Lua脚本
前言
在上一篇文章 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应该是框架实现的全局函数。
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,不存在问题,依然用之前介绍的使用方式。