浮岛物语(FORAGER): 在 GameMaker 中做优化
Forager是如何管理成千上百個實例的
有些時候,你很幸運有機會在項目啟動時就參與其中,這樣可以對你的代碼庫有更全面的了解和掌控。但另一種情況是,你需要接手一個比較復雜的,將近50000行代碼的項目然后被告知“搞定這個問題”。嗨,我是lazyeye,Forager的程序負責人。
在我成為這個游戲的程序負責人之前,我就定下了一項任務:優化。Forager是一個規模龐大的游戲,玩家可以在巨大的地圖上收集資源并制作各種裝置,這意味著游戲中很容易就會出現5000個實例同時運作的狀況,甚至更多。這種特殊的游戲類型會遇到一個常見的優化難題——玩家具備創建幾乎無限實例的能力,而我們還要保證游戲可以在所有的平臺上順暢運行。
實例數量過多是GameMaker制作的游戲中常見的一個問題;幾乎在所有我參與過的項目中,過多的實例數量都是給性能拖后腿的一環。通常人們會陷入使用對象而不是去學習更有效率的工具的陷阱,比如:粒子系統(particle systems),數據結構(data structures),素材圖層(asset layers)等等。了解如何使用這些方法應對不同場合其實非常重要,因為這樣可以提升性能并讓一些復雜的工作變得更加簡單。比如某些物體不會移動,也不需要根據深度進行排序,也沒有任何碰撞事件,那就不該用對象來實現它。
但是,Forager很不一樣,它與我見過的大多數項目不同,游戲中幾乎每一個物體都應當以實例來實現,并且嘗試不用實例來完成類似的效果簡直是一場噩夢。這讓人非常沮喪,這讓我意識到我必須卷起袖子然后跳出這個框架重新思考一下。
?
我說的大量實例……是真的超級多!
繪制優化
因此,我們堅持使用了實例。但是仍然有許多方法可以改進每一幀運行的代碼性能。性能下降通常源于對繪制方法的工作原理不夠了解。不幸的是,這個問題對我而言非常超綱(我并不是專家),但一些基本的理解還是有助于避免一些常見的問題。
你之前可能聽說過“指令集中斷(batch breaks)”,一個指令集(batch)指GameMaker發送給GPU的一組用于繪制畫面的指令。GPU在繪制成組內容時非常高效,但當發送到GPU的指令集過多時,在指令集直接切換的時間就越多,從而影響了性能。每當更新GameMaker的繪制配置時都有可能發生指令集中斷,比如在修改顏色/Alpha,字體,混合模式等時,以下是一些常見的“指令集斷路器”
?
- Primitives(draw_rectangle,draw_circle,draw_primitive_begin,etc.)
- Surfaces(surface_set_target,surface_reset_target)
- Shaders(shader_set_target,shader_reset_target)
- Draw settings(draw_set_font,draw_set_colour,etc.)
- GPU settings(gpu_set_blendmode,gpu_set_alphaenable,etc.)
這種指令集中斷是難免會遇到的,不同平臺的處理方式也有差異。而關鍵是要仔細構建代碼盡量減少這些中斷的發生。通常情況下,我們可以通過將類似的指令集組合到一起來進行優化。比如,與其讓大量實例各自采用以下繪制事件(Draw Event):
?
你可以嘗試在一個控制器對象中統一完成此類操作
?
深度順序的差異時常會影響這個操作,有時候會使得畫面看起來異常。你可以使用圖層腳本來進行輔助,更精確地控制圖層繪制的先后順序從而進行優化。
步(Step)事件優化
優化步事件需要不斷捫心自問:“這個操作需要每一幀都執行嗎?”通常情況下,我會反復思考這個問題幾次,剛開始答案總會是“沒錯,這個操作必須每幀都執行”,但可能到了第八次時就會變成“哦,那個法子可能行得通。”
實例需要每一幀都從全局的數據結構里獲取信息嗎?也許你在創建(Create)事件里執行一次就夠了。你會在步事件里不斷刷新玩家的背包嗎?也許只需要當發生添加或刪除操作時更新一下就行了。沒有以上狀況需要處理?也許根本沒人在意這些代碼是不是隔幾幀執行一次。
還有一個小技巧是利用GML的短路(short-circuiting)特性。這是GML里當獲得一個FALSE后決定是否停止繼續執行后續代碼的機制。比如以下這個例子。
?
由于1+1不可能等于3,因此GameMaker根本不會去判斷instance_place的調用。因為判斷條件里使用了&&因此兩個條件必須都為TRUE,因此當第一個條件為FALSE時則整個條件不可能返回TRUE。因此你可以在制定判斷條件時控制好順序,如果你確定某個條件比其它的都重要,那務必把它放在最前面!另外要注意哪些條件最容易返回FALSE——當你前五個條件幾乎總是TRUE,后面跟一個經常出現FALSE的條件時,那前面那些判斷就都是在浪費時間。
更進一步:動態加載實例
這些優化機制都很棒,但是對于浮島物語而言,我們真正需要的是一個“宏觀”解決方案。作為一個優化者,你的工作是騙過玩家,讓他們以為某件事情正在發生即可,而實際上可以把很多東西藏到幕后去處理。這種技巧是優化的基礎:再聰明的玩家也想象不到游戲里的處理機制,而只能基于自己看見的內容進行理解。
雖然我們已經確保了所有的實例都是必須的,但這不意味著每一個實例都是那么關鍵。以“objTree”為例,當玩家和它們發生交互時,樹木需要處理深度排序,具備碰撞效果,還有各種視覺效果。但是,如果玩家必須靠近樹木才能進行交互,那我們在95%的狀況下可能都不用去實例化這棵樹。如果一棵樹在森林里消失了,但玩家并沒有站在邊上看到這一切,他們會在意這些嗎?
這需要讓我們的動態加載系統來進行處理了。如果玩家看不見某個實例,我們就可以將其停用。如果玩家移動到可以看到該實例的位置,我們可以在它出現在視野里之前就迅速激活它。下面這個GIF圖再現了這個過程——白色矩形表示了我們的視野區域,在該范圍以外的實例會被臨時禁用進行動態加載處理。
?
注意邊界位置
一些實際代碼
下面這個CullObject的腳本代碼,就是用來在步事件中檢測活動的實例是否需要被暫時禁用
?
?
所有的截圖代碼的高亮風格都基于TonyStr的Dracula主題
我們把需要動態加載的對象作為參數傳入,來檢測該實例的圖像是否在視野之外。如果在視野外,我們創建一個數組來保存這個實例的ID和邊界框(Boundary box),然后把這個數組塞進記錄停用實例的列表里。要注意這里的邊界框是基于精靈圖像縮放繪制的尺寸,而不是基于實例的碰撞盒。我們添加了少許的縮放,手機號碼購買平臺因為浮島物語中偶爾會基于某些變量繪制稍小尺寸的圖像。
下一個腳本——ProcessCulls用于把停用的實例“恢復”
?
注意:浮島物語中實際使用的這些腳本中會有更多內容,但是為了方便演示,我把這些腳本都脫水只保留了核心代碼
這個腳本里我們只是處理停用實例列表,檢測相機視野是否移動到了能展示出某個實例的位置,如果是就立刻激活該實例并將相關數據從列表中刪除即可。
等一下,我搞砸了
當我把以上修改代碼推送到代碼庫后不久,我想到“嗯,我需要知道游戲里有沒有用到instance_exists,instance_number和其它實例函數,這些代碼可能會因為實例被停用而出紕漏”
我立刻搜索了instance_find,instance_exists和instance_number這些關鍵詞,然后看到了500多行結果。
糟糕…
這個狀況非常棘手——這些函數將無法返回正確的結果,因為這些函數只能作用于激活狀態的實例。如果游戲中一直在使用這些實例函數處理相關邏輯……那在動態加載中被禁用的實例就會出大問題。
但我沒有放棄,我決定在動態加載系統中再增加一層判斷。我需要一個方法來檢測實例是否存在,無論這個實例是否處于激活狀態。我還需要獲得所有這些實例的準確數量并能快速檢索它們。處理instance_exists函數將是一個挑戰,這個函數我們可以傳入三種類型的參數——ID,對象名稱或者父對象名稱
真·實例函數
第一步是在創建一個可以動態加載的實例時把它添加到一個全局的數據結構里:
?
我們把這個實例的ID添加到我們的實例緩存列表中。接下來,我們遍歷所有可能的父對象ID數組,檢查該實例是否是某個父對象的子對象。如果是,我們把它的ID也添加到這個父對象的實例列表中。這是因為GameMaker中,當我們把一個父對象屬性的實例作為參數傳入某個實例函數時,這會影響到這個父對象所有的子對象,因此我們的腳本中同樣需要實現這一點。
接下來,我們需要在銷毀實例時從緩存中釋放掉相關資源,因此在清理事件(Clean Up)中需要以下內容:
?
?
這跟之前的過程剛好相反
現在我們的實例已經創建好了,我們需要準備好我們用來替換的函數。
?
你可能已經注意到了,在所有這些函數中,同時兼容了不屬于我們用自定義的“真·實例”系統所處理的實例函數。請記住,在程序中有500多處相關代碼,因此我試圖盡可能節省時間。可以快速進行查找和替換這一點至關重要。
?
現在我們可以看到這個激活變量的用途——這樣一來我們就可以在結果返回之前就確認這個實例是否處于激活狀態,如果不是,那就可以將其激活并在臨時激活列表中加以記錄。但是我們并不會將其真正激活,這樣我們就可以很好的區分出真正被激活的實例和那些臨時激活的實例。
在返回結果之前臨時激活實例是很有必要的,因為這樣就可以在實例被恢復之前就通過代碼來修改其中的一些值。在GameMaker中被停用的實例只能從中讀取值而不能直接寫入修改。而在一個理想的系統之中,這種激活狀態應該是可選的,但因為我需要保持參數格式不變以兼容系統原有的實例函數,因此我把激活狀態設為必要的前置條件了。
終于,我們在TrueInstanceExists可以通過檢測傳入參數是否大于100000來確定這是不是一個實例或對象ID,而TrueInstanceFind可以用來確保在返回某個實例之前先將它激活。
?
?
最后,我們必須停用掉我們臨時激活的實例,這里會有一個小問題——GameMaker在每一個步事件(step)和繪制事件(draw)之間會重建事件隊列。這意味著我們必須確保我們的控制器對象在每個事件里都運行以下腳本,否則我們可能會遇到某個實例在不恰當的時機試圖觸發它的某個事件。
?
?
再快一點
回想一下我最開始說的,我們必須反復質疑我們步事件中的代碼是否需要每一幀都運行?這同樣適用于此——宏(macro)。SYSTEM_CHECK_INTERVAL,它控制著我們的系統級腳本(比如動態加載機制)的運行頻率。其中的腳本會根據特定系數來進行調控,比如,在控制植物生長的系統腳本中,增長值將根據我們的間隔系數進行增長。如果系統每20幀刷新一次,則增長值將以20為單位進行增長。我們把需要控制的腳本放進一個switch循環,這可以把這些操作平均分配到20幀里去。
?
?
結語
?
?
游戲優化是個極其龐大的話題,我們在這里只是討論了其中的一部分內容。實例數量是影響大部分項目的一個方面,在GameMaker引擎里,正確理解紋理渲染的機制和其它一些游戲開發過程中的問題,對于優化游戲至關重要。
但是,類似“不要太早優化你的游戲”這種建議仍然非常有道理。在你明確自己的游戲有性能上的問題之前,沒必要過分擔心性能問題,并因此妨礙了開發進度。事實上,GameMaker對于用它制作的那些游戲而已非常合用——大部分開發人員永遠都不需要擔心這些問題。
也就是說,如果你最后做了一個體量巨大的游戲而面臨重大的性能問題,我支持你充分調動智慧和好奇心,進行各種試驗和研究。
或者,你懂的,來找我吧;)
總結
以上是生活随笔為你收集整理的浮岛物语(FORAGER): 在 GameMaker 中做优化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 面向对象的程序设计在游戏开发中使用(一)
- 下一篇: Unity3D游戏制作 移动平台上的角色