深入Lua-based GUI系統架構與實做細節

cpp-to-lua-architecture在前篇「使用Lua實做GUI系統的遊戲實例」中介紹了 Lua 於 GUI 系統的基本用法後,本文開始進入 GUI 的核心功能層面。

一般來說,有數種不同的架構方式能夠結合 Lua 與 C++ 實做 GUI 系統。其一是將 Lua Script 當作純粹資料描述用的程式碼,僅儲存 UI Layout 相關的資料(如前篇文章所示),而由 C++ Code 掌控核心功能並且讀取 Lua Script 進行資料的處理。其二則是於 C++ 端實做出一組完整的 UI Widget 類別,然後再將這組 Widget 的所有函式、甚至所有類別,註冊給 Lua 端自行呼叫使用。

另一種方法則是在 Lua Script 中包含資料描述以及核心功能,將 GUI 系統的全部相關功能全權交由 Lua 端處理。而 C++ 端程式,則負責發送鍵盤與滑鼠的輸入訊息給 Lua 端程式,供 GUI 系統判斷各種 UI 事件。最後再將測試的結果,傳回給 C++ 端程式進行後續判斷與處理。本文將使用這一種架構來實做 Lua-based GUI 系統

使用上述的架構,在 C++ 端只需要實現唯一一個類別:GuiManager,做為 Facade 介面與遊戲引擎的其他系統溝通。然後在遊戲主迴圈的更新程序中,呼叫 GuiManager::Update() 函式進行 GUI 系統相關的更新程序;而在遊戲主迴圈的繪圖程序中,呼叫 GuiManager::Render() 函式將控制權遞交給 Lua 端程式,以進行 GUI 系統的繪圖流程。其他與 GUI 系統相關的操作,例如鍵盤事件與滑鼠事件,同樣是在移動滑鼠或按下鍵盤按鍵時,呼叫相對應的函式,並傳入滑鼠的座標或是按下的按鍵,以供 Lua 端程式進行判斷處理。這裡以 GuiManager 類別的部分程式碼為例:

// @file GuiManager.cpp

void GuiManager::Render() {
    g_ScriptManager->CallFunction("GuiRender", "Gui");
}

bool GuiManager::OnMouseDown(eMouseButton button) {
    bool bHandled = false;

    g_ScriptManager->CallFunction("OnMouseDown", "Gui", button);
    g_ScriptManager->GetReturnValue(bHandled);

    return bHandled;
}

bool GuiManager::OnMouseMove(int x, int y) {
    bool bHandled = false;

    g_ScriptManager->CallFunction("OnMouseMove", "Gui", x, y);
    g_ScriptManager->GetReturnValue(bHandled);

    return bHandled;
}

對整個 Lua-based GUI 系統的架構有了基礎的概念,並且瞭解 GUI 系統的 C++ 端如何運作之後,接著先看看在遊戲中使用 GUI Script 的實例:

Frame
{
    name = "ingame",
    x = 0, y = 0,
    width = 20, height = 20,
    backdrop = "Image/bk.png",

    Button
    {
        name = "ingame_main",
        x = 0, y = 0,
        width = 15, height = 20,
        graphics = StandardButtonGraphics,

        mouse_up = 
            function()
                Gui.ShowFrame("main_menu");
                Core.SetGameState(GAME_PAUSE);
            end,
    };
}

上述這段程式碼,定義了一個名稱為 in_game 的 Frame 元件,位於螢幕座標 (0, 0) 的位置,長度與寬度的大小都是 20 個像素,背景圖片使用 Image/bk.png。然後在這個 UI Frame 中內含了一個 Button 元件,當「放開滑鼠按鍵」的事件產生時,會執行 mouse_up 函式內的程序,顯示出名稱為 main_menu 的 UI Frame,並同時將遊戲的 State 設定為暫停的狀態。

由於這裡是利用「將 Table 當作物件建構參數」的技巧,建立起 UI Frame 與 Widget 的階層架構,所以元件的建立順序為由內而外進行;也就是說,在上述 GUI Script 的實例中,會先呼叫並且執行 Button() 函式後,才會執行 Frame() 函式。

