Godot入門到棄坑:給敵人血肉


3樓貓 發佈時間:2025-01-21 23:35:50 作者:cameLcAsE Language

現在應該跟這個抽象的敵人說拜拜了。我們來做一個有血有肉的3D角色。
當然,我們做一個第三人稱視角的玩家角色,玩家可以直接操作一個看得見身子的角色似乎更有意思。不過相比之下第三人稱角色操作和攝像機代碼會更加複雜,所以這裡暫時先把前面的敵人改了。

3D模型資源

之前我們只是做了個簡單的3D模型來象徵性地表示一下敵人。很多(應該說大部分吧)3D遊戲都會用更精美的3D模型來表示遊戲中的虛擬角色和場景。這些模型通常是用各種3D軟件來製作然後產生特定格式的文件再導入到遊戲引擎中。
Godot支持多種3D資源文件格式,不過首推的是glTF 2.0文件。這是一個由Khronos Group(也是OpenGL、Vulkan等圖形API相關標準的維護組織)維護的3D模型數據標準。這種格式的數據主要有兩個後綴名,一個是gltf(文本格式)和glb(二進制)。
fbxobj這樣的常見格式也有支持。此外也支持直接導入Blender的.blend文件,不過需要在本機上有安裝Blender。
如果你有合適的模型或者自己可以做模型也可以導入自己的模型。當然為了方便講解,這裡以一個免費的帶動畫的小模型作為例子中使用的模型。這裡下載。這個模型就是一個Godot機器人的形象。

導入3D模型資源

和導入其它類型的資源一樣,導入3D資源只需要簡單地把文件拖進來即可。
不過,略微有一點點反直覺的是,3D模型導入後會變成一個場景而不是某種單個的資源。當然仔細想想一個準備就緒的“3D模型”實際上“肯定”不只是一堆網格數據——除非你真的只想要一個只有網格的模型。這樣的模型除了模型本身的網格數據,通常還有各種紋理(就是一些讓模型表面有顏色、圖案的圖片)、材質(確定模型表面如何響應光照)等等。
上述的模型導入到Godot之後會出現兩個文件,一個是後綴名為.glb的場景,一個是png紋理——這個是Godot自動生成的Mipmap,不用管它。
場景打開之後是這樣一個窗口。雖然它也是一個場景的圖標但是和我們自己創建的場景還是不一樣。這個窗口實際上是調整此文件的導入參數以便於重新導入。我們目前不需要調整這些參數,但是可以簡單瞭解一下它的結構。
左側的面板中可以看到導入的場景樹。從各節點的圖標和名稱我們就可以略知一二。
AnimationPlayer也是我們的老朋友了。前面的文章已經介紹過,AnimationPlayer本質上就是一個複雜的插值工具,它也不分什麼3D/2D。
我們唯一沒見過的就是這個Skeleton 3D節點。它是一個骷髏頭圖標,skeleton就是骨架的意思。之前在介紹動畫的文章中我提到可以用tween和AnimationPlayer通過一些簡單的插值來製作一些簡單的小動畫。實際上在3D場景中,我們依然可以用這些技巧而不通過“真正的”3D軟件來製作一些簡單的小動畫——前提是它們足夠簡單,比如只是簡單地旋轉某一坨什麼的。
要知道,複雜的3D模型是由不計其數的頂點和多邊形構成的,我們不可能挨個去控制這些頂點構成的多邊形移動、旋轉、縮放什麼的!好吧,事實上,3D軟件說到底就是在做這些事情,但是這不是我們在製作遊戲、設計遊戲、為遊戲玩法寫代碼時首要應該考慮和操心的。
為了讓網格動起來,特別是讓一些包括人型模型在內的、生物類模型栩栩如生地動起來,需要用到一種虛擬的骨骼(skeleton)來控制網格以便製作動畫,這些骨骼會根據各種參數讓和它綁定的網格按照指定的規則動起來。現在在各種3D軟件中有廣泛的應用。
Godot中的Skeleton 3D節點就是代表這種骨骼,AnimationPlayer中的各種動畫其實就是控制這些骨骼製作的。
最後場景的中各個MeshInstance3D節點自然就是真正的“模型”數據了,它構成了模型的網格。

