Unity Copy Paste Animator Transitions
2020-12-26 04:53:00
1.6k words / 6-minute read
目錄 Table of contents
  1. 前言
  2. 實作
    1. 1. Unity專案重啟後那些transition就沒了
    2. 2. AnyState, EntryState, and ExitState
    3. 3. Transition with State Machines
      1. 2022/11/19更新
      2. AnimatorStateMachine.GetStateMachineTransitions()
  3. 成果
    1. Copy selected transitions
    2. Copy all transitions of selected state
    3. AnyState, EntryState, and ExitState
  4. 後話
  5. 專案網址

前言

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拿的到選取的AnimatorStateTransitionsAnimatorState,這兩個的關係甚麼呢?我翻一下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.lastActiveSceneViewGUILayoutUtility.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