Unity3D熱更新之LuaFramework篇[07]--怎麼讓unity對象綁定Lua腳本

前言

在上一篇文章 Unity3D熱更新之LuaFramework篇[06]--Lua中是怎麼實現腳本生命週期的 中,我分析了由LuaBehaviour來實現lua腳本生命週期的方法。html

但在實際使用中發現,只有一個這樣的腳本還不夠。c#

LuaBehaviour驅動XxxPanel.lua腳本的方法,只適用於界面相對簡潔的狀況(界面上只有少許的Image、Text和其它UI組件),一但遇到稍微複雜一點的狀況,就有點捉襟見肘了,好比一個包含多個子項的排行榜頁面。架構

現以一個排行榜的示例來講明。 併發

1、建立一個排行榜頁面

一、建立一個大廳場景,相機及Canvas設置與以前的main場景相同,而後建立一個HallPanel面板。框架

同時建立HallPanel.lua和HallCtrl.lua腳本並作相應註冊(添加到CtrlNames和PanelNames裏並作Require)。ide

面板上放兩個按鈕(排行榜、商城),且這個面板不作成由PanelMgr加載的預製體,就這麼掛在Canvas下好了。函數

 

二、建立一個排行榜RankingPanel,其結構主要是幾個垂直排序的RankItem,以下圖所示。佈局

同時建立RankingPanel.lua和RankingCtrl.lua並作相應註冊。post

這個面板也不作成由PanelMgr加載的那種,就放在Canvas下,經過SetActive來控制顯示與隱藏(開發中這種使用方式應該也很常見)。測試

 

三、功能需求:

  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腳本,怎麼讓它們產生綁定關係?

   下面來嘗試解決問題。

 

2、處理HallPanel的UI事件

     方法1:使用LuaBehaviour腳本

     一、直接給HallPall對象添加LuaBehaviour腳本;

  二、在Game.lua中把初始自動加載Panel的語句註釋掉。

    CtrlManager.Init();
    local ctrl = CtrlManager.GetCtrl(CtrlNames.Login);
    if ctrl ~= nil and AppConst.ExampleMode == 1 then
        -- ctrl:Awake(); --就是這一句決定首先加載什麼面板
    end

 三、給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方法的調用。

 四、運行遊戲

   點擊運行後,發現,InitPanel方法中的日誌語句沒有輸出,點擊按鈕也沒有響應。

   經跟蹤調試發現,在處理HallPanel面板時,其身上的LuaBehaviour腳本中Awake方法的執行時,Lua虛擬機的初始化還沒完成,甚至是在執行Start方法時其初始化也沒初始化完成。

    因此,從LuaBehaviour的Awake中調用HallPanel.lua腳本的Awake是不可能成功的(Lua虛擬機沒初始化完成,全部Lua腳本也沒被加載)。

    LuaBehaviour腳本自己沒問題,這個問題的出現,是由於咱們想繞過LuaFramework的加載流程引發的。

   五、解決問題

    想解決這個問題,就須要修改 Awake方法的調用時機。

    爲了避免破壞原有的LuaBehaviour腳本,咱們複製一個LuaBehaviour腳本並重命名爲"CustomBehaviour"。

    並在CustomBehaviour的Awake的0.1秒以後,再調用HallPanel.lua的Awake方法,見下圖:

    從新給HallPanel對象掛載CustomBehaviour腳本後,再運行遊戲,

能看到InitPanel方法被正確執行了,按鈕事件也生效了。

 

說明:用延時的方法去執行Awake,雖然讓Lua中的方法執行了,但也破壞了Awake的本來執行順序。若是對框架了解不深或遊戲邏輯處理不夠嚴謹,則會引發問題。

這只是一個臨時方法,完善的解決方案能夠看看PanelMgr的加載流程,應該能找到答案。

 

 3、顯示RankingPanel面板並處理RankItem子項

 一、顯示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

 

二、處理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,不存在問題,依然用以前介紹的使用方式。

相關文章
相關標籤/搜索