使用導入的模型

為了替換掉之前的敵人模型,我們直接把這個導入的場景加入場景樹中即可,刪掉之前佔位用的簡單模型。
你可能需要修改之前的CollisionShape3D的大小、位置來使之與新的模型匹配。
另外,如果你確實需要單獨使用導入的場景中的各個東西的話,你可以新建一個場景,加入導入的場景,然後右鍵Make Local。這樣所有的東西就單獨拷貝到這個場景了,你就可以單獨另存其中的一部分為資源,然後供其它場景使用了。

我前面呢?

如果你的代碼和我之前寫的代碼一樣,那麼現在運行遊戲的話,應該轉身看向玩家的敵人結果背對著玩家的攝像機了!
看看模型的正面朝哪個方向呢?
+Z方向!
還記得前面介紹攝像機的“前方”嗎?按照Godot採用的慣例,攝像機的前方是-Z方向,Vector3.FORWARD也是-Z方向。但是!模型的前方是+Z方向!Godot希望模型的前方是對著攝像機的視線的。
如果你的代碼和我的代碼差不多,之前的轉向代碼是這樣的:
transform.looking_at(body.position)
此處的body是玩家角色。looking_at方法實際上有三個參數,只不過後面兩個參數都有默認值所以這裡省略了。第二個參數指出旋轉時的上方,可以用默認的上方。第三個參數是我們這裡需要關注的。第三個參數叫use_model_front
,即是否要使用模型的前方(即Vecto3.MODEL_FRONT)。此參數為true時,將採用+Z方向為前方而不是默認的-Z方向。這個參數設置好後,就可以看到期望的結果了。

動起來動起來

有了前面的經驗,我們依然可以用2D角色類似的套路,定義一個表示狀態的枚舉類型然後根據狀態來更新動畫。
不過3D動畫有時候會出現更為複雜的情況,這裡我們來學習一個更擅長處理複雜情況的節點來處理我們的動畫播放。
AnimationTree(後文中稱其為動畫樹)節點是一個專門負責控制動畫播放的節點。可以通過組合各種資源來控制AnimationPlayer中的動畫播放、混合等操作。
在場景中加入AnimationTree節點之後,會出現感嘆號提示沒有設置AnimationPlayer引用。我們剛才看到,這個導入的Godot機器人確實有動畫,並且它的場景中也有個AnimationPlayer,我們現在也確實需要拿到它,但是它是作為一個整體節點出現在場景樹中的,我們如何拿到它裡面的那些東西呢?
不要忘了場景樹節點的右鍵菜單中的Make Local選項。我們可以直接把這個場景展開作為此場景的本地副本(不影響原本導入的場景)。現在就可以引用其中的AnimationPlayer(及其動畫)了。選中動畫樹節點後,在檢視面板找到Anim Player屬性,這裡你可以點擊選擇,也可以直接從場景樹中把AnimationPlayer拖過來。
設置好AnimationPlayer之後仍然還有個感嘆號,因為此時動畫樹的Tree Root(樹根)仍然是空的。動畫樹的節點是派生自AnimationNode的若干類的實例。Godot內置了幾種動畫節點可選。但是,這裡不要選AnimationRootNode,這個節點沒有實際功能。這裡我們選擇AnimationNodeStateMachine(狀態機節點)。其它幾種帶blend字樣的主要是用於混合動畫的,暫時不涉及。

什麼是狀態機

狀態機(state machine)是一種抽象機器。
狀態機由一個(通常是有限的)狀態集合和轉移函數確定。一個狀態機在一個時刻只能處於給定狀態集合中的一個狀態。一個狀態在滿足特定條件時可以轉換到另一個狀態。且通常在特定條件滿足時只能轉換到同一個狀態。
圖片借用維基百科

圖片借用維基百科