function Button(t)
    local widget = ButtonData:Instance(t);
	
    widget.displaylists["normal"] = CreateWidgetGraphic(widget, "normal");
    widget.displaylists["hover"] = CreateWidgetGraphic(widget, "hover");
    widget.displaylists["pushed"] = CreateWidgetGraphic(widget, "pushed");
    widget.displaylists["disabled"] = CreateWidgetGraphic(widget, "disabled");
    widget.displaylists["current"] = widget.displaylists["normal"];

    table.insert(g_TempWidgets, widget);
end

在 Button() 函式中,先利用 Lua 的物件導向設計能力,具現化出一個 ButtonData 物件。然後使用 CreateWidgetGraphic() 函式,創建 Button 元件在各種狀態中所應顯示的圖片,包括:一般狀態 (Normal)、滑鼠移過 Button 的狀態 (Hover)、滑鼠按下按鍵的狀態 (Pushed),與禁止使用的狀態 (Disabled)。

在 CreateWidgetGraphic() 函式裡,會由 Lua 端程式呼叫 C++ 端程式以建立起 UI Frame 所需的繪圖資源。這裡所使用的是 OpenGL 的 Display List 資源;藉由傳入 Vertex Coordinates、Texture Coordinates與 Texture ID,呼叫 C++ 端的繪圖引擎程式碼,產生出相對應的 Display List,然後再將 ID 回傳給 Lua 以供後續的繪圖程序使用。

將 Button 元件建立完成後,當 C++ 端程式傳來滑鼠事件時,就能夠在 ButtonData:OnMouseMove() 函式中,處理 Button 元件對於滑鼠移動事件的程序:

function ButtonData:OnMouseMove()
    if (self.disabled) then
        return;
    end
	
    if (IsPicked(self)) then
        if (not self.is_mouse_down) then
            SetButtonState(self, "hover");
            if (not self.sound) then
                Audio.Play("../Data/Sound/menu_rollover.ogg");
                self.sound = true;
            end
        end
    else
        SetButtonState(self, "normal");
        self.is_mouse_down = false;
        self.sound = false;
    end
end

在 ButtonData 物件中,還可以定義如 OnMouseDown()、OnMouseUp() 與 OnDisabled() 等函式,以處理各種不同功能作用的事件。

function ButtonData:OnMouseDown()
    if (self.disabled) then
        return;
    end
	
    if (IsPicked(self)) then
        SetButtonState(self, "pushed");
        self.is_mouse_down = true;
        Audio.Play("../Data/Sound/menu_click.ogg");
		
        if (self.mouse_down ~= nil) then
            self:mouse_down();
        end
    end
end

在 Button() 函式的程序處理完成後,如果 Frame 中還有其他的 UI 元件如 Picture 或 Label 等等,也會一一建置處理並將這些 UI Widget 全部插入 g_TempWidgets 中,直到所有內含於 Frame 的 UI Widget 都處理完畢後,最終才會處理 Frame() 函式。

function Frame(t)
    g_GuiFrames[t.name] = {};
	
    local var = g_GuiFrames[t.name];
    var.x = t.x or 0;
    var.y = t.y or 0;
    var.width = t.width or 32;
    var.height = t.height or 32;

    if (t.backdrop ~= nil) then
        var.texture = Graphics.CreateTexture(t.backdrop);
    end
		
    -- Frame display list
    var.displaylist = CreateQuad(var.width, var.height, var.texture)
	
    -- Widgets in temp table
    var.widgets = {};
    for widget in IterateTable(g_TempWidgets) do
        -- Translate widget vertices
        widget.x = widget.x + var.x;
        widget.y = widget.y + var.y;
        table.insert(var.widgets, widget);
    end
end

在 Frame() 函式的處理程序中,首先以 Frame 的名稱做為索引鍵值,將 Frame 物件加入預先定義好的 g_GuiFrames 中後,再依需求創建 Frame 背景圖的 Display List。最後,對之前建立完成插入 g_TempWidgets 中的 UI Widget 一一進行必要的處理。

