Unity3D热更新之LuaFramework篇[07]--怎么让unity对象绑定Lua脚本
分类:
IT文章
•
2022-05-15 08:51:37
前言
在上一篇文章 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(); --就是这一句决定首先加载什么面板
end
3、给HallPanel的InitPanel方法添加查找按钮控件的语句,并在HallCtrl中添加按钮事件,具体修改见代码:
local 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
HallPanel.lua
HallCtrl = {};
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
HallCtrl.lua
有一点需要注意的是,之前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
local 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
View Code
完整的HallCtrl.lua
HallCtrl = {};
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
View Code
2、处理RankItem
思路: 我们的目标是让RankItem具有独立处理逻辑的能力(包括生命周期函数的执行),想到的第一个办法就是继续使用上边讲到的CustomBehaviour脚本。
CustomBehaviour适用于面板加载,且每个面板要对应一个XxxPanel.lua和XxxCtrl.lua,并且还要注册,用起来有点不方便。所在决定重新创建一个C#脚本,以处理各种Item类型的Unity对象(如RankItem,ShopItem等)与Lua的绑定关系。
考虑到RankItem可能是动态创建的,所以这个脚本应该有绑定unity对象与Lua脚本对象的能力。
步骤:
1)创建一个LuaComponent脚本
将这个脚本放在 “AssetsLuaFrameworkScriptsUtility”下,这个脚本包含将GameObjet与LuaTable进行绑定的Add方法以及调用Lua脚本生命周期函数的方法。见下图
LuaCompnent.cs的完整代码:
/*
* 让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);
}
}
}
}
View Code
这个脚本的写法参考了知乎上 罗培羽 大佬的一篇文章 :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);
end
RankItem.lua的完整代码在这里:
RankItem = {
--里面可以放一些属性
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
View Code
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代码在这里:
local 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---->>>");
end
View Code
4)运行
运行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,不存在问题,依然用之前介绍的使用方式。