在這篇文章裡,將以國外一個非常著名的休閒遊戲 Diner Dash 2 的實例,探討 Lua 的應用功能之一:GUI 系統。自前篇「Scripting系統概論與Lua簡介」對 Lua 的 What(是什麼)與 Why(為什麼使用)有了初步的認識之後,接下來的重點就是瞭解 Lua 的 How(如何使用)。
在進入 How 的主題之前,先補充一下 Where(在何處使用)的概念。Lua 的能力十分強大,似乎無所不能,可以完成任何的任務與功能。但是就實際應用層面的考量來說,哪些功能與系統才是真正適合使用 Lua 的部分呢?又要如何區分 C++ 語言與 Lua 語言所能達成的任務呢?
以兩者擅長處理的事項來分類:
- C++:低階核心、記憶體管理、資源管理、繪圖底層、數學運算、訊息發送。
- Lua:使用者介面、人工智慧、資料管理、遊戲邏輯、動態行為。
複雜的運算與低階的底層核心,適合使用 C++;而因應設計需求,經常會變動的各種功能系統,例如使用者介面、人工智慧、遊戲邏輯等等就適合使用 Lua 來完成。另外,有安全性考量的功能層面,也應該使用 C++ 撰寫比較合適。
在 Diner Dash 2 這款遊戲中的圖形化使用者介面,也就是常聽到的 GUI(或簡稱為 UI)系統,都是以 Lua 進行開發設計的。首先,需要瞭解一點基本的 GUI 開發概念。在一個完整的 GUI 系統中,需要許多的基本元件,例如常見的按鈕 (Button)、文字 (Text)、圖片 (Picture)、編輯輸入 (Edit)、進度條 (Progress Bar) 等等,這些物件稱為介面元件 (UI Widget);藉由這些基礎的元件,就能夠組合出遊戲中所使用的各種 UI。而將個別的元件集合起來形成一個群組的容器,常稱做框架 (Frame) 或圖層 (Layer)。最後,在架構頂層做為上述這些 UI Widget 的管理者,包含住一至多個 Frame,負責高階功能與介面的元件,則稱做視窗 (Window) 或是對話盒 (Dialog)。
一般常見的 GUI 系統實做方法,是在 C++ 中將各個元件封裝成 Class,例如 CText、CButton、CPicture 等等,加上 CFrame 與 CDialog 類別,分別負責各自的功能,然後再以這些類別創建出來的物件,組合成一個一個的完整介面。這樣的實做方法,在物件導向程式的領域中是很直覺的設計模式,但是也存在著不少缺點;像是每個元件都需要存在個別獨立的 Class,而若要修改或新增元件的功能,即使只是很微小的部分,也必須在 C++ 中撰寫完畢後,重新建置整個程式專案完成後才能夠使用。另外,在處理資料的讀取程序中,也會有相當瑣碎且重複性極高的修改步驟;只要更動了資料的讀取順序或資料型態,同樣非得經過重新建置的程序不可。
Lua,可以讓一切變得不同。以下,開始進入遊戲的實例說明:(閱讀以下內容,需具備基本的 Lua 程式設計能力)
先來看一段使用 Lua 製作 Text 元件的簡短程式碼:
- Text
- {
- font = DialogTitleFont, -- 使用 DialogTitleFont 字型
- name = "oktitle", -- 元件名稱為 oktitle
- x = 12, y = 8, -- 元件的位置
- w = kMax,h = 40, -- 元件的長度與寬度
- flags = kVAlignCenter + kHAlignLeft, -- 對齊用的旗標
- label = gDialogTable.title, -- 顯示的文字
- };
上述這段程式碼,即定義出一個 Text 文字項元件的種種規格;如註解所示,這個元件使用 DialogTitleFont 這個字型,在指定的位置處顯示出 gDialogTable.title 變數的內容。而 DialogTitleFont 這個字型,定義在其他的程式碼中:
- DialogTitleFont = {
- standardFont, -- 又是另一個變數
- 18, -- 字級大小
- BorderColor -- 字型顏色
- };
- standardFont = "fonts/mercurius.mvec"; -- 字型使用的實體檔案
- BorderColor = Color(30, 42, 102, 255); -- 顏色
由以上的程式碼片段可以瞭解,UI 元件的建構,是利用 Lua Function 能夠接受 Table 結構做為函式參數的強大特徵,將所需的參數傳入 Function 中進行處理。如果是沒有特別指定的參數,就直接使用預設值去建立,能夠毫不費力地建構出 Default Parameters 的功能,使得 UI 元件的建構方式變得非常具有彈性與可擴充性。
再來看一個製作 Button 元件的例子:
- Button
- {
- x = kCenter,
- y = 500,
- font = StandardButtonFont,
- graphics = StandardButtonGraphics,
- name = "back",
- type = kPush,
- flags = kHAlignCenter + kVAlignCenter,
- label = "back",
- command =
- function()
- PopModal();
- end
- };
- StandardButtonGraphics = {
- "buttons/dialog_button_a1",
- "buttons/dialog_button_a2",
- "buttons/dialog_button_a3"
- };
在 StandardButtonGraphics 這個 Table 裡,定義了 Button 在 Normal、Pushed、Disabled 三個狀態下顯示的圖片。接下來的 type 變數,指定這個 Button 的型態是一般常見的 Push Button 類型;其他還包括了 Radio Button 與 Check Button 類型。而 command 變數,是使用者按下 Button 時所需執行的程序,也就是去呼叫 PopModal() 這個函式。除了一般按下 Button 所觸發的執行程序之外,也能夠很輕易地實做出滑鼠進入 Button 範圍的 Function,或是 Button 切換至其他狀態下所應該觸發的 Function。其他的各種元件也以同樣的方法製作,就能夠迅速而便利地建立起一組 UI 元件與完整的使用者介面。
依照以上 standardFont、StandardButtonGraphics 與 BorderColor 變數的定義方法,可以將所有 UI 相關的格式設定,以及與實體檔案相關的變數,全部定義在另外的 Lua 檔案中,便於集中管理、修改與使用。其中的程式碼片段如下:
- -- File: style.lua
- -- 字型的實體檔案
- standardFont = "fonts/mercurius.mvec";
- -- 定義各種顏色
- BlackColor = Color(0, 0, 0, 255);
- BlueColor = Color(16, 225, 226, 255);
- YellowColor = Color(255, 255, 0, 255);
- -- 標準按鈕用的圖片
- StandardButtonGraphics = {
- "buttons/dialog_button_a1",
- "buttons/dialog_button_a2",
- "buttons/dialog_button_a3"
- };
- -- 小型按鈕用的圖片
- SmallButtonGraphics = {
- "buttons/dialog_button_a_small_1",
- "buttons/dialog_button_a_small_2",
- "buttons/dialog_button_a_small_3"
- };
- -- 大型按鈕用的圖片
- LargeButtonGraphics = {
- "buttons/dialog_button_a_large_1",
- "buttons/dialog_button_a_large_2",
- "buttons/dialog_button_a_large_3"
- };
最後,以遊戲中的一個 Lua 檔案為例,列出建構起一個 Dialog 所需的完整程式碼:
- -- File: ok.lua
- require( "scripts/style.lua" );
- MakeDialog
- {
- Bitmap
- {
- name = "yesnobackground",
- image = "backgrounds/popup",
- x = kCenter,
- y = kCenter,
- Button
- {
- font = StandardButtonFont,
- graphics = StandardButtonGraphics,
- close = true,
- flags = 5,
- label = "ok",
- name = "ok",
- default = true,
- x = kCenter,
- y = 250,
- },
- Text
- {
- font = DialogTitleFont,
- name = "oktitle",
- x = 12, y = 8,
- w = kMax, h = 40,
- flags = kVAlignCenter + kHAlignLeft,
- label = gDialogTable.title,
- };
- Text
- {
- font = DialogBodyFont,
- name = "okbody",
- x = 20, y = 46,
- w = kMax-20, h = kMax-70,
- flags = kVAlignCenter + kHAlignCenter,
- label = gDialogTable.body,
- };
- },
- }
以上這段 Lua 程式碼,製作出由一個 Bitmap、一個 Button 以及兩個 Text 元件所組成的 Dialog 介面。撰寫完成後,在遊戲中看到這個 Dialog 所呈現出來的結構,如圖所示:
Bitmap 元件負責顯示整個 Dialog 的背景圖片;第一個 Text 用來顯示 Dialog 的標題列文字,第二個 Text 則顯示 Dialog 中的主要說明文字;而最後的 Button,就只是用來執行簡單的確認行為。所有的元件建構完成後,就當作 Function 的 Table 參數傳入 MakeDialog() 中創建出 Dialog。
以上文章所提的內容,其實只佔了整個 GUI 系統其中一半的部分:資料描述。也就是將 Lua 當成一種 Data Description Language,用來儲存與讀取遊戲所需的資料。藉由 Lua 的強大威力,能夠讓開發者輕易地進行各種資料寫入與讀取的動作,完全不必費力撰寫繁複瑣碎的 Parse 或 Serialize 程序。
資料處理的部分是一半,而另一半是與功能相關的部分,也就是真正在內部進行GUI 相關行為處理的程序。將資料傳入 Text()、Button()、Picture() 這些函式中之後,裡面到底做了些什麼?如何建構 GUI 系統的資料結構?如何和原來的 C++ 端主程式相結合?如何傳遞滑鼠與鍵盤的輸入訊息?如何真正畫出這些 UI?
下篇待續。