zoukankan      html  css  js  c++  java
  • 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脚本

         将这个脚本放在 “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,不存在问题,依然用之前介绍的使用方式。

  • 相关阅读:
    ETF上线技术要素
    oracle修改用户的schema
    list
    交易系统分类OMS/EMS
    类的大小2
    webpack5教程
    vue配置stylelint教程
    提高国内访问 GitHub 的速度的 9 种方案
    git常见的操作
    img 图像底部留白的原因以及解决方法
  • 原文地址:https://www.cnblogs.com/imteach/p/11220029.html
Copyright © 2011-2022 走看看