在遊戲主迴圈進行繪圖程序時,由 C++ 端的 GuiManager 物件呼叫 Lua 端的 GuiRender() 函式:

function GuiRender()
    for frame in IterateTable(g_ActiveFrames) do
        frame:OnRender();

        for key, widget in pairs(frame.widgets) do
          widget:OnRender();
        end
    end
end

在 GuiRender() 函式的程序中,對於目前所有的有效 UI Frame 進行處理:首先交由 Frame 物件本身進行背景繪製與其他程序的處理,然後再將控制權交給 Frame 底下的每個 UI Widget 進行繪圖處理。以 Frame 與 Button 元件的 OnRender() 函式為例:

function FrameData:OnRender()
    Graphics.ApplyTransform2D(self.x, self.y);
    Graphics.DrawDisplayList(self.displaylist);	
    Graphics.RestoreTransform(); 
end

function ButtonData:OnRender()
    Graphics.DrawDisplayList(self.displaylists.current);
end

參考以上的方法與說明,就能夠一步步建立起一個極具彈性與威力的 Lua-based GUI 系統

將整個 GUI 系統建立完成後,更進一步的功能加強與改進,可以考慮使用多執行緒模式,使 GUI 系統在遊戲主迴圈外獨自擁有一個執行緒的資源。這樣就能夠減少遊戲程式的反應時間,即使是在進行漫長的 I/O 程序或複雜的繪圖運算時,玩家也能夠繼續操作部分的 GUI 行為,而不會使遊戲程式顯得好像完全失去反應作用與回應能力一樣。

對以上 Lua-based GUI 系統的架構與實做有什麼看法?有想到能夠改善這個架構的作法或可能性?或者是有其他結合 Lua 與 C++ 的實做方法?不論是任何意見都歡迎提出討論喔~

6 Replies to “深入Lua-based GUI系統架構與實做細節”

  1. 第一種方法我認為用XML來做會比較適合。

    是否第二種方法比較符合腳本語言的本質?

    半路:
    WOW 就是使用 XML 做 layout、Lua 做 function 的架構方法囉。
    使用 XML 的彈性和擴充性都會更好一些~

    要說第二種方法是腳本語言的本質嘛,倒也不盡然如此。
    我覺得只要能夠達到原先設定的目的,不論是什麼樣的架構方法都是好方法囉~ XD

  2. 第二种方法太过动态,在脚本和C++责任划分上太太过于偏向脚本
    导致编写脚本的复杂性

    我认为比较好的方法是让脚本负责配置一些状态,并注册一些回调,

    不要把全部逻辑都放到脚本处理,否则编写这些脚本也是恶梦

  3. @chentan:
    你好,

    如第二種方法將大量程式邏輯交予 Lua 端負責,確實有可能會產生出過於複雜的系統架構。另外一個問題,則在於腳本語言的除錯較傳統語言更為困難,同樣也是區分 C++/Lua 端權責時必須考量的關鍵要點。所以,我也傾向於將腳本語言使用在比較輕量或者獨立的程式模組上。

    謝謝你的回覆。

  4. 感覺lua不錯,之前用xml所不能的簡單邏輯判斷,lua都能做了,而且lua又超小的^^
    不過lua還用來做資料描述比較實用的感覺,如果大量的邏輯都在lua裡面不如用動態連結dll來當腳本了= =
    像網路上有luaplus等加強版,但我覺得還是原版好,加強版大多以性能為代價加了很多腳本不需要的特性。
    最近在用lua練習實作控制遊戲介面,以上為個人淺見

  5. @天亮damody:
    你好,

    你說得沒錯,Lua 的優勢就是短小精悍又具有彈性!使用 Lua 撰寫遊戲邏輯的優點,在於可以節省大量編譯建置程式的時間,更能夠做到許多傳統程式語言無法達成的事情。只是如果寫得越深入越複雜,反而會失去 Lua 的速度優勢,所以在各種 Tradeoff 之間,還是需要視實際應用的情形來做衡量囉。

Leave a Reply