使用 AnimationTree
前言
透過 AnimationPlayer,Godot 擁有遊戲引擎中相當靈活的動畫系統之一。它幾乎可以為任何節點或資源上的任意屬性建立動畫,並具備專用的 Transform、Bezier、函式呼叫、音訊與子動畫軌道等能力,十分獨特。
不過,透過 AnimationPlayer 混合動畫的能力較為有限,只能設定固定的交叉淡化過場時間。
AnimationTree 是 Godot 3.1 引入用於處理進階轉場的節點。它取代了舊的 AnimationTreePlayer,並帶來大量功能與更高的彈性。
AnimationTree 與 AnimationPlayer
開始之前,請先了解 AnimationTree 節點本身不包含動畫,它會使用 AnimationPlayer 節點中的動畫。你在 AnimationPlayer 中建立、編輯或匯入動畫,再用 AnimationTree 來控制播放。
AnimationPlayer 與 AnimationTree 可用於 2D 與 3D 場景。匯入 3D 場景及其動畫時,你可以使用 名稱後綴 來簡化流程並以正確屬性匯入。最後,匯入的 Godot 場景會在一個 AnimationPlayer 節點中包含這些動畫。由於在 Godot 中很少直接使用匯入場景(通常會實例化或繼承),你可以在包含該匯入場景的新場景中放置一個 AnimationTree 節點。之後,將該 AnimationTree 指向匯入場景中所建立的 AnimationPlayer 。
作為參考,第三人稱射擊範例 就是這樣配置的:
這裡為玩家建立了一個以 CharacterBody3D 為根節點的新場景。場景中產生實體原本的 .dae``(Collada)檔案,並新增一個 ``AnimationTree 節點。
建立樹狀結構
要使用 AnimationTree,你需要設定一個根節點。動畫根節點是一種用來包含並評估子節點、輸出動畫的類別。子節點有三種類型:
動畫節點,會從已連結的
AnimationPlayer取得動畫來使用。動畫根節點:用於混合子節點,且可以巢狀。
動畫混合節點:用於
AnimationNodeBlendTree(一個 2D 節點圖) 中。混合節點具有多個輸入端子,並輸出單一端子。
可用的根節點類型有:
AnimationNodeAnimation:從列表中選擇並播放一段動畫。這是最簡單的節點,通常不會直接用作根節點。AnimationNodeBlendTree:在圖表中包含多個子節點。提供許多混合節點,例如 mix、blend2、blend3、one shot 等。AnimationNodeBlendSpace1D:允許在一維空間中於多個動畫節點間做線性混合。你可在 1D 混合空間中控制位置來混合動畫。AnimationNodeBlendSpace2D:允許在二維空間中於三個或以上的動畫節點間做線性混合。你可在 2D 混合空間中控制位置來混合動畫。AnimationNodeStateMachine:在圖表中包含多個子節點。每個節點代表一個狀態,並提供多種在狀態間切換的機制。
混合樹
建立 AnimationNodeBlendTree 後,會在下方面板的 AnimationTree 分頁中得到一個空的 2D 圖表,預設只包含一個 Output 節點。
要讓動畫播放,必須有節點連到輸出端。你可以透過 Add Node.. 功能表或在空白處按右鍵來加入節點:
最簡單的做法是直接把一個 Animation 節點連到輸出,這樣就會播放該動畫。
以下是其他可用節點的說明:
混合2/混合3
這些節點將通過使用者指定輸入的兩個或三個混合值之間進行混合:
混合可使用 濾鏡 來個別控制哪些軌道要被混合、哪些不混合。這對於做動畫分層非常有用。
若需要更複雜的混合,建議改用混合空間(Blend Space)。
OneShot
此節點會執行一次動畫並在結束時返回。你可以自訂淡入淡出時間與濾鏡。
# Play child animation connected to "shot" port.
animation_tree.set("parameters/OneShot/request", AnimationNodeOneShot.ONE_SHOT_REQUEST_FIRE)
# Alternative syntax (same result).
animation_tree["parameters/OneShot/request"] = AnimationNodeOneShot.ONE_SHOT_REQUEST_FIRE
# Abort child animation connected to "shot" port.
animation_tree.set("parameters/OneShot/request", AnimationNodeOneShot.ONE_SHOT_REQUEST_ABORT)
# Alternative syntax (same result).
animation_tree["parameters/OneShot/request"] = AnimationNodeOneShot.ONE_SHOT_REQUEST_ABORT
# Get current state (read-only).
animation_tree.get("parameters/OneShot/active"))
# Alternative syntax (same result).
animation_tree["parameters/OneShot/active"]
// Play child animation connected to "shot" port.
animationTree.Set("parameters/OneShot/request", (int)AnimationNodeOneShot.OneShotRequest.Fire);
// Abort child animation connected to "shot" port.
animationTree.Set("parameters/OneShot/request", (int)AnimationNodeOneShot.OneShotRequest.Abort);
// Get current state (read-only).
animationTree.Get("parameters/OneShot/active");
時間縮放
此節點可在連到其 in 輸入的動畫中尋址到特定時間點。用它可以從某個播放位置開始播放一段 Animation。注意尋址的值以秒為單位,若要從開頭播放請設為 0.0,若要從第 3 秒開始播放請設為 3.0。
# Play child animation from the start.
animation_tree.set("parameters/TimeSeek/seek_request", 0.0)
# Alternative syntax (same result).
animation_tree["parameters/TimeSeek/seek_request"] = 0.0
# Play child animation from 12 second timestamp.
animation_tree.set("parameters/TimeSeek/seek_request", 12.0)
# Alternative syntax (same result).
animation_tree["parameters/TimeSeek/seek_request"] = 12.0
// Play child animation from the start.
animationTree.Set("parameters/TimeSeek/seek_request", 0.0);
// Play child animation from 12 second timestamp.
animationTree.Set("parameters/TimeSeek/seek_request", 12.0);
時間縮放
此節點可調整連到其 in 輸入的動畫速度。動畫速度會乘上 scale 參數的數值;設為 0 會暫停動畫;設為負值則會反向播放。
轉場效果
此節點是狀態機的簡化版。你將動畫連到各個輸入端,當前狀態索引決定要播放哪個動畫。你可以指定交叉淡化的過場時間。於屬性檢視器中也可更改輸入端數量、重新排列或刪除輸入端。
# Play child animation connected to "state_2" port.
animation_tree.set("parameters/Transition/transition_request", "state_2")
# Alternative syntax (same result).
animation_tree["parameters/Transition/transition_request"] = "state_2"
# Get current state name (read-only).
animation_tree.get("parameters/Transition/current_state")
# Alternative syntax (same result).
animation_tree["parameters/Transition/current_state"]
# Get current state index (read-only).
animation_tree.get("parameters/Transition/current_index"))
# Alternative syntax (same result).
animation_tree["parameters/Transition/current_index"]
// Play child animation connected to "state_2" port.
animationTree.Set("parameters/Transition/transition_request", "state_2");
// Get current state name (read-only).
animationTree.Get("parameters/Transition/current_state");
// Get current state index (read-only).
animationTree.Get("parameters/Transition/current_index");
狀態機
建立 AnimationNodeStateMachine 後,會在下方面板的 AnimationTree 分頁得到一個空的 2D 圖表,預設包含 Start 與 End 兩個狀態。
要新增狀態,請按右鍵或使用 建立新節點 按鈕 (方框內加號圖示) 。你可以加入動畫、混合空間、混合樹,甚至是另一個狀態機。要編輯這些較複雜的子節點,點選該狀態右側的鉛筆圖示。要回到最上層狀態機,點選面板左上角的 Root。
在狀態機能發揮作用前,必須以轉場把各個狀態連起來。要新增轉場,按下 連結節點 按鈕 (右向箭頭的線條圖示) ,然後在兩個狀態間拖曳建立。你可以在兩個狀態間建立兩條轉場,分別對應兩個方向。
轉場類型有三種:
立即 :會立刻切換到下一個狀態。
Sync (同步):立即切換到下一個狀態,但會將新狀態快進並到舊狀態的播放位置。
At End (末尾):將等待目前狀態播放結束,然後切換到下一個狀態動畫的開頭。
轉場也有一些屬性。點選任一轉場後,可在屬性檢視器中看到其設定:
Xfade Time (疊化時間)是在這個狀態和下一個狀態之間交叉漸變的時間。
Xfade Curve:以曲線而非線性方式進行交叉淡化。
Reset:決定切換進入的狀態是否從頭開始播放(true)或保持當前進度(false)。
Priority (優先順序)與程式碼中的
travel()函式一起使用(後述)。當從一個狀態到另一個狀態時,會優先使用優先順序較低的過渡。Switch Mode:即轉場類型(見上)。建立後仍可於此更改。
Advance Mode:決定推進模式。若為
Disabled,則不使用該轉場;若為Enabled,則只會在travel()期間使用;若為Auto,則在推進條件與推進運算式成立,或沒有設定任何條件/運算式時使用。
Advance Condition 與 Advance Expression
狀態機轉場的最後兩個屬性是 Advance Condition 與 Advance Expression。當 Advance Mode 設為 Auto 時,這兩者決定轉場是否會推進。
Advance Condition 是布林檢查。你可在文字欄位填入自訂變數名稱,當狀態機到達此轉場時,會檢查該變數是否為 true。若為 true,則進行轉場。請注意,Advance Condition 只會 檢查變數是否為 true,無法檢查為 false 的情況。
這使得 Advance Condition 的能力相當受限。若你想基於同一個屬性在兩個狀態間來回切換,就得建立兩個互為相反值的變數,並檢查其中任一是否為 true。這也是為什麼在 Godot 4 中加入了 Advance Expression。
Advance Expression 與 Advance Condition 類似,但它不是檢查單一變數為 true,而是評估任意運算式。運算式可以是你會放在 if 敘述中的任何條件。以下是可用於 Advance Expression 的範例:
is_walkingis_walking== trueis_walking && !is_idlevelocity > 0player.is_on_floor()
以下是使用 Advance Condition 設定不當的狀態機轉場範例:
這樣無法運作,因為 Advance Condition 中包含了 ! 的變數,無法被檢查。
以下是同一個情境的正確作法,使用兩個相反值的變數:
以下是同一個情境改用 Advance Expression 的作法,就不需要兩個變數了:
要使用 Advance Expression,必須在 AnimationTree 節點的屬性檢視器中設定 Advance Expression Base Node。預設為 AnimationTree 節點本身,但應指向實際持有動畫變數腳本的那個節點。
狀態機 travel
Godot 的 StateMachine 有個相當方便的能力:travel。你可以指示圖表從目前狀態前往另一個狀態,並依序經過所有中間狀態。這是透過 A* 演算法完成的。若從目前狀態到目標狀態之間不存在轉場路徑,圖表就會直接傳送到目標狀態。
要使用 travel 能力,應先自 AnimationTree 節點取得 AnimationNodeStateMachinePlayback 物件(它以屬性形式匯出),接著呼叫其中的相關方法:
var state_machine = animation_tree["parameters/playback"]
state_machine.travel("SomeState")
AnimationNodeStateMachinePlayback stateMachine = (AnimationNodeStateMachinePlayback)animationTree.Get("parameters/playback");
stateMachine.Travel("SomeState");
在使用 travel 前,狀態機必須已開始運作。請確保你已呼叫 start(),或已連結某節點到 Start。
BlendSpace2D 與 BlendSpace1D
BlendSpace2D 是在二維空間進行進階混合的節點。你會將代表各動畫的點加入 2D 空間中,並控制空間中的位置來決定混合結果:
你可以在圖表上按右鍵或使用 新增點 按鈕 (鋼筆與點的圖示) 把點放在任意位置。無論你把點放在哪裡,兩兩之間的三角形都會以 Delaunay 自動生成。你也可以設定並標示 X 與 Y 的範圍。
最後,你也可以更改混合模式。預設情況下,混合會在最近的三角形內進行插值。處理逐格的 2D 動畫時,你可能會想切換到 Discrete 模式。或者,若你希望在離散動畫間切換時保留目前的播放進度,可使用 Carry 模式。以上模式可在 Blend 功能表中更改:
BlendSpace1D 的概念與 BlendSpace2D 相同,但在一維空間(線段)上運作,且不涉及三角形。
為了更好的混合
在 Godot 4.0+ 中,為了使混合結果具有確定性(可重現且始終一致),混合屬性值必須具有特定的初始值。例如,在要混合兩個動畫的情況下,如果一個動畫具有屬性軌跡而另一個動畫沒有,則計算混合動畫時就好像後一個動畫具有具有初始值的屬性軌跡一樣。
當對 Skeleton3D 骨骼使用位置/旋轉/縮放 3D 軌跡時,初始值為 Bone Rest。對於其他屬性,初始值為“0”,如果軌道存在於“RESET”動畫中,則使用其第一個關鍵影格的值。
例如,下面的AnimationPlayer有兩個動畫,但其中一個缺少Position的Property軌道。
這意味著缺少的動畫會將這些位置視為「Vector2(0, 0)」。
這個問題可以透過加入一個位置屬性軌跡作為「RESET」動畫的初始值來解決。
備註
請注意,「RESET」動畫的存在是為了定義最初載入物件時的預設姿勢。假設它只有一影格,並且預計不會使用時間軸進行播放。
此外請留意:若 3D 旋轉軌道與 2D 旋轉的屬性軌道的插值型態設為 Linear Angle 或 Cubic Angle,混合動畫將會避免從初始值出現超過 180 度的旋轉。
這對於 Skeleton3D 很有用,可以在混合動畫時防止骨頭穿透身體。因此,Skeleton3D 的 Bone Rest 值應盡可能接近可移動範圍的中點。 這意味著對於人形模型,最好以 T 形姿勢匯入它們。
您可以看到,優先考慮骨骼休息的最短旋轉路徑,而不是動畫之間的最短旋轉路徑。
如果您需要透過混合動畫進行運動來將 Skeleton3D 本身旋轉超過 180 度,則可以使用 Root Motion。
根骨骼運動
處理 3D 動畫時,一種流行的技術是動畫師利用根骨骼為其餘部分骨骼製作運動動畫。這使得動畫角色的腳步與下面的地板相配對。並且允許在電影拍攝期間與物體進行精確的互動。
在 Godot 中重播動畫時,可以選擇這根骨骼作為*根運動軌跡*。這會在視覺上取消這根骨骼的變換(動畫將保持原狀)。
然後, 實際運動可以通過 AnimationTree API 作為轉換:
# Get the motion delta.
animation_tree.get_root_motion_position()
animation_tree.get_root_motion_rotation()
animation_tree.get_root_motion_scale()
# Get the actual blended value of the animation.
animation_tree.get_root_motion_position_accumulator()
animation_tree.get_root_motion_rotation_accumulator()
animation_tree.get_root_motion_scale_accumulator()
// Get the motion delta.
animationTree.GetRootMotionPosition();
animationTree.GetRootMotionRotation();
animationTree.GetRootMotionScale();
// Get the actual blended value of the animation.
animationTree.GetRootMotionPositionAccumulator();
animationTree.GetRootMotionRotationAccumulator();
animationTree.GetRootMotionScaleAccumulator();
可以提供給 CharacterBody3D.move_and_slide 等函式,用來控制角色移動。
另外還有一個工具節點 RootMotionView,你可以把它放在場景中,作為角色與動畫的自訂地面(此節點在遊戲執行時預設為停用)。
使用程式碼控制
建立樹和預覽之後,只剩下一個問題:“如何使用程式碼控制所有的節點?”。
請記住動畫節點只是資源,因此會在所有使用它們的實例之間共享。直接修改節點中的值會影響所有使用該 AnimationTree 的場景實例。這通常不是你想要的,但也有一些不錯的應用,例如可以複製/貼上動畫樹的部分結構,或在不同的動畫樹中重複使用複雜的節點(如狀態機或混合空間)。
實際的動畫封包含在 AnimationTree 節點中, 並通過屬性存取. 檢查 AnimationTree 節點的 "參數" 部分, 查看所有可以即時修改的參數:
這很方便,因為你可以從 AnimationPlayer 甚至 AnimationTree 本身來為它們建立動畫,從而實現非常複雜的動畫邏輯。
若要在程式碼中修改這些數值,你必須取得屬性路徑。將滑鼠游標懸停在任一參數上即可查看:
接著你就能讀寫它們:
animation_tree.set("parameters/eye_blend/blend_amount", 1.0)
# Alternate syntax (same result)
animation_tree["parameters/eye_blend/blend_amount"] = 1.0
animationTree.Set("parameters/eye_blend/blend_amount", 1.0);
備註
狀態機的 Advance Expression 不會出現在參數底下,因為它們儲存在 AnimationTree 之外的腳本中;而 Advance Conditions 則會出現在參數中。