Unity3D熱更新之LuaFramework篇[07]--怎麼讓unity物件繫結Lua指令碼

子非魚`發表於2019-07-27

前言

在上一篇文章 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應該是框架實現的全域性函式。

 

相關文章