Godot Engine Walk-through | 第一個小遊戲測試

短暫休息

因為這學期非常繁忙,一陣子沒有更新blogger了,趁寒假期間來繼續深入學習Godot Engine,而也因為經過這學期修的遊戲程式設計的選修課洗禮,意識到GDD規劃真的必須確實執行、並且考量開發時程,如何在目前極其有限的資源、少量的時間下,實際做完並且做好一個專案,是接下來的短期目標。

ㄛ對了,上次規劃的瘦子拉卡諾宣告不治,將在接下來一個月重新規劃一次。

Godot Engine Walk-through

接觸Godot引擎也已經剛滿一年,但礙於學期間都在使用Unity,重拾舊工具的時候已完全不熟悉,所以我決定乾脆以系列文的方式,一邊重新walk through一次documentation的重點(或偶爾針對某些問題做系統化整理)、一邊紀錄學習的過程,方便自己審視也跟少數的讀者做小小交流。

 

第一個範例遊戲

如果大家有用過Godot Engine,教學文件第一個小教學就是一個閃躲怪物的小遊戲,看似極其簡單的遊戲機制,經過一年再寫一次同樣的code竟然也花上五個小時(其實大多時候都在思考如何安排nodes 和 訊息交流的次序/責任歸屬),但是也思考到了一年前沒有想到的層面,並且程式架構與邏輯也非常清晰,feelsGoodMan...


如果讀者有興趣,source code在這裡,可以用3.0.6版本開啟專案編輯與試玩。
以下就分為關於程式架構描述、各種坑爹bugs整理、問題與想法來對專案做收穫總結,然後萬一三生有幸被高手看到呢,希望可以指點一下應該改善的地方。
(註:以下圖表非UML標準,而是以實用原則為主的示意圖)

 遊戲機制流程圖


簡單分析了一下這個小糞game的核心機制(彈幕躲避類),乍看下好像只有移動的操作,其實應該再細分為沒有危險的"moving"跟執行閃躲動作的"dodge"兩個任務,因為起始條件一定是還離敵人很遠的狀態,此時玩家並不是沒有事情做,而是應該要盡可能地往中間靠(如此對任何方向來的敵人都有相對平均的反應時間)、或者往已經很密集的地方靠近(統計上該地方再出現敵人的機率應該相對較小,在敵人隨機分布的情形下),有了這個機智的前提下呢,在躲避狀態才會相對輕鬆哦哦哦。當然躲避成功就是重複上面兩個高強度的動作、失敗則顯示存活秒數,並給予再次挑戰的機會。
對了,在最後try again是不能按不要的,嘻嘻。

主程式場景架構(Scene Tree) / 狀態機(FSM)


相當直覺的,場景中出現的玩家、怪物、UI都對應到一個packedScene實體,而輔助為主的物件則統一畫在圖中右側(計時器、背景、音樂音效),並以紅色來表明signal訊息、對應到一個物件的slot,例如:當玩家發出hit的signal就會呼叫Main中的game_over()函式,表示玩家只要被碰到就會直接導致遊戲結束。

另外我也在Main中加了一個相當奇異的add_survive_time(),他的工作就是"increment survive_time、然後再update HUD上的數值",雖然任務相當簡單,但我還是把兩項任務合併到一個function以便讓slot的回應恰恰都是呼叫"一個任務",而非散落的兩三行,我認為這樣大大提高了可讀性、也多一個可能在未來也相當有機會再被呼叫的函式(把add_survive_time想成是add_score,當然就不只由累計時間加分、也有可能由其他方式加分)。

 以下為Main scene的狀態機


玩家Scene Tree / FSM 


玩家場景相對簡單,trail是殘影效果、然後body_entered觸發的slot會再進一步發出hit的signal(之後會再傳給Main),上圖此處有誤,最先的訊號應該不是hit而是body_entered才對。



