在前篇「使用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;
- }
// @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,
- };
- }
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
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
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
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
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
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
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++ 的實做方法?不論是任何意見都歡迎提出討論喔~