Ui 設計4

歡迎回到“使用C++和DX開發GUI”的第四部分。接着我們的主題(我是如何爲我未來的遊戲開發GUI的 — Quaternion),本文將關注所有的有關遊戲GUI的細節問題。

4.1、保存窗口
  窗口序列化(存儲和載入窗口)對你的工程而言或許不重要。如果你的遊戲GUI很簡單,你可以全靠程序在遊戲中實現窗口。但如果你的GUI相對複雜,或者隨着開發的過程經常會改變,那麼你會想寫程序以把一個窗口(和所有它的子窗口)存到文件裏,然後再裝載它。對初學者而言,窗口序列化代碼允許你改變遊戲的GUI而不用重新編譯,而且它對於多人協調工作也是有益的。
  我的計劃是從主對話窗口開始,然後遍歷它的所有子窗口,把每一個存到磁盤上。如果我用C語言寫程序的話,我告訴自己的第一句話是“好的,如果我必須保存這些窗口,我需要每個窗口有一個字節來告訴我它是什麼樣的窗口,以使我能正確的載入它。比如1是按鈕,2是列表框,3是圖表,等等。”
  這種問題是C++的RTTI(運行期類型識別)關心的。RTTI提供了兩個東東,type_info類和typeid()函數,兩者允許我查詢一個對象的所屬類名稱,例如gui_window,gui_button等等。我沒有使用枚舉和ID,而是簡單地爲每個將要保存的窗口調用typid(),並“寫下”窗口的類名稱。
  我注意到使用RTTI的對象識別函數來幫助保存窗口的兩個小缺點。第一,RTTI ID是字符串而不是整型,這意味着它們將佔用磁盤上的更多空間(按照Pascal方式存儲字串將是前4個字節代表字串的長度,接下來是字串本身的數據)。第二,如果你改變一個窗口類的名字,你會破壞已經存好的所有窗口文件。
  由於這些原因,你可能不會這樣使用RTTI。畢竟,並不是有技術就一定要使用它。然而,我發現RTTI對我的代碼而言卻是救生員。要獲得更多的關於RTTI和這兩個函數的信息,請在你的聯機幫助文檔裏查找。
  另外,如果你決定在VC++裏使用RTTI,確保你在工程屬性的C/C++欄和C++語言選項中打開它。

4.2、載入窗口
  載入窗口比存儲他們要難一點兒,這主要是因爲你必須新建(new)每個窗口,載入,並當它不再需要的時候刪除。
  這是個遞歸函數,用PDL表達的話如下所示:

  void gui_window:load(int filehandle)
  {
    // read window properties (colorsets, etc.)
    // read total number of children for this window
    // for each child?
      // read window ID from disk
      // new a gui_window derivative based on that ID
      // tell the newly created window to load itself (recurse!)
    // next child
  }

  換句話說,你得像你所想得那樣從磁盤載入窗口。第一,要處理基類窗口:讀取他的屬性。然後,讀取基類窗口的所有子窗口的數目。對每個子窗口,讀取ID字節,根據ID新建一個窗口,然後讓新窗口載入自己(低軌到它)。當所有的字窗口載入完畢時,就結束了。
  當然,你的文件結構一致也非常重要。確保你的存儲代碼以你想要載入的順序存儲信息。

4.3、資源編輯器
  要想真正使你的GUI大放光彩,你必須有一個資源編輯器。當然你不需要做個象開發環境提供的資源編輯器那樣的華麗和強大,但是你起碼得有個基本的程序來完成加入,編輯,刪除,排列,免除你爲對話框的每個控件計算虛擬座標位置的麻煩事兒。
  寫一個功能完善的,所見即所得(WYSIWYG)的資源編輯器超出了本文的範圍,但是我能給你一些小提示來幫助你完成這個壯舉:

共享你的代碼。特別地,讓你的資源編輯器和你的遊戲一起分享同樣的渲染代碼。這樣,你就得到了所見即所得支持,並且免除了開發兩套GUI代碼的麻煩,我向你保證,調整你的DirectX代碼以使它渲染一個GDI表面而不是一個雙緩衝系統將比重新開發一整套新的繪製代碼簡單。記住過一段時間之後你的GUI系統可能會改變,你不會想要經常在兩個不同的地方改寫代碼。


不要試圖模仿開發環境的外表和感覺。換句話說,不要費時間模仿開發環境的細節(例如,屬性頁和預覽窗口)。如果你的編輯器相對而言比較難看的話,不要沮喪;的確,小組的效率是和他使用的工具的效能成直接正比關係的,但是同時,小組之外的人是不可能使用你的資源編輯器的,你也不會用它去開發一個完整的GUI程序;你不過只是做幾個對話框而已。你不需要環境文本幫助(context sensitive help)。你不需要環境文本菜單(context menus),除非你覺得這會簡化一個特定的繁複的操作。如果你的資源編輯器不那麼漂亮也無所謂,只要它能完成工作就行了。


