GMS 中文教程特別篇 #2:GML 編程實踐的經驗與建議
引言
由於青銅的幻想忙於《冰杖秘聞》的開發,本期的特別篇由 indienova 整理自 Mark Alexander 撰寫的官方的一篇技術博客,並在其基礎上做了適當的內容增刪。
教程定位依然是面向初學者,但風格方面可能會略微有所變化,可以步步遵循的內容會適當減少,但會就一個細節問題會展開談更多。另外由於編者對 GMS 的使用經驗並不能算作非常豐富,今後教程中很多知識點主要會來自官方文檔和一些社區指南,部分內容可能需要在學習過程中進行進一步的驗證,囿於水平問題,一些錯漏可能也難以避免,還望大家不吝指出,幫助這個教程做得更好,有什麼內容方向上的建議,也歡迎在教程中指出,有意願參與本系列教程編撰的同學也歡迎私信報名。
未來幾期筆者打算介紹一些文本和UI相關的話題,為了讓之後計劃的教程內容可以順利展開,本文我們就先來聊聊使用 GameMaker:Studio 中的腳本語言 GML 的編程最佳實踐問題,同時藉機一窺這款引擎的某些內部機制。
為順利理解本文中的所有內容,最好先實踐下本系列之前的章節,並大致瀏覽過官方的文檔。
儘管本文主要針對 GML 來進行討論,但是一般原理均有相通之處,希望對其他遊戲編程的初學者來說也能提供一些幫助。
兩條基本建議
在正式展開本文的主題前,我們先強調兩個比較重要的內容:
- 這篇指南絕非意在提供遊戲開發的全能百科。文中內容主要從遊戲開發的整體組織或細微優化的角度談起,並強調了一些在遊戲編程中應當養成的優秀習慣(尤其是當你逐漸對 GML 得心應手,開始思考如何更好地編寫代碼的時候,非常適合詳細參考本文的一些觀點)。
- 另一個重點是:遊戲開發切忌畫蛇添足。如果你的遊戲運行良好,而且沒什麼讓你特別不滿意的地方,而優化方案又要求你改動大量的程序和設計,那麼,不要優化,不要優化,不要優化!如果只是為了一點點多出來的 fps 或者其他提升,一般並不值得冒這樣大的風險。遊戲編程需要平衡多個方面:對可讀性,靈活性,模塊化組織等代碼質量的追求需要同開發時間、精力與成本相權衡,對完美的過分追求有時反而會令人難以及時地達到理想目標。一言以蔽之,對告一段落的項目,如果沒有到崩壞的地步,就不要投入大量精力去優化它,請帶著學到的經驗教訓全心投入到新作的開發中去。
我們接下來聊聊編寫儘可能好的代碼需要注意哪些方面:
代碼風格
任何寫代碼的人都會逐漸養成屬於自己的風格。一般來說,代碼風格包括你放置括弧的方式,你如何進行代碼縮進,你如何聲明變數……等等看似非常細枝末節的問題,總的來說,代碼風格和其最終的可讀性與清晰性相關。代碼風格的標準化和統一化對多人協作極具意義:如果代碼風格成熟而良好,會給其他人或者擱置項目一段時候後的自己在閱讀時提供很多方便。
代碼風格多種多樣。通常來說,一門語言的社區會出現一種到幾種比較廣泛接受的代碼風格標準。開源組織或一定規模的軟體公司也會形成自己的規範,很多人會堅稱只有自己的風格才是最好的,但事實是,只要可讀性好而且合乎你的使用習慣,就是適合你的代碼風格。當然,如果涉及和他人協作,使用統一的代碼風格也是非常必要的一件事。
對新手需要特別提示的一點是:追求源碼在視覺上的緊湊沒有實際意義。實際上,這些源碼在編譯成可執行程序的階段,那些多餘的空格換行及注釋(有些編程語言中的特殊注釋也會影響最終編譯的程序,但不包括 GML)都會被自動忽略掉。
使用局部變數
還是延續上面提到的編程風格的問題,很多新人恨不得在一行代碼里塞下全部內容,比如他們會寫出這樣的代碼:
draw_sprite(sprite_index, image_index, x + lengthdir_x(100, point_direction(x, y, mouse_x, mouse_y)), y + lengthdir_y(100, point_direction(x, y, mouse_x, mouse_y)));tn
這樣不僅可讀性很差,效率不高(point_direction() 被調用了兩次),看起來還很醜陋。所以為什麼不試試神奇海螺……不對……為什麼不試試局部變數呢?
我們可以這樣來寫:
var p_dir = point_direction(x, y, mouse_x, mouse_y);nvar local_x = x + lengthdir_x(100, p_dir);nvar local_y = y + lengthdir_y(100, p_dir);ndraw_sprite(sprite_index, image_index, local_x, local_y);n
創建局部變數的內存和自由開銷都很小,但好處卻顯而易見,令代碼變得清晰明了許多。在編寫 GML 腳本時,也應當養成這個習慣,在腳本開始處將腳本的參數存儲為局部變數,這樣就可以使用可讀性更好的變數名,不必讓整篇代碼充滿argumentX 這樣不加註釋基本看不懂的東西,減少了很多犯錯的機會。
另外,如果多次重複使用同一個表達式,那麼建議使用局部變數存儲它們以減少性能開銷。另外,特別需要強調的一點是,頻繁引用的全局變數最好也存儲到本地變數再進行使用,這樣可以避免因為某處錯誤造成對全局變數的污染。
數組
數組速度很快,相比其他數據結構來說內存開銷也很小,但仍然有很多可以優化的空間。創建數組時系統所分配的內存空間是基於數組大小的,因此你最好一開始就按它的最大容量來聲明,即便你暫時用不到那麼多內存。比如,如果你希望創建一個容納100個數值的數組,你應該先這樣對它進行初始化:
array[99] = 42;n
這樣聲明的內存空間位於同一區塊中(並且所有的數組變數均初始化為默認值42),這樣性能會最好,否則每當你新添加一個數組成員,就得為其重新分配內存。
特別注意:這種快速聲明並初始化的技巧不適用於 HTML5 模塊,因此如果需要導出 HTML5 版本,應該按如下方法處理(參考這裡):
if (os_browser == browser_not_a_browser)n{n array[99] = 42;n}nelsen{n for(var i = 0; i < 100; i++)n {n array[i] = SET_VALUE_TO_THE ANSWER_TO_LIFE_THE_UNIVERSE_AND_EVERYTHING;n }n}n
另外,數組名本身也能作為參數傳遞給腳本,但請一定要注意,傳入的參數只是數組的副本,你對其進行的任何改動都不會直接影響原數組的值,需要通過返回值的形式來得到結果,否則什麼也保存不下來。如果採用這種方式,運行效率和內存佔用都不會非常理想,因此,編寫腳本時,應謹慎以傳參方式來修改數組。不過呢,GML 也特別提供了一個特殊訪問符「@」,允許你直接訪問原數組的元素,例子如下:
// 調用腳本時將數組作為參數傳入nscript(array);nn// 腳本代碼:na = argument0; // 將參數保存到局部變數中na[0] = 100; // 這樣寫會創建原數組的拷貝na[@0] = 100; // 這樣寫會直接修改原數組的元素n
內置數據結構
相較老版 GM,GMS 對一些內置數據結構的設計做了很多優化提升。它們依然需要在釋放內存前手動進行銷毀,也確實比數組這樣簡單的數據結構慢一些。但 GML 內置了許多方便的函數令其變得十分好用,那一丁點的性能損失相比之下完全不是什麼大事。所以,盡情使用它們就好。
ds_maps是一個需要重點關注的數據結構類型,它為開發者提供了其他語言中 hashmap 那樣的 key-value 型的數據結構。ds_maps擁有很多方便的 API,用於讀入寫出數據,能夠勝任各式各樣的任務。
GMS 也提供了一些特殊的訪問符來簡化代碼書寫,你可以在官方文檔的相關頁面中找到這些內容。
碰撞
GMS 提供了許多方法來實現碰撞,但其中多數都會造成大量 CPU 運算開銷。常用的 collision_ functions,place_ functions 和 instance_ functions 等函數都依賴於包圍盒檢查,而這種演算法基本上沒有什麼優化的空間。此外,如果使用了精確到像素級的碰撞盒,那麼就會引入像素級的碰撞檢查,導致速度被拖慢。
我並非建議你放棄使用這些函數,它們是非常便利的工具,但在使用它們之前,你必須要了解它們之間的區別以及性能差異。一般來說,place_ functions比 instance_ functions更快,而後者又比 collision_functions 和 point_ functions,最好的做法是使用前仔細閱讀手冊,根據實際需求來選用合適的函數。
此外,如果希望構建基於瓷磚的碰撞系統,可以使用2維數組或 ds_grid。它們比基於精靈碰撞盒的演算法快得多,能夠大大提升你的遊戲性能。不過,當你使用了一些無法對齊到網格的不規則地形,牆壁或對象,這種方法可能就沒那麼適用了。這方面的例子在 GMS 的默認示例中就能找到一個,有興趣的話可以看看它的具體效果。
紋理交換與頂點批處理
如果開啟了 debug overlay,你會注意到屏幕上會出現兩項指標:紋理交換數和頂點批處理數。在進行優化的時候,以性能為出發點,咱們的目標是在保證實現遊戲功能的前提下儘可能減少其規模。(牢記本文開頭的提示:不要在不需要的時候進行任何優化,當然,懂得一些基本的原則,!)
優化紋理交換數主要通過調整精靈和背景圖的存儲方式,即精靈的屬性選項中的紋理組(Texture Groups)選項,你也可以在全局遊戲設置(Global Game Settings)的第二個選項卡中找到相關設置,快捷鍵為 Shift + Ctrl + G。優化的大體原則是將同時使用的材質分為一組,減少不必要的紋理交換數。如果你有一些只用於主界面的素材,可以將其編入單獨的紋理組中。同理,只在特定場景中出現的材質也應當放入獨立的紋理組中。
頂點信息是按"批次"傳送給GPU進行繪製的,一般情況下,批處理的量越大越好。因此,應當在繪製過程中應當儘可能避免中斷頂點批處理,以免頂點批處理數上漲。有許多操作會中斷頂點批處理,切勿分散且頻繁地使用它們,包括:使用圖層混合模式(blend mode),設置繪製顏色,設置繪製透明度,繪製內置圖形等操作。
舉個栗子,假設你有一堆子彈的實例在繪製時使用了 bm_add 混合模式(查看這裡的文檔說明),你會為每一個子彈都進行一次頂點批處理,在性能上這是非常草稿的行為!相反,理想的做法是在遊戲中加入一個控制對象,用於繪製遊戲中的所有子彈,類似下面的寫法:
draw_set_blend_mode(bm_add);nwith (obj_BULLET)n{n draw_self();n}ndraw_set_blend_mode(bm_normal);n
這樣所有子彈的繪製都在一次批處理之中完成。同樣,在調整透明度和顏色的時候也應該注意類似問題。
儘管 GMS 的 3D 支持非常支持,但是萬一你還是想稍微嘗試一番,下面是我認為會有所幫助的一些建議:
另外,禁用精靈/背景選項菜單中的 "Use for 3D" 選項,它基本上沒有什麼實際用途。每張 3D 貼圖都會單獨生成獨立的紋理頁並分別進行批處理,因此,一般情況下,選擇使用普通材質更加合理。你可以通過 sprite_get_uvs()來獲取 UV 坐標並賦值給變數以備後用。雖然這樣寫代碼上繁瑣了一點,但從性能角度來考慮還是划得來的。
粒子效果
運用得當的粒子系統能夠顯著提升遊戲的表現效果,但也會導致很多優化方面的問題:目前 GMS 內置的粒子效果精靈都獨立存儲在單獨的紋理頁中,這意味著如果你使用了大量不同的粒子效果,就會大幅度地增加紋理交換數。比較合理的方案是像載入其他普通的精靈那樣載入它們(一般存儲在 ~/%appdata%/GameMaker-Studio/Windows8/html5game/particles/ folder 路徑下)。
此外,對這些粒子效果使用混合模式也會降低遊戲性能。如果你的目標導出平台包括移動端,請不要使用它們。這種情況下,推薦使用普通的精靈動畫來自製粒子效果代替內置的系統。
其他建議
- 相比粒子效果,碰撞,字元串這些,三角函數運算的效率很高,不要害怕使用它們;
- draw 事件中不要放任何與繪製無關的代碼;
- 考慮使用 alarm event 來編寫那些不必每一幀都調用的代碼。
- 牢記我們在文章最開頭反覆強調過的觀點:不要做無謂的優化,在遊戲規模體量不大,運行良好的時候,優化基本上只是單純增加工作量而已,把更多的精力放在玩家能夠感受到的遊戲體驗上才是更划算的選擇。
參考來源
- 官方技術博客
推薦閱讀:
※FC遊戲的偽多捲軸
※非IT人士都能看懂的工程代碼重構方法:三步走
※【《Real-Time Rendering 3rd》 提煉總結】(九) 第十章 · 遊戲開發中基於圖像的渲染技術總結
※從零開始手敲次世代遊戲引擎(MacOS特別篇 貳)