狀態機自然可以用有向圖表示,但是我們並不一定要用實現圖的方式去實現。很多東西都可以抽象成狀態機。
實際上我們在2D部分給玩家角色寫的代碼就是一個有限狀態機。我們根據不同狀態播放動畫,在特定情況下調整狀態。這裡的狀態機動畫節點就是控制動畫的來回切換。
狀態機無論是本身的概念還是實現都比較簡單,並且我們還可以讓另一個狀態機作為一個狀態機的狀態,這樣一來可以構造非常複雜的抽象機器,你可能已經想到了,這也是實現簡單遊戲AI的方法。

設置動畫狀態機

選中動畫樹節點之後,編輯器最下面的面板中就會出動畫樹編輯器的選項(也只有選中動畫樹節點的時候才會出現,說實話來回切換有點麻煩)。打開後目前只有一個Start(偽)狀態,指明狀態機的初始狀態。
我們首先配置一個Idle狀態作為初始狀態,這很合理。在編輯器中右鍵,Add Animation子菜單中即可選擇引用的AnimationPlayer中的動畫創建一個狀態。這裡還可以看到我剛才提到的,你可以新建另一個狀態機作為節點。
這裡選擇Idle動畫,一個叫Idle的狀態自動創建好了。現在Start狀態還沒有連接到任何狀態所以還不會播放動畫。
要連接各個狀態,需要選擇上方的連接節點,然後點擊某個狀態節點就可以看到連線出現了,選中另一個狀態就連接好了。如果你要回到選擇模式,要點那個指針圖標回去。
現在點擊狀態節點上面的播放按鈕就可以看到我們的機器人已經在播放Idle動畫了。
只有一個Idle動畫顯然沒什麼意思,我們再加入一個Run動畫。狀態機圖的動畫節點名稱默認是動畫名稱,你可以點擊名字修改。自然,我們需要在某些時候讓它從Idle狀態進入Run狀態,然後某些時候又回到Idle狀態。連線吧。
現在你的狀態機圖看起來應該是這樣的:

設置轉移條件

我們已經將Idle連到了Start上,現在的動畫樹實際上在開始時會無條件進入Idle狀態。除了Start這種特殊狀態,如果只是連線而不設置轉移條件的話,不僅沒什麼用,而且你還會看到很鬼畜的效果,不信你可以試一下。
對於轉移發生的條件,我們可以像之前那樣專門定義一個表示狀態的enum,也可以直接根據velocity屬性來判斷是否在移動。
先打個預防針。Godot的動畫樹放在現在來看雖然不能說簡陋(應該實現的功能還是有),但是它的很多設計和API給人的感覺還是有點彆扭。
在狀態機圖中,連接各狀態節點的連線本身也是一個可以配置的對象。點擊一條連線,檢視面板中展示了相關屬性。目前我們首先要關心的是Advance欄:
這不是什麼“高級設置”,不要看到它首先就無視了。畢竟它寫的是Advance不是Advanced。這裡的Advance應當是指“前進”,指狀態的變更。
第一個屬性為轉移的模式。在狀態機圖中連線的時候,默認設置為自動模式。自動模式的意思其實是,通過下面的Condition和Expression這兩個屬性來確定是否執行此狀態轉移。自動模式下,如果Condition(條件)和Expression(表達式)都沒設置,那意思就是可以無條件轉移到這個狀態。
接下來你會遇到第一個彆扭,那就是Condition到底應該怎麼設置怎麼用。條件?我要在這裡寫個已經定義了的變量名嗎?還是說寫一個表達式?等一下,不是下面還有個表達式嗎?怎麼那裡還有個&符號?
首先,Condition的類型是StringName,所以有個&符號。其實輸入普通的字符串就行了。我建議先看一眼文檔裡對這個屬性的描述再來看我寫的,因為我覺得它的描述和代碼還是有一定的誤導性。
實際上,你在condition處填寫的名稱,會被變成一個布爾類型的變量,此變量為真時,便會執行此轉移。但是!這個生成的變量的名字並不叫你寫在這裡的名字!
例如,我現在要設置從Idle到Run的轉移。我在這裡寫上moving。那麼在代碼中,為了執行這一轉移,我需要這樣寫(其中animation_tree是對動畫樹節點的引用):
animation_tree.set("parameters/conditions/moving", true)
啊?說好的變量呢?這麼這麼複雜。
首先set是一個定義在超級基類Object上的方法。當然,這個方法本身沒什麼神奇的,它的作用就是設置屬性的值。只不過我們通常都不會這樣寫而是直接用點和等號。
它的第一個參數就是屬性名。但是這裡的屬性名怎麼還有斜槓分開了?是指這個屬性其實是parameters.conditions.moving嗎?不!這個屬性的名字就叫parameters/conditions/moving!你可以print一下animation_treeget_property_list方法返回的所有屬性信息,你會看到很多名字是這種款式的屬性,都是動畫樹生成的。
這種特殊的屬性名稱決定了你沒法在代碼中直接用點來設置,不信你可以試試。一定程度上可能是為了防止動畫樹中的變量汙染你的名字空間造成衝突,還有個原因我們稍後就會看到。

