ゴマちゃんフロンティア

アザラシが大好きなエンジニアの開発日記です

【Unity】スクリプトからアニメーションをAnimatorControllerの各ステートへ設定する方法

time 2017/11/30

というわけで、今回は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);

usingUnityEditorを追加する必要があるので注意します。VisualStudioならクラス名入力後にAlt+EnterEnterでサクっと入れてくれます。
モデルパスはAnimator.avatar、AnimatorControllerはAnimator.runtimeAnimatorControllerGetAssetPath()の引数に指定することで取得できます。単に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();

各ステートへのアニメーション割り当て

モデルアニメーション名がコントローラのステート名と一致するか順番に調べ、一致したステートのMotionAnimationClipを放り込みます。
パッと思い付く方法はダブル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) で括っちゃうのも手ですね。
また、割り当てたアニメーションはゲーム再生を停止しても解除されないようです。初回だけでいいのならエディタ拡張とかで作ったほうがよさそうでしたが、そこまで頑張れる気力はなかったので、別の機会に改めて挑戦してみます。

down

コメントする