玩家狀態只有兩種,在遊戲狀態的OVER到START之間,是屬於HIDE狀態(隱藏置中),
 等到START後到結束前都是PROCESSED,此時是玩家可以操控角色的狀態(_process(delta)被開啟執行),而這裡雖然移動機制相同、但我實作了兩種動畫顯示方式,說不定在真的大型專案裡的某些部份也需要多寫幾個小差異版本來做測試吧(直接讓美術一次閉嘴閉久一點?)。

怪物與產怪器 Scene Tree / FSM 




Mob Spawner這裡比較炫一點,教學用了Path2D和PathFollow2D兩個node做搭配,來讓PathFollow以隨機的方式在"Path2D邊界"隨選一點作為生成點(所以以後需要在地圖上某個區域邊界隨機生成物件就可以採用這個方法囉,fantAstic)、透過spawn_random()再instance出一隻新的怪物,並做些計算後給予速度和方向;接著在刪除怪物的判定部分使用了VisibilityNotifier2D,避開了直接使用物理引擎判定刪除物件的負擔(雖然不確定具體到底省了多少資源、不過肯定是比較輕量的)


怪物狀態更為簡單,生成後只會移動,超出邊界後被delete掉,這裡之所以特地畫了一個INIT是因為有做一個隨機化怪物的動作(生成時會給予不同動畫、速度)、然後才移動。

HUD Scene Tree



簡單的UI顯示系統,不同狀態下會開或關各個元件的顯示,這裡也把各個狀態做的事情包成functions來對main做邏輯化的接口(在main的層級下存取到button應該也蠻怪的),FSM圖就省略不畫、跟main的FSM基本上一模模一樣樣。


BUGs紀錄

俗話說,十個霸哥九個蠢,以下紀錄了在本次專案中的五個蠢錯誤:

  • 錯誤命名:把position拼成positon,事實上我看了足足三分鐘才看出來==,一直以為是因為某些特殊原因而無法獲得position member...
  • instanced但忘記加入scene tree:剛跳脫Unity、忘記Godot使用scene tree來管理與追蹤物件,所以實體化完記得要使用add_child或其他方式加入scene tree。
  • call_group參數:在gameover時執行了一項"關掉目前存活怪物hitbox"任務,卡了一陣子function spec,發現第二個參數應該要是個string:call_group("[group name]", "[function name]", [...]),而非直接指定function(這樣一來就變成了function reference了、那樣肯定不對)
  • 重置遊戲粒子特效在舊的位置有殘存、產生不必要的效果:當player reset的時候(關掉process)似乎會"暫停"particles的處理而非完全刪除,所以當start時還要在手動重啟一次particles的程序才能避免有殘存粒子出現在奇怪的地方。
  • 物理性物件無法"直接"更改Node2D的scale(hint是說交由物理系統來handle),所以在縮放怪物的大小的時候要縮放AnimatedSprite而並非RigidBody2D本身;作為對比,玩家就可以直接縮放root,因為玩家是一個Area2D(並非物理性物件)

 

問題與想法

以下有的非急迫問題暫時沒有結論,待往後尋找答案,若很有心得也可能變成一篇文章~
  • CanvasItem.hide()與Scene Hiearchy的眼睛圖示(CanvasItem.visible)是一樣的嗎?效用看起來是一樣的,暫時看不出有差異的地方
  • get_viewport_rect()和get_tree.root.get_visible_rect()是一樣的嗎?
  • _process()中的參數delta與get_process_delta()是一樣的嗎?
    測試過兩者應該差不多(不確定有沒有時序上的延遲誤差、但不影響肉眼觀察結果)
  • 在_process()使用Input.*和在_input()裡面接收input具體有什麼差異?
  • 在啟用與關閉物件的實作當中,hide/show和instanced/free和add/remove scene-tree具體而言有什麼優劣、什麼時機該使用何種方法
  • scene的元件該切多細呢(什麼時候要另外save成另一棵scene tree)?通常誰來當root呢(有什麼好處)?script都應該綁在root node上嗎?
  • Game Logic的FSM能不能使用插件來支援(像是角色動畫狀態機那樣),那樣可能更快並且可以給game designer有更具體的流程表現表達方式?

留言

熱門文章