Hello unity3d!

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(); --就是这一句决定首先加载什么面板
    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脚本

     将这个脚本放在 “Assets\LuaFramework\Scripts\Utility”下,这个脚本包含将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,不存在问题,依然用之前介绍的使用方式。

 

posted @ 2019-07-27 15:43  子非鱼`  阅读(3130)  评论(2编辑  收藏  举报
;