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

深入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 類別的部分程式碼為例:

  1. // @file GuiManager.cpp   
  2.   
  3. void GuiManager::Render() {   
  4.     g_ScriptManager->CallFunction("GuiRender""Gui");   
  5. }   
  6.   
  7. bool GuiManager::OnMouseDown(eMouseButton button) {   
  8.     bool bHandled = false;   
  9.   
  10.     g_ScriptManager->CallFunction("OnMouseDown""Gui", button);   
  11.     g_ScriptManager->GetReturnValue(bHandled);   
  12.   
  13.     return bHandled;   
  14. }   
  15.   
  16. bool GuiManager::OnMouseMove(int x, int y) {   
  17.     bool bHandled = false;   
  18.   
  19.     g_ScriptManager->CallFunction("OnMouseMove""Gui", x, y);   
  20.     g_ScriptManager->GetReturnValue(bHandled);   
  21.   
  22.     return bHandled;   
  23. }  
// @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 的實例:

  1. Frame   
  2. {   
  3.     name = "ingame",   
  4.     x = 0, y = 0,   
  5.     width = 20, height = 20,   
  6.     backdrop = "Image/bk.png",   
  7.   
  8.     Button   
  9.     {   
  10.         name = "ingame_main",   
  11.         x = 0, y = 0,   
  12.         width = 15, height = 20,   
  13.         graphics = StandardButtonGraphics,   
  14.   
  15.         mouse_up =    
  16.             function()   
  17.                 Gui.ShowFrame("main_menu");   
  18.                 Core.SetGameState(GAME_PAUSE);   
  19.             end,   
  20.     };   
  21. }  

上述這段程式碼,定義了一個名稱為 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() 函式。

  1. function Button(t)   
  2.     local widget = ButtonData:Instance(t);   
  3.        
  4.     widget.displaylists["normal"] = CreateWidgetGraphic(widget, "normal");   
  5.     widget.displaylists["hover"] = CreateWidgetGraphic(widget, "hover");   
  6.     widget.displaylists["pushed"] = CreateWidgetGraphic(widget, "pushed");   
  7.     widget.displaylists["disabled"] = CreateWidgetGraphic(widget, "disabled");   
  8.     widget.displaylists["current"] = widget.displaylists["normal"];   
  9.   
  10.     table.insert(g_TempWidgets, widget);   
  11. 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 元件對於滑鼠移動事件的程序:

  1. function ButtonData:OnMouseMove()   
  2.     if (self.disabled) then  
  3.         return;   
  4.     end  
  5.        
  6.     if (IsPicked(self)) then  
  7.         if (not self.is_mouse_down) then  
  8.             SetButtonState(self, "hover");   
  9.             if (not self.sound) then  
  10.                 Audio.Play("../Data/Sound/menu_rollover.ogg");   
  11.                 self.sound = true;   
  12.             end  
  13.         end  
  14.     else  
  15.         SetButtonState(self, "normal");   
  16.         self.is_mouse_down = false;   
  17.         self.sound = false;   
  18.     end  
  19. end  

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

  1. function ButtonData:OnMouseDown()   
  2.     if (self.disabled) then  
  3.         return;   
  4.     end  
  5.        
  6.     if (IsPicked(self)) then  
  7.         SetButtonState(self, "pushed");   
  8.         self.is_mouse_down = true;   
  9.         Audio.Play("../Data/Sound/menu_click.ogg");   
  10.            
  11.         if (self.mouse_down ~= nilthen  
  12.             self:mouse_down();   
  13.         end  
  14.     end  
  15. end  

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

  1. function Frame(t)   
  2.     g_GuiFrames[t.name] = {};   
  3.        
  4.     local var = g_GuiFrames[t.name];   
  5.     var.x = t.x or 0;   
  6.     var.y = t.y or 0;   
  7.     var.width = t.width or 32;   
  8.     var.height = t.height or 32;   
  9.   
  10.     if (t.backdrop ~= nilthen  
  11.         var.texture = Graphics.CreateTexture(t.backdrop);   
  12.     end  
  13.            
  14.     -- Frame display list   
  15.     var.displaylist = CreateQuad(var.width, var.height, var.texture)   
  16.        
  17.     -- Widgets in temp table   
  18.     var.widgets = {};   
  19.     for widget in IterateTable(g_TempWidgets) do  
  20.         -- Translate widget vertices   
  21.         widget.x = widget.x + var.x;   
  22.         widget.y = widget.y + var.y;   
  23.         table.insert(var.widgets, widget);   
  24.     end  
  25. end  

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

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

  1. function GuiRender()   
  2.     for frame in IterateTable(g_ActiveFrames) do  
  3.         frame:OnRender();   
  4.   
  5.         for key, widget in pairs(frame.widgets) do  
  6.           widget:OnRender();   
  7.         end  
  8.     end  
  9. end  

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

  1. function FrameData:OnRender()   
  2.     Graphics.ApplyTransform2D(self.x, self.y);   
  3.     Graphics.DrawDisplayList(self.displaylist);    
  4.     Graphics.RestoreTransform();    
  5. end  
  6.   
  7. function ButtonData:OnRender()   
  8.     Graphics.DrawDisplayList(self.displaylists.current);   
  9. end  

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

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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值