在編輯器中測試動畫狀態機

設置好這樣一個條件變量後,我們在代碼中就可以測試動畫了。當然,每次都要啟動遊戲去測試動畫肯定有點麻煩。
在編輯器中,選中動畫樹節點後,在檢視面板中會有Parameters一欄:
這裡就出現了剛才在轉移條件處寫下的一個moving變量。這裡還有一個點值得注意。鼠標放到條件變量的名字上會顯示它在代碼中使用的完整名稱:
發現變量找不到或者不確定的時候可以確認一下,然後在檢視面板的屬性上也可右鍵複製粘貼屬性名(特別是這種動態生成的又臭又長的動畫樹變量)。
現在在動畫樹面板激活時,勾選或者不勾選這個變量就可以看到動畫的效果了,相對比較方便。

使用表達式轉移狀態

要從Run轉回到Idle,同樣要設置合適的條件。條件是什麼呢?當然,我們可以又設置一個idle變量,處於moving狀態時,當idle為真時就轉移回去。沒毛病。
這裡順便介紹Expression的用法。如果你發現僅僅是為了轉移回去就又定義一個變量有點麻煩,那麼可以直接寫一個表達式進去。比如,條件表達式設置為“沒有moving”。
表達式是一段可以求得值的代碼。但是求值要考慮表達式的環境/上下文。比如2 > 1在目前的初等數學或者說常識範圍內是公理,它不依賴任何外部狀態且恆真。但是velocity > Vector3.ZERO這樣的代碼只有在一個有velocity屬性的地方才是可以求值的。
所以說這個Advance的條件表達式要放在哪裡求值?是AnimationTree節點上還是它的上級場景/節點?
答案是這個動畫樹上。以剛才說的從Run轉移回Idle為例。我們要這樣寫:
!get("parameters/conditions/moving")
這個表達式求值時會在動畫樹上求值,所以直接調用get方法獲得moving的值就好了。取非就表示沒在moving時就轉移。
需要注意的是,很多時候動畫的參數會和遊戲玩法的代碼中的各個數值交互,所以你很有可能會需要訪問動畫樹上級場景的腳本中的變量。比如我們不用變量直接檢查速度:
get_parent().velocity > Vector.ZERO
需要多一個get_parent的調用(或者其它找到指定節點的方法)。

受擊動畫的問題

