七八月的 BOOOM 告一段落了,我也算是長出了一口氣,因為不用再擔心熬夜熬到人無了。
其實開發開始之前,我看著隊伍裡的人還是很有信心的,第一次這麼多隊友,一人一錘子,怎麼也錘完一個作品了吧。於是做出了這次最失敗,但並不後悔的決定——做個 3D 恐怖遊戲。
“我當時很興奮啊!3D 恐怖遊戲,就完全是那個 3D 的!恐怖遊戲!一個月鼓搗出來,不要什麼 Jump Scare,就完全是心理恐怖,氛圍!是不是很大膽!”不過我倒是沒有放棄,因為根本輪不到放棄,我都沒做完……實在是讓人頓足捶胸、痛哭流涕。
書歸正文,還是回到開發記錄上來。因為我是第一次製作 3D 遊戲,所以著重記錄下怎麼在 Godot 裡實現一個 3D 第一人稱遊戲。
第一人稱操作
第一人稱是一種討巧的辦法,代入感好,甚至可以省略整個人物的模型。我這次需要的基本操作只有行走、跑步、旋轉攝像頭觀察、以及查看手機——手機上有一些功能。
得益於以往對插件的關注和積累,我選中了一款 Godot 社區中的插件——CharacterController,在此基礎上進行一番學習和改造。
縱觀整個插件,它利用了 Godot 的節點組合思想,將 Player 分成了兩個部分——攝像頭操作和其他行為。行走、跑步、跳躍、下蹲、游泳、飛行等都是一些相對獨立的行為,這些對 Player 移動行為上的操作被分成了一個個可選擇掛載的 MovementAbility,當行為被掛載在 Player 上這些操作就生效了,它們分別控制 Player 主體的運動。
另一部分是攝像頭操作。首先是視角旋轉,將旋轉分為了兩部分:水平的旋轉為人物本身的旋轉,豎直的旋轉為攝像頭所在節點的旋轉。這樣做的好處是可以避免單個節點旋轉的歐拉角鎖問題,也方便理解和控制,因為並不涉及到歪頭這種旋轉方式。
同時,因為頭的標記點是和身體分開的,在遊戲中涉及到鏡頭運動的一些演出上,我就可以維持人物本身不動,而控制頭標記的位移,實現例如從地面醒來並坐起、坐在凳子上、以正視圖視角查看屏幕等操作。此時Player本身並沒有移動,而是頭標記在空間中運動。(我採用了兩種動畫方式,一種是可打斷的、簡單快速響應的 tween 動畫,一種是演出複雜、不可打斷的手動 k 幀動畫)
同時得益於頭部節點的獨立性,更多的擴展效果得以加入,這些效果只需要讀取根節點上數值的變化和事件的觸發,做出對應的反應。例如走路時左右晃動模擬腳步的攝像頭運動,根據步長播放腳步聲,在攝像頭前放置燈光和飛蠅這些我就不一一介紹了。感興趣的朋友可以評論交流。
屏幕效果與UI交互
這次設計的 3D 場景中的主要難點是監控攝像頭的查看和控制、對電腦中 UI 的獨立控制。最開始我並沒有想直接做在 3D 中,雖然效果比較好,但是也比較複雜,不如點擊後直接彈出一個 UI 來查看和操作。不過我最終還是選擇了 3D 直接展示,因為效果真的很棒!而且 Godot 引擎中很容易可以實現這一點。
遊戲中的屏幕一共有三種:監控室的電視、資料室中的電腦、Player 手上的手機。
三種屏幕的基礎顯示都很簡單,Godot 中有一個節點叫做 SubViewport。這個節點可以獨立於當前主視口生成畫面,如果學習過圖形學/圖形 API 的朋友應該能想到,這個功能是使用獨立的 FrameBuffer 實現的——即在項目中放置多個相機渲染到不同的目標上,再將目標作為某個 Mesh 的貼圖顯示出來。需要注意的是,Mesh 的 UV 一定要正確,否則可能出現畫面不全或者位置不正確的現象。對於像有 CRT 效果的電視和電腦顯示器,就可以在這個貼圖的基礎上使用 Shader 實現了。
可能有的朋友會問,使用這麼多 Camera 和 FrameBuffer 性能不會很糟糕嗎。對我們項目來說其實還好,因為這次做的是復古低分辨率的類型,整個視口的分辨率很低(512*360),這些子視口的分辨率自然更低了,所以在沒有特意做優化的情況下,也不會特別卡。同時,這些顯示器在不注視的情況下圖像是不會更新的,也優化了一定的性能。
然後就是最大的難點了,電腦屏幕中鼠標要和畫面進行交互,可以選擇車輛、切換監控畫面等等。
好在 Godot 中,不同視口的事件是可以主動傳遞的(`vp.push_input(event)`),我在當前主視口操作時將鼠標相關事件加工並傳遞給電腦屏幕對應的視口。事件加工主要是修改鼠標相對於視口的位置座標,在主視口中鼠標被隱藏,但它的位移要根據比例縮放轉換成屏幕視口的座標。點擊選車時,在對應相機發出射線與停車場中的車輛求交,獲取信息。
另外由於實現了座標的轉換,我也順手實現了鼠標模型的移動,讓操作電腦時鼠標的模型也跟著動起來,看起來還蠻有意思的。
地圖搭建
不幸的是,由於使用的引擎太小眾,我的 3D 美術小夥伴沒用過,所以完全沒辦法幫我做地編工作,地圖搭建的工作就只能落在我的頭上。
至於搭好一個大場景再導入進引擎,那工作不比我自己搭地圖要簡單多少。因為燈光等物體有一些特殊操作(如不定時閃爍),物體很多需要寫導入腳本,加上溝通成本……於是我開始琢磨使用引擎裡的 GridMap,搭建一個網格地圖。
說實話,Godot 裡的 GridMap 不算是特別好用,至少不比它的 Tilemap 好用(這裡強推一下 Godot 的 2D 遊戲製作能力,Tilemap 等自帶工具非常順手,完全不用找個第三方瓦片地圖工具比如 Tiled 之類的)。但功能還算齊全,而且這種網格地圖的渲染一般是使用了 GPUInstance 技術實現的,一個模型在空間中渲染很多次也不會有太大的性能要求,比如有幾千塊地板什麼的。
經過一段時間的使用,我總結的小技巧如下:
1. 網格庫的創建:可以先建一個場景,使用 CSG 系列節點,對不同的網格進行合併,做出基礎的組件,再安裝一個 CSGToMesh 插件,就可以將一組 CSG 轉換為單個 MeshInstance 節點了(只有 MeshInstance3D 和下屬的導航網格或物理體可以導出網格)。對於一個單層、橫平豎直的地圖,一般只需要地板、天花板、地板與牆壁相連的兩個面、角落的三個面,這四個網格組件,就可以搭建出來。導出網格庫時,推薦“應用MeshInstance變換”,這樣方便搭建時網格的對齊。
2. 網格地圖的繪製:不推薦將所有的地圖內容繪製在一個節點上,比如地面牆壁和天花板。在後期非網格地圖的細緻場景搭建中,可能需要俯視圖觀察和修改場景,如果地面和天花板是同一個網格地圖節點,就沒法“打開天窗說亮話”了。所以我選擇了多個 GridMap 節點完成場景中不同物體的搭建工作。編輯時將天花板隱藏,所有的錯誤就無所遁形了(bushi
3. 單個網格不能滿足的特殊物體:例如本次地圖中,我需要所有的燈都是我自己實現的一個複雜場景,但全圖有幾十個這種燈,一個個手擺太煩人了也容易對不準位置。所以我使用了一個 Light 的 GridMap,其中的網格只用來做擺放位置的標記,在項目運行時,會在對應位置實例化場景,並把這些標記清空。
4. 網格地圖工具操作:文檔裡這塊沒說清楚,繪製地圖時,當不選中某個網格塊的時候,視角是可以自由移動的,選中後 WASD 鍵位就變成了對網格塊在不同軸上的旋轉了。這時再按 ESC 鍵就可以退出網格塊的選中,可以重新自由移動視角了。另外推薦使用三視圖進行地板等橫平豎直物體的繪製。
可交互物體和區域
在 3D 遊戲中,需要和很多物體進行交互,例如查看、拾取、操作等等。在交互時基本操作都是,看向物體,物體提示操作的按鍵,按鍵後開始交互。
剛開始實現時,我將相關邏輯寫在 Player 下,比如坐在椅子上、看屏幕等等。但當物品漸漸增多,Player 的代碼變得臃腫起來,不好定位問題也不方便查看和擴展。於是我將交互物體部分進行了重構。
正如開頭所寫,物體的交互是有一套基本操作的。那麼 Player 實際關心的只有對物體的注視,和一些基本狀態的維護,把對物體的操作歸還給物體本身,來實現一定程度的邏輯解耦。和直覺的邏輯實現不同,讓物體自己負責被操作這件事,自己的事情自己做(
所以,把 Thing 抽象為一個父類,實現被注視狀態的維護,當 Player 的射線拾取到 Thing 時,就將 Thing 設置為被注視。而各個不同的物體對 Thing 進行擴展,描述當被注視時不同的操作會對物體本身和 Player 造成什麼影響。例如椅子會操作Player坐過來,電視會操作Player看過來,手機會消失並通知Player已經有手機了、身上的手機可以顯示了等等。
跟上述思想類似,同時也和 Player 上掛載不同的 MovementAbility 類似,我將可互動的區域也做成了繼承 Ability 父類的不同的行為。當 Player 進入區域和退出區域時會觸發事件,而事件會通知所有是 Ability 類型的子節點。這些子節點定義了 Player 進出區域會進行的操作。在遊戲中,部分區域回聲、BGM、閃屏演出、音頻片段的播放等,都是一個個 Ability,節省了很多硬代碼的編寫,方便在場景編輯時組合使用。
其他
Godot 在 4.3 中新增了交互式音頻的能力,結合 AudioStreamPlayer3D 可以實現類似於音頻中間件的音頻過渡、條件觸發音頻、播放列表等功能。說實話,不算特別好用,但是總比沒有強。這次 BOOOM 我也學習和試了一下。
新增的且我主要使用的是 AudioStreamInteractive,可以包含若干剪輯和一張過渡表,可以自己配置不同剪輯間切換時的過渡方式,但這個過渡方式沒那麼精細,只是在多少節拍間過渡,過渡哪個位置,播放完會自動跳轉到哪個剪輯,有沒有淡入淡出之類,對 db 和音調等等是沒法控制的。 新增的另一個 AudioStreamPlaylist 我就沒用上了,可以把多個子音頻按列表形式播放,本次項目用不到。
另外是本來就有的功能 AudioStreamPlayer3D,可以根據距離衰減聲音,我使用這個節點開發了通過車鑰匙找車的功能。
如果隊友裡有可以用音頻中間件製作音效音樂的還是推薦用音頻中間件,解耦功能又全,我看了下,現在 Godot 社區中應該是對 wwise 和 fmod 都有插件支持,看 star 數量使用的人應該不少。
總結
因為時間排布一團亂麻,這次 BOOOM 不是 solo 勝似 solo。加上本職工作也一直在加班,熬得我是昏天黑地、地崩天裂、裂得不能再開了。不過好在是最後兩天及時剎車,自己構想並補了一個效果十分勉強的演出作為結尾,把項目及時交上去了。
在演出結束後,我放了一個圖:
這次的故事確實沒講完,有很多設想的東西沒有加進去。風格化渲染我也不滿意——因為沒時間做本來想實現的類似 PS1 平臺的渲染缺陷風格,大概率需要建模和程序的共同配合。好在氛圍感確實做到了一些,在 3D 製作上也學習到了很多。
和結束語相同的是,我的故事確實還未結束,不同的是,我不會退出遊戲。
共勉!