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

本文深入探讨了Lua-basedGUI系统的架构与实现细节,包括GUI核心功能层面对比,不同架构方式的实现,以及游戏实例中的GUIScript应用。通过详细解析,展示了如何在C++端实现唯一GuiManager类作为界面与游戏引擎沟通的桥梁,并在游戏主循环中进行GUI系统相关操作的处理。同时,介绍了如何在Lua端构建UIFrame与Widget的层级结构,以及实现各种事件响应与状态切换的处理过程。最后,讨论了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++ 的實做方法?不論是任何意見都歡迎提出討論喔~

我这个dockerfile 创建的docker 里面为什么会有一个gcc FROM ubuntu:18.04 LABEL Authors="Kun Liu <liukun@tp-link.com.hk> " LABEL Description="Broadcom SDK Build based on 18.04 LTS" Version="1.0" ENV STAGINGDIR="" ENV CONFIGDIR="" ENV PACKAGEDIR="" RUN echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections RUN apt-get clean && \ apt-get update && \ apt-get install --yes \ --no-install-recommends \ --no-install-suggests \ build-essential net-tools inetutils-ping \ libncurses5-dev libdigest-crc-perl libpopt-dev xxd \ liblzo2-dev zlib1g-dev lib32z1 \ libssl-dev libglib2.0-dev libreadline-dev \ libjson-c-dev libxml2-dev \ libsqlite3-dev sqlite3 \ libuv1-dev libxslt1-dev libfcgi-dev \ libevent-dev libyajl-dev liburiparser-dev libwebsockets-dev \ locales lua5.1 liblua5.1-dev \ python python3 python3-dev python3-pip \ git git-lfs \ subversion \ autoconf automake \ pkg-config ccache \ cmake ninja-build \ clang-tools-6.0 \ bison flex gettext \ ca-certificates \ libtool patch vim \ unzip uuid-dev \ wget curl cpio \ bc gawk \ gdb valgrind diffstat \ chrpath texinfo \ ssh openssh-client openssh-server netbase \ quilt protobuf-c-compiler dropbear-bin openvswitch-switch \ tmux dirmngr lighttpd alien \ mlocate iproute2 dos2unix \ lsb-release \ rsync \ gdisk \ man \ pax \ moreutils \ python3-dev \ python3-setuptools \ sudo RUN curl -fsSL https://deb.nodesource.com/setup_16.x | bash - && \ apt-get install -y --no-install-recommends nodejs # ← 兼容修改:Ubuntu 18.04 使用 clang-tools-6.0 而非 clang-tools # ← 兼容修改:liblua5.3-dev 在 bionic 中版本太旧,默认不安装 # ← 兼容修改:python/pip 默认 Python 3.6 # Enable 32-bit program execution on 64bit system RUN dpkg --add-architecture i386 && \ apt-get update && \ apt-get install --yes \ --no-install-recommends \ --no-install-suggests \ libc6:i386 libncurses5:i386 libstdc++6:i386 libc6-dev:i386 \ libselinux1:i386 gcc-multilib g++-multilib # Install Runtime debugging tools and Java environment RUN apt-get update && \ apt-get install --yes \ --no-install-recommends \ --no-install-suggests \ strace openjdk-8-jdk-headless # Add BRCM cross-tc gcc10 dependency and openwrt cmake fixing RUN rm /bin/sh && ln -sf bash /bin/sh && \ ln -s /usr/bin/perl /usr/local/bin/perl && \ ln -sf /usr/lib/x86_64-linux-gnu/libreadline.so /usr/lib/x86_64-linux-gnu/libreadline.so.7 # ← 兼容修改:bionic 的 libreadline.so 是 .so.7,而非 .so.6 ENV JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64 ENV TERM xterm-256color ENV LANG=C.UTF-8 ENV LD_LIBRARY_PATH /usr/local/lib # COPY fs_resources/ / # RUN chmod 644 /etc/sudoers RUN pip3 install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple \ gdbgui==0.13.2.0 \ kconfiglib pydot jinja2 pyyaml #RUN pip3 install --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple gdbgui kconfiglib pydot jinja2 pyyaml RUN cd ~ RUN apt-get -q -y autoremove && \ rm -rf /var/lib/apt/lists/* /var/tmp/* /tmp/* #RUN curl https://storage.googleapis.com/git-repo-downloads/repo > ~/repo && \ # chmod a+x ~/repo && chown 0:0 ~/repo && mv ~/repo /usr/bin/repo RUN apt-get update && \ apt-get install -y software-properties-common gnupg wget && \ wget -q -O - https://apt.kitware.com/keys/kitware-archive-latest.asc | gpg --dearmor - > /etc/apt/trusted.gpg.d/kitware.gpg && \ apt-add-repository "deb https://apt.kitware.com/ubuntu/ bionic main" && \ apt-get update && \ apt-get install -y cmake # 验证CMake版本 RUN cmake --version RUN git clone https://git.openwrt.org/project/libubox.git && \ git clone https://git.openwrt.org/project/ubus.git && \ cd libubox && cmake . && make && make install && \ cd ../ubus && cmake . && make && make install && \ cd .. && rm -rf libubox ubus && mkdir -p /var/run/ubus RUN wget https://github.com/warmcat/libwebsockets/archive/refs/tags/v4.2.2.tar.gz -O lws.tar.gz && \ tar -xzf lws.tar.gz && \ cd libwebsockets-4.2.2 && \ mkdir build && cd build && \ cmake .. -DLWS_WITH_LIBUV=1 && \ make -j$(nproc) && make install && \ cd ../.. && \ rm -rf lws.tar.gz libwebsockets-4.2.2 #RUN git clone https://github.com/warmcat/libwebsockets.git && \ #cd libwebsockets && git checkout v4.2.2 && mkdir build && cd build && \ #cmake .. -DLWS_WITH_LIBUV=1 && make && make install && \ #cd ../../ && rm -rf libwebsockets RUN wget https://github.com/c-ares/c-ares/releases/download/cares-1_17_2/c-ares-1.17.2.tar.gz && \ tar -xzf c-ares-1.17.2.tar.gz && \ cd c-ares-1.17.2 && \ ./configure --prefix=/usr --disable-static && \ make && make install && \ cd .. && \ rm -rf c-ares-1.17.2 c-ares-1.17.2.tar.gz #RUN git clone https://github.com/c-ares/c-ares.git && \ # cd c-ares && git checkout cares-1_17_2 && \ # ./buildconf && ./configure --prefix=/usr --disable-static && \ # make && make install && cd ../ && rm -rf c-ares # To build prplos # RUN curl -sL https://deb.nodesource.com/setup_14.x | bash - && \ # apt-get update && apt-get install --yes nodejs && \ # npm install -g ember-cli # ← 兼容修改:bionic 对 Node.js 16 支持差,改为 Node.js 14 RUN mkdir /data #CMD ["/usr/local/bin/init.sh"]
最新发布
11-08
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值