強調數據完整而不是速度。資源編輯器是個數據整合者,而不是個高性能程序,沒有什麼比你花了一個小時來設計的東西由於程序錯誤而丟失而更讓人惱火的事兒了。當寫你的GUI的時候,保存數據是你的最高目標。花些時間來做自動存儲,釋放緩衝區(autosaves, undo buffers)等等,別把優化看得那麼重要。
4.4、生成子類(subclass)
  那些熟悉Win32處理窗口的人們可能已經知道“subclass”這個術語的含義了。如果不知道的話,當你“subclass”一個窗口的時候,你就“衍生(derive)”一個新的窗口類型,然後把新的窗口類型嵌入到舊窗口要使用的地方。
  讓我做更詳細地解釋。比如我們需要一個超級列表框。我們已經有個普通的列表框類,但是因爲某種原因它不適合;我們的遊戲需要超級列表框。所以我們從普通的列表框類中衍生出一個超級列表框類。就是這樣。
  但是我們如何在我們的遊戲對話框中放置這個超級列表框呢?由於超級列表框是爲我們的程序特製的,我們不能爲我們的資源編輯器增加函數來支持它。但同時,我們怎樣通知GUI系統爲這個特殊的實例(我們的遊戲),讓所有的列表框都是超級列表框呢?這就是生成子類(subclass)要做的事情。這不是個精確的技術定義,但是表達了足夠的信息。
  這裏我要講述的方法稱作“載入過程生成子類(subclassing at load time)”。要理解它,讓我們從上一節所述的基本的載入代碼開始。我們有個載入函數,它第歸地完成創建,載入,增加窗口。這裏我們用PDL表述如下:

  // read total number of children for this window
  // for each child...
    // read window ID from disk
    // new a gui_window derivative based on that ID
    // ...
  // next child

  要完成生成子類,我讓我的窗口載入函數“給程序一個創建這一類型窗口的機會“,像這樣:

  // read total number of children for this window
  // for each child...
    // read window ID from disk
    // give application a chance to create a window of this type
    // if the application didn't create a window,
      // then new a gui_window derivative based on the ID
    // else, use the application's created window
    // ...
  // next child

  我通過一個函數指針給程序這個機會。如果程序需要爲一個窗口生成子類,就在函數指針裏填上自己函數的地址。當窗口載入的過程中,調用這個函數,傳入想要創建的窗口ID。如果程序想要根據ID爲一個窗口生成子類,新建一個適當的對象並把新指針返回給窗口。如果程序不需要這個ID,則返回NULL,窗口函數根據返回值創建恰當的默認對象。這種方法允許程序“預先過濾”引入的窗口ID信息,併爲特定的窗口類型重載默認函數。太完美了(譯者:這一段我翻譯得實在不能說是完美,相反是簡直不知所云,這裏把原文貼出來,請您自己斟酌吧)。
  Specifically, I give the application this chance by way of a function pointer. If the application needs to subclass a window, it fills in the function pointer with the address of its own function. When the windows are loading, they call this application function, passing in the ID of the window they want to create. If the application wants to subclass a window from this ID, it news up the appropriate object and returns the new pointer back to the window. If the app doesn't want to do anything special for this ID, it returns NULL, and the window function senses this and news up the appropriate default object. This method allows the app to "pre-filter" the incoming window ID bytes, and to override the default behavior for certain window types. Perfect!

  用這種方法在創建自定控件的時候給了我很大的自由。我爲我的資源編輯器增加代碼以使我能爲每個存儲的窗口改變ID。然後,當我需要自定控間的時候,我只需用資源編輯器改變保存這個窗口ID的字節。在磁盤上保存的是ID和所有爲自定控件的其他基類屬性。
  很快吧?還有其他的方法來做同樣的事,這是在模仿STL需要創建對象時使用的方法。STL使用特定的“allocator(分配符)”類,這有點兒像“類廠”(factories),它們按照客戶告訴他們的需要來創建類。你可以使用這種方法來創建窗口。
  這種方法的工作原理如下:創建一個類並把它叫做“gui_window_allocator”。寫一個虛擬函數,叫做CreateWindowOfType,它接受一個給定的窗口ID並傳出一個新的指針給窗口。現在你就得到了一個簡單的分配符類,你的窗口載入代碼將使用它來新建需要的窗口。
  現在,當你的程序需要爲窗口重載“new”操作符,衍生一個新的、程序相關的gui_window_allocator類,並告訴你的窗口載入代碼來使用這個分配符,而不是默認的那個。這種方法就象提供一個函數指針,只用了一點兒C++。

4.5、加速GUI渲染
  還有一個能幫助你加速GUI渲染的小提示。
  關鍵的概念是,就象其它繪製函數的優化一樣,不要畫你不需要的東西。默認方式,GUI花很多時間繪製沒有變化的部分。然而,你能通過告訴GUI繪製改變的窗口(windows that are dirty)作些優化。當窗口的外觀需要改變的時候,窗口設定它們的dirty標誌,在繪製的時候清除它們的dirty標誌。
  由於我們的GUI控件可能是透明的,當一個控件被標記爲dirty,它的父窗口必須也被標記爲dirty。這樣,當它被繪製的時候,背景沒有改變,因爲父窗口也剛剛被重繪。

4.6、總結:相逢在XGDC
  呼,好長的4篇文章,然而我們還是遺漏了很多。
  我準備在即將來臨的Armageddon XGDC(eXtreme Game Developer's Conference)上講述GUI。我將盡力讓我的發言有用,現在你已經看了所有的文章,如果你有什麼需要擴展的問題,或是我有什麼遺漏,請給我寫信讓我瞭解。祝各位做遊戲愉快。

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章