現在我假定你已經按照自己的方法配置好了基本的動畫狀態機,可以實現從Idle到Run的來回轉換。
我們之前也介紹瞭如何實現攻擊敵人。敵人被攻擊時,在視覺上也應當有相應的動畫來表現這一互動。
經過前面的講解,你應當可以非常自然地想到,可以再加一個Hurt狀態來播放Hurt動畫。但是這個問題沒那麼簡單。
首先一般來說,敵人只要還沒死,那麼任何狀態都可以被攻擊。也就是說,我們需要從每個狀態都拉一條線拉到Hurt上。受擊動畫較短,且只會在被攻擊的時候播放,播放完了還得回到之前狀態。如果這樣我們還得拉很多線回去。這樣一來你要處理的情況就翻倍了。
當然,這個問題很好解決。我們其實可以不給一個狀態連線,而是在特定情況下直接用代碼轉移狀態。編輯器中的AnimationNodeStateMachine是一種資源,但是且慢,遊戲在運行時要操作狀態機的話,需要獲得用另一個類型表示的對它的引用。
動畫樹為樹中的各動畫節點也生成了屬性。如果你沒給狀態機改名字,那麼你可以在動畫樹的parameters一欄裡看到一個叫Playback的東西,它在代碼中的名字是parameters/playback。通過它拿到一個AnimationNodeStateMachinePlayback類型的對象,表示狀態機的播放狀態。
這裡主要要提的就是它的travel方法。這個方法的作用就是轉移到指定方法。如果在狀態機圖中存在到目標狀態的(最短)路徑,那麼就會播放沿途的動畫。要解決前面提到的問題,主要利用travel也可以直接傳送(teleport)到一個沒有連接的孤島狀態上,這樣我們就可以不用挨個連線。
跳轉到Hurt狀態很容易,但是要回到之前的狀態又不那麼容易了。要獲得當前的狀態則需要調用get_currend_node,這樣可以記錄轉移前的狀態。但是相關的節點上並沒有暴露出狀態機發生狀態轉換時的信號,所以我們只能通過playback的get_current_play_positionget_current_length來判斷是否播放完了。也算不上特別好的辦法。
然而我們的問題還沒完。
首先,這種轉移非常突兀。受擊動畫通常是和其它狀態同時發生的。比如可以在移動的時候被打了,那麼它應該可以在保持跑動的狀態下同時疊加播放受擊動畫。比如下半身仍然在跑動,但是上半身抖一下什麼的。現在的配置,等於是本來在跑,然後被攻擊了,轉移到受擊動畫,開始完整播放受擊動畫,然後又回去。你可以看看自己配置的動畫。
另一方面,這個模型自帶的Hurt動畫,實際上是一個坐在地上垂頭喪氣的樣子。這樣一來跑動的時候被打就更突兀了!你很有可能會得到這樣的結果:
這顯然不太符合預期。

混合動畫

接下來要學習的技巧可以同時解決上述所有問題,是非常實際的解決方案。
為了解決這種死板的狀態轉移,我們要讓兩個動畫相互疊加。這個時候需要用到另一種動畫樹節點:AnimationNodeBlendTree
現在我們要刪掉之前作為動畫樹根節點的狀態機節點,選擇一個混合樹作為根節點。好在我們目前的狀態機不復雜,重做也沒啥問題。如果你已經做了更多的狀態,那麼只能說抱歉了。其實我嘗試了一下,這裡不太好重複利用之前的狀態機節點。混合樹中暴露出來的是一個AnimationNodeStateMachinePlayback槽,我把作為根節點的狀態機的Playback保存然後填進去好像也加載不出來。有興趣可以嘗試一下這種做法到底行不行。
混合樹的界面和狀態機有明顯不同。混合樹只有一個出口節點,並且必須要一個節點的輸出節點連接到這個輸出節點才能發揮作用。混合樹中可以包含多種節點,其中之一就是之前的狀態機。我們首先增加一個狀態機節點,點擊Open Editor(打開編輯器)就是我們已經熟悉的狀態機圖界面,然後復刻出之前的Idle和Run兩個基本狀態(不要Hurt)。可以看到編輯狀態機節點的內容時,由於當前的動畫樹已經出現了更復雜的層次(作為根節點的混合樹裡面還有一個較為複雜的狀態機節點),動畫樹面板上方顯示了當前位於樹的哪一個層次:
要回到根混合樹節點,點擊Root即可。此時我們的動畫應當回到了剛才只用狀態機節點的狀態。
接下來,加入OneShot節點,然後將StateMachine節點的輸出端口連到OneShot的輸入,最後讓OneShot的輸出連到最終的輸出節點的輸入端口。
在編輯混合樹時,我們要採用一種不同的視角。我們要把這個樹(圖)看作是動畫每一幀要做的事情。從左往右,一個節點的輸出可以產生一箇中間結果,然後作為另一個節點的輸入,最終一直到Output節點就得到了我們在某一幀中看到的動畫(其實到這裡已經和Unreal的動畫藍圖很像了)。

