目錄 Table of contents
前言
RT,我因為要修改人物動畫而需要調整Animator State,一般來說可能是要將一個動畫clip切成多個階段(如Intro -> Loop -> Outro),又或者是我打算把一個clip state改成blend tree以方便用參數調整動畫。
而主要問題就是Unity從來不提供複製Transition的功能。
這件事在專案開發初期還好,反正本來也就幾條transition而已我就手動加。但修改動畫狀態機這件事其實一再發生,直到今天我的人物狀態機已經複雜到不行了,簡單描述如下:
- 幾乎每個狀態都有6 ~ 8個transition
- transition中可能有1 ~ 3個condition
- 包含大量跨sub state machine的連結
(這部分我也有架構上的問題沒解決才導致狀態機如此混亂)
但我真的受不了要自己重拉線這件事了,誰知道未來還有多少次這種折磨。
不過我其實google了好幾次都沒找到有人做過這種工具,於是就有了這次的實作。
實作
我理想中的使用方法是可以直接拿到AnimatorWindow的狀態來做複製貼上。
首先測試了一下Selection.objects
拿的到選取的AnimatorStateTransitions
和AnimatorState
,這兩個的關係甚麼呢?我翻一下AnimatorController
內的所有屬性,整個階層架構展開大概長這樣:
AnimatorController
|-AnimatorStateMachine
|-layers (AnimatorLayer)
|-states (AnimatorState)
|-transitions (AnimatorTransition)
|-destinationState (AnimatorState)
|-subStateMachines (StateMachine)
...
注意AnimatorTransition
本身不包含他的source state,所以我用Selection拿到的transition時也拿不到他的source,這樣就無法複製Ingoing transition了。
因此我只好直接暴力搜尋當前layer內所有的transition來找source state。
不過有個問題是我實在找不到取得AnimatorWindow當前的AnimatorController以及Layer的方法,本來我也decompile了UnityEditor.dll
且在裡面找到這兩個field:
internal static AnimatorController lastActiveController = null;
internal static int lastActiveLayerIndex = 0;
想說這看起來跟SceneView.lastActiveSceneView
和GUILayoutUtility.GetLastRect()
的命名邏輯很像,應該可以拿到最近一次focus的Controller了。
但用了Reflection.GetValue拿到的始終是null
,只好作罷。
於是改成了使用者需要自己設定好目標AnimatorController以及Layer才能開始複製貼上,弄這個也多花時間在做錯誤處理(防範使用者複製貼上到不同的controller或layer)。
老實說剩下就沒什麼好紀錄的,頂多就一些小細節,如資訊要完整複製、Transition的order要維持或支援undo/redo這種,畢竟就只是很單純的寫一個EditorWindow的工具而已。
---- 隔了一天的更新 20/12/26 ----
才怪,有幾件非常重要的事情我沒處理。
1. Unity專案重啟後那些transition就沒了
這件事很嚴重,因為這樣根本不能用,害我馬上把github專案改回private緊急維修。
原因很單純,那些AnimatorStateTransition根本沒有存進專案內(AnimatorController內部)。我原本是直接用constructor new了一個出來,如下:
var transition = new AnimatorStateTransition();
state.AddTransition(transition);
我本以為那個AddTransition
的方法已經會幫我處理這塊了,結果並沒有。因為內部是長這樣:
/// <summary>
/// <para>Utility function to add an outgoing transition.</para>
/// </summary>
/// <param name="transition">The transition to add.</param>
public void AddTransition(AnimatorStateTransition transition)
{
this.undoHandler.DoUndo(this, "Transition added");
AnimatorStateTransition[] transitions = this.transitions;
ArrayUtility.Add<AnimatorStateTransition>(ref transitions, transition);
this.transitions = transitions;
}
而其他參數的多載方法是長這樣:
public AnimatorStateTransition AddTransition(AnimatorStateMachine destinationStateMachine)
{
AnimatorStateTransition animatorStateTransition = this.CreateTransition(false);
animatorStateTransition.destinationStateMachine = destinationStateMachine;
this.AddTransition(animatorStateTransition);
return animatorStateTransition;
}
可以發現它有一個Create方法來產生Transition,裡面是:
private AnimatorStateTransition CreateTransition(bool setDefaultExitTime)
{
AnimatorStateTransition animatorStateTransition = new AnimatorStateTransition();
animatorStateTransition.hasExitTime = false;
animatorStateTransition.hasFixedDuration = true;
bool flag = AssetDatabase.GetAssetPath(this) != "";
if (flag)
{
AssetDatabase.AddObjectToAsset(animatorStateTransition, AssetDatabase.GetAssetPath(this));
}
animatorStateTransition.hideFlags = HideFlags.HideInHierarchy;
if (setDefaultExitTime)
{
this.SetDefaultTransitionExitTime(ref animatorStateTransition);
}
return animatorStateTransition;
}
flag
那個判斷式就是重點了,他在確認目前的AnimatorState
有存在於專案之中後會把這個transition物件塞進去。
把這部分處理完之後就解決這問題了。
2. AnyState, EntryState, and ExitState
這三個State都沒有實體物件存在,所以我的舊版本都沒處理它們。
這三種State都有個特性是存在於每個StateMachine之中,起初我還在煩惱要區分不同stateMachine的實體很麻煩,但後來想想 跨state machine連結這三個state根本沒意義 ,於是就簡單很多了。
實作後發現有個小坑: AnyState的Transitions只存在於Layer第一層的AnimtorStateMachine.anyTransitions內,並不受sub state machine管理。
3. Transition with State Machines
事實上不只有AnimatorState可以互相連結,你也可以在編輯器中連結StateMachine以及State或是StateMachine彼此互聯。
但是,我實在找不到取得StateMachine的outgoing transitions的方法,完全翻不到相關欄位屬性方法。唯一比較像的是GetStateMachineTransitions()
,但不管怎麼使用都是拿到空陣列[0]就作罷。
結論就…放生吧,事實上我的使用情境是完全沒用到StateMachine的連結啦,所以應用上很OK了。
2022/11/19更新
過了兩年,收到一個Pull Request#3,他加了一個小功能支援State -> StateMachine的transition,只要透過Transition屬性中的 destinationStateMachine
即可做到。
我在順手修一些陳舊爛code的時候順便重新審視了整個實作,而這次我經過一些測試後總算搞懂 AnimatorStateMachine.GetStateMachineTransitions() 的用法了,官方文檔真的是爛斃了。
AnimatorStateMachine.GetStateMachineTransitions()
public AnimatorTransition[] GetStateMachineTransitions(Animations.AnimatorStateMachine sourceStateMachine);
這個方法最讓我疑惑的點是「他是一個AnimatorStateMachine的成員方法然後又需要傳入參數AnimatorStateMachine」,到底這兩個AnimatorStateMachine是代表什麼?
總之,經過測試加上這段code之後確認了一件事:「如果你要取得stateMachineA的outgoing transitions,你要透過其parent state machine來呼叫方法,並傳入stateMachineA作為參數。」
transitions = parentStateMachine.GetStateMachineTransitions(stateMachine);
搞懂這個之後,全功能總算是做完了PR#4。
成果
Copy selected transitions
Copy all transitions of selected state
還挺滿意的~ 除了EditorWindow沒辦法即時刷新小可惜(要滑鼠滑上去才更新)。
– 20/12/26更新 –
AnyState, EntryState, and ExitState
後話
功能做出來也花不到4小時吧,剩下時間也只是在修UI跟一些錯誤處理。我實在搞不懂Unity官方為什麼不做這個功能,我相信他們工程師要搞的話一天內就能做好整個功能包測試。
更新後又花了快半天,那我覺得Unity官方至少一個禮拜內能搞定吧。
專案網址
https://github.com/qwe321qwe321qwe321/Unity-AnimatorTransitionCopier