2020/11/09
というわけで、今回はAnimatorControllerへのモーション設定に関するお話です。
新しくboneを追加した時や.fbx
形式で取り込んでいる場合など、一度削除して再インポートしなければならないことは少なからずあります。その度にAnimatorControllerの各ステートへ再設定するのは面倒です。
そこで「モデルのアニメーションデータをAnimatorControllerに割り当てなおす」のを実行時にできないかと考えてみたお話です。
せっかちな方のために冒頭でまとめておきますが、
・AssetDatabase.GetAssetPath()
でアセットのパスを取得
・LoadAllAssetsAtPath()
でモデル内のアニメーションデータ取得
・AnimatorController
からステートを取得し一致するモーションを設定
という感じです。
以下、順番に詳細を書いていきます。
作成クラス
今回作成したクラスのソースコードです。これをキャラクターのオブジェクトに設定します。
using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEditor.Animations; using UnityEngine; public class AnimationAllocator : MonoBehaviour { void Start() { Animator animator = gameObject.GetComponent<Animator>(); // パス取得 string model_path = AssetDatabase.GetAssetPath(animator.avatar); string controller_path = AssetDatabase.GetAssetPath(animator.runtimeAnimatorController); // モデルのAnimationClip取得 Object[] assets = AssetDatabase.LoadAllAssetsAtPath(model_path); List<AnimationClip> modelAnimationClips = assets .Where(a => a.GetType() == typeof(AnimationClip)) .Select(a => (AnimationClip)a) .ToList(); // コントローラの各ステート取得 AnimatorController animatorController = AssetDatabase.LoadAssetAtPath<AnimatorController>(controller_path); List<ChildAnimatorState> states = animatorController.layers[0].stateMachine.states.ToList(); foreach (var modelAnimationClip in modelAnimationClips) { ChildAnimatorState childState = states.Find(s => s.state.name == modelAnimationClip.name); if (childState.state != null) { childState.state.motion = modelAnimationClip; } } } }
各アセットのパス取得
モデルと設定先のAnimatorControllerは既にインポート・作成済みのものを使用します。それをUnityEditor
側の機能であれこれいじるのであれば、該当アセットまでのパスが必要です。
パスを直書きするのは避けたいので、実行時のインスタンスからパスを取得するようにします。AssetDatabase.GetAssetPath()
を使うといけるみたいです。
https://docs.unity3d.com/ja/current/ScriptReference/AssetDatabase.GetAssetPath.html
Animator animator = gameObject.GetComponent<Animator>(); string model_path = AssetDatabase.GetAssetPath(animator.avatar); string controller_path = AssetDatabase.GetAssetPath(animator.runtimeAnimatorController);
using
にUnityEditor
を追加する必要があるので注意します。VisualStudioならクラス名入力後にAlt+Enter
→Enter
でサクっと入れてくれます。
モデルパスはAnimator.avatar
、AnimatorControllerはAnimator.runtimeAnimatorController
をGetAssetPath()
の引数に指定することで取得できます。単にAnimator
のインスタンスを渡しただけでは何も取ってきてくれません。
ちなみにGetAssetPath()
ですが、取得できなかった場合は「アクセスできないアセット」か「存在しないアセット」かで返り値が異なるようです。エラー処理を拘りたい人は参考にしてみてください。
モデルのアニメーションデータ取得
モデルのパスをAssetDatabase.LoadAllAssetsAtPath()
の引数に指定すると取ってこれるとのこと。
ただし、インポートしたモデルの状態によってはメッシュなどの不純物まで取ってきてしまうので、LINQのWhere()
を使ってAnimatorClip
型のオブジェクトに限定して取得するようにします。
その後はSelect()
を使った射影でAnimationClip
型にキャストし、扱いやすいようにList型にしておきます。
Object[] assets = AssetDatabase.LoadAllAssetsAtPath(model_path); List<AnimationClip> modelAnimationClips = assets .Where(a => a.GetType() == typeof(AnimationClip)) .Select(a => (AnimationClip)a) .ToList();
AnimatorControllerのステート取得
UnityEditor.Animations
名前空間にAnimatorController
というまんまなクラスがあり、そこからコントローラの各要素を参照できます。
レイヤーを1つしか使っていない場合は特に難しくありません。
AnimatorController animatorController = AssetDatabase.LoadAssetAtPath<AnimatorController>(controller_path); List<ChildAnimatorState> states = animatorController.layers[0].stateMachine.states.ToList();
各ステートへのアニメーション割り当て
モデルアニメーション名がコントローラのステート名と一致するか順番に調べ、一致したステートのMotion
にAnimationClip
を放り込みます。
パッと思い付く方法はダブルforeach
ですが、ここはモデルアニメーションをforeach
で回し、一致するステートの判定・取得にはLINQのFind()
を使います。
foreach (var modelAnimationClip in modelAnimationClips) { ChildAnimatorState childState = states.Find(s => s.state.name == modelAnimationClip.name); if (childState.state != null) { childState.state.motion = modelAnimationClip; } }
アニメーションデータ取得に続いてLINQが大活躍です。初見ではラムダ式と相まって理解しにくいものですが、ある程度分かってくると非常に便利です。痒いところに手が届くとはこのことですね。
開発時は便利ですが、リリースする際は外さないとオーバヘッドになります。というかUnityEditor
を使っているので、Unity上でないと動きません。プリプロセッサ ディレクティブ (#if~#endif
) で括っちゃうのも手ですね。
また、割り当てたアニメーションはゲーム再生を停止しても解除されないようです。初回だけでいいのならエディタ拡張とかで作ったほうがよさそうでしたが、そこまで頑張れる気力はなかったので、別の機会に改めて挑戦してみます。