一次性動畫

接下來使用OneShot節點解決剛才的問題。OneShot顧名思義是一次性的,使用這個節點混合的動畫播放完之後就會結束。我們的受擊動畫正是這樣的一類動畫,捱打的時候播一下,然後該怎麼樣就繼續怎麼樣。這樣一來我們就不用把它放到狀態機裡和其它節點的連線打架了。
前面已經連上了OneShot的輸入端口,這裡輸入的東西會作為基本動作。接下來的要連上shot端口,shot端口中輸入的動作就是要播放的一次性動畫。這裡我們新建Animation節點,就是普通的動畫節點。新建之後,點擊上面的膠片圖標選擇Hurt動畫:
在動畫樹的檢視面板的Parameters欄中已經可以看到我們給混合樹所添加的節點生成的屬性。OneShot生成的屬性有幾種狀態。Fire就是播放,Abort就是淡出,FadeOut是淡出。我們在AnimationTree的檢視面板中選中Fire就可以看到它的效果,如果同時設置Moving屬性,可以看到fire這個OneShot之後會繼續跑。也可以直接在動畫樹編輯器中在各個節點上勾選變量或者選擇OneShot的請求來測試動畫。
在代碼中,要觸發OneShot就像這樣:
animation_tree.set("parameters/OneShot/request", AnimationNodeOneShot.ONE_SHOT_REQUEST_FIRE)
OneShot是你的具體的OneShot節點的名稱,可以在動畫樹編輯器中點擊節點名字修改,request是固定的。這個參數的類型是枚舉OneShotRequest,其取值就是在檢視面板參數測試部分可以選的三個值。

應用動畫到指定骨頭

現在我們只是解決了一些狀態會造成狀態機蜘蛛網的問題,而這個Hurt動畫看起來依然不夠自然,因為它會整個坐到地上。
我們只能另外去做一個動畫嗎?不,我們依然可以利用這個現成的動畫。
AnimationTree面板中的OneShot節點上還有一個Filter按鈕。此按鈕會打開一個骨骼樹菜單,並帶有勾選框。它的作用你可能已經猜到了,就是在混合動畫時,僅將此動畫應用到指定的骨頭上。這樣一來,我們就可以做到保持部分骨頭按照前一個動畫的動作來運動,而指定的骨頭則播放這個要混合的動畫。
首先我們要看一下當前操作的模型的骨骼結構。我們選中模型的Skeleton3D節點即可在3D視口中看到骨骼的狀況,檢視面板中也會顯示骨骼樹:
骨骼(skeleton)是由若干骨頭(bone)組成的。
例如我現在只想用Hurt動畫“垂頭喪氣那一部分”,也就是說只想要它下巴掉下來那一部分動畫。那麼我看到骨骼頭部以上的骨頭都是連接在spine.004這個骨頭上的。
現在回到混合樹中,選擇編輯過濾器。首先勾選spine.004,然後點擊Fill Selected Children(填滿選中骨頭的子骨頭),連接到它身上的所有骨頭都會自動勾選。最後勾選左上方的Enable Filtering(啟用過濾)即可:
現在隨便用Parameters中的選項或者啟動遊戲測試一下就可以看到效果了。
最後從Hurt變回Run的時候,頭動得貌似還是有點,“跳躍”。這裡我簡單調整一下這個OneShot的淡入淡出(FadeIn、FadeOut),就可以得到一個還將就的效果。
另一方面這個Hurt動畫對於受擊來說可能有點慢了。此時我們可以根據需要在混合樹中加入一個TimeScale節點即可調整播放速度。倍數大於1會加速播放,小於1就是減速。
動畫樹中還可以自由地組合各種節點來實現更為複雜的動畫效果,接下來就該你發揮了。
預祝大家新年快樂。

© 2022 3樓貓 下載APP 站點地圖 廣告合作:asmrly666@gmail.com