UnityEditorを使って2D格闘(2D Fighting game)作るときのモーション遷移図作成の半自動化に挑戦しよう<その2>

  • 0
    いいね
  • 0
    コメント

    201701221033a52b.png
    CSVをエクスポートするウィンドウを作ってみた。

    201701221033a51.png
    これ、白い矢印の線(トランジション)の一覧。不完璧でよければ データは引っこ抜けるのだが……。

    ソース公開中 Open source

    201701220440gif34.gif

    https://github.com/muzudho/KifuwarabeFighter2

    もっといい方法があるんじゃないか?

    UnityEditor を使って ループで回しながら データを1つ1つ拾っているんだが 鈍臭い。
    メモリ・イメージ を1発で リレーショナル・データベース形式に エクスポート、インポート できないか あとで探しておく。

    知りたいことは、
    (1)公式に有るか
    (2)誰か作ってるか
    (3)自分で作るのか
    で、
    もし3なら

    (4)なにまで無料か
    (5)なにまで有料か
    (6)なにから禁止か
    だ。

    UnityEditor は無料版でも使える機能でオープンだ。ここでは 鈍臭い 方法を説明する。

    各部名称

    アニメーター

    201701221033a52c.png
    これ、アニメーター。

    レイヤー

    201701221033a52e.png
    これ、レイヤーの一覧。 上図では [Base Layer] の1つしか使っていない。

    ステートマシン

    201701221033a52f.png
    これ、ステートマシン。

    201701230746a2.png
    この六角形のも ステートマシン が入っているので、ステートマシンというのは 親子の入れ子構造になっている。

    ステート

    201701230746a3b.png
    この四角いのが ステート。

    トランジション

    201701230746a4b.png
    ステート と ステート をつないでいるのが、トランジション(白い矢印)だ。
    ステートマシン(六角形)のやつ ともくっつく。

    コンディション

    201701230746a5b.png
    トランジションは、コンディションを持っている。[Inspector]で見れるやつだ。

    これらの部品にアクセスできれば、C#スクリプトから編集できるということだ。

    鈍臭いスキャン部分の抜粋

    このまま書いても動かないが、全文はオープンソースなので Git Hub から拾って欲しい。

    // アニメーター・コントローラーの取得例。
    // AnimatorController ac = (AnimatorController)AssetDatabase.LoadAssetAtPath<AnimatorController>("Assets/Resources/AnimatorControllers/AniCon@Char3.controller");
    
        void ScanRecursive(List<AnimatorStateMachine> aStateMachineList, AnimatorStateMachine aStateMachine)
        {
            aStateMachineList.Add(aStateMachine);
    
            foreach (ChildAnimatorStateMachine caStateMachine in aStateMachine.stateMachines)
            {
                ScanRecursive(aStateMachineList, caStateMachine.stateMachine);
            }
        }
    
        void ScanAnimatorController(AnimatorController ac, out string resultMessage)
        {
            Debug.Log("States Scanning...☆(^~^)");
            layersTabel.Clear();
            statesTabel.Clear();
            transitionsTabel.Clear();
            conditionsTabel.Clear();
            positionsTabel.Clear();
    
            foreach (AnimatorControllerLayer layer in ac.layers)//レイヤー
            {
                LayerRecord layerRecord = new LayerRecord(layersTabel.Count, layer);
                layersTabel.Add(layerRecord);
    
                // ステート・マシン
                List<AnimatorStateMachine> stateMachineList = new List<AnimatorStateMachine>();
                ScanRecursive(stateMachineList, layer.stateMachine);
                foreach (AnimatorStateMachine stateMachine in stateMachineList)
                {
                    MachineStateRecord stateMachineRecord = new MachineStateRecord(layersTabel.Count, stateMachinesTabel.Count, stateMachine, positionsTabel);
                    stateMachinesTabel.Add(stateMachineRecord);
    
                    foreach (ChildAnimatorState caState in stateMachine.states)
                    {
                        StateRecord stateRecord = new StateRecord(layersTabel.Count, stateMachinesTabel.Count, statesTabel.Count, caState, positionsTabel);
                        statesTabel.Add(stateRecord);
    
                        foreach (AnimatorStateTransition transition in caState.state.transitions)
                        {
                            TransitionRecord transitionRecord = new TransitionRecord(layersTabel.Count, stateMachinesTabel.Count, statesTabel.Count, transitionsTabel.Count, transition);
                            transitionsTabel.Add(transitionRecord);
    
                            foreach (AnimatorCondition aniCondition in transition.conditions)
                            {
                                ConditionRecord conditionRecord = new ConditionRecord(layersTabel.Count, stateMachinesTabel.Count, statesTabel.Count, transitionsTabel.Count, conditionsTabel.Count, aniCondition);
                                conditionsTabel.Add(conditionRecord);
                            } // コンディション
                        }//トランジション
                    }//ステート
                }
    
            }//レイヤー
    
            resultMessage = "Scanned☆(^▽^)";
            Debug.Log(resultMessage);
        }
    

    new LayerRecord( ~ ) といったコンストラクタで プロパティーの値を1個1個見ているんだが、
    全部 調べているわけではない。
    リレーショナル・データベースに 一発で インポート/エクスポート してくれないものか。

    一応、自力で データを拾った内容の スクリーンショットを貼り付けておく。

    レイヤー

    201701230746a6.png
    レイヤーの使い方をまだ覚えていないので1つだけ。

    ステートマシン

    201701230746a7.png
    手を抜いているところは、手を抜いている。「UnityEditor.Animations.AnimatorStateTransition[]」とか ToString( ) で取った ただの文字列だ。

    ステート

    201701230746a8.png
    ステートマシンを再帰関数で全走査し、1つのファイルに ステートを並べた。

    トランジション

    201701230746a9.png
    これを編集したいわけだ。

    コンディション

    201701230746a10.png
    Mode には イコールや ノットが入っているので演算子だろうか。

    ポジション

    201701230746a11.png
    座標は ポジション・テーブル として外に出した。

    インポートはどうするのか?

    データの取り方が不完全なので、データベースで丸ごと編集して、Unityに丸ごと放り込むといったことができない。

    そこで、編集したいステートだけを名指しして、Exit Time だけ更新するといった、部分更新 で うまく回せないか、調べたい。

    (追記)UnityのAnimator controllerの中身に、URLは無いの?

    有るのか無いのか分からないので、自分で作ることにする。

    例えば 自作だが、次のようなコードを書き、
    201701230746a12.png
    LookupState は わたしが試し書きしたもの。中でループを回して検索しているだけ。

    SMove ステートの Tag は今は空っぽだが……、
    201701230746a13b.png

    メニューからさっきのコードを実行すると……、
    201701230746a14b.png

    自動で タメシさん が入ってくれることは分かった。
    201701230746a15b.png

    だが いちいち、次のような自作のコードは書きたくない。

            AnimatorState state = AinmationControllerOperation.LookupState(ac, 0, "Base Layer", "SMove");
            state.tag = "tamesi(^q^)";
    

    せめて、次のように書けないか。

            AnimatorState state = AinmationControllerOperation.LookupState(ac, "0/Base Layer/SMove");
            state.tag = "tamesi(^q^)";
    

    ルートが レイヤー番号で、
    途中の節は ステートマシン名で、
    葉は ステート名、

    というふうにすれば行けなくはないか。ワイルドカードも用意して

            AnimatorState[] states = AinmationControllerOperation.LookupState(ac, "0/*/JMove1");
            foreach( AnimatorState state in states )
            {
                state.tag = "tamesi(^q^)";
            }
    

    とできてもいい気がする。影響範囲が怖いが。

    構文解析

    スラッシュ区切りは便利なもので、
    201701230746a16b.png
    こんな "0/Base Layer/SMove" みたいな文字列が……。

    201701230746a19.png
    切り刻まれた。トントントントン ラーメンに入れるネギみたいだ。

    201701230746a20b.png
    スプリット一発だ。ステートマシン名や、ステート名に「/」が含まれていると使えないが運用でカバーしてもらおう。

    階層はドット区切り?

    Unity の公式で、階層をドット区切りにしていたので それに倣った。

    また、レイヤー番号が 0 のステートマシンは Base Layer のことなので、レイヤー番号と最上位のステートマシン名は一対一対応づくのかもしれない。そこで パスにレイヤー番号は含めないことにした。

    201701230746a21b.png

    これでも動く。
    201701230746a22b.png
    ひとまず ワイルドカードは無しで 公開する。

    自作のパスの部分は 自分のプロジェクトに合わせて書き換えてほしい。

    using UnityEditor.Animations;
    using UnityEngine;
    
    public abstract class AinmatorControllerOperation
    {
        [MenuItem("(^_^)Menu/Set Tag13")]
        static void SetTag()
        {
            // アニメーター・コントローラーを取得。
            AnimatorController ac = (AnimatorController)AssetDatabase.LoadAssetAtPath<AnimatorController>("Assets/Resources/AnimatorControllers/AniCon@Char3.controller");
    
            AnimatorState state = AinmatorControllerOperation.LookupState(ac, "Base Layer.JMove.JMove0");
            state.tag = "tamesi(^q^)5";
        }
    
        /// <summary>
        /// ノードの最初の1つは レイヤー番号
        /// </summary>
        public const int ROOT_NODE_IS_LAYER = 1;
        /// <summary>
        /// ノードの最後の1つは ステート名
        /// </summary>
        public const int LEAF_NODE_IS_STATE = -1;
    
        /// <summary>
        /// パスを指定すると ステートを返す。
        /// </summary>
        /// <param name="path">"Base Layer.JMove.JMove0" といった文字列。</param>
        public static AnimatorState LookupState(AnimatorController ac, string path)
        {
            string[] nodes = path.Split('.');
            // [0~length-2] ステートマシン名
            // [length-1] ステート名
    
            if ( nodes.Length < 2){ throw new UnityException("ノード数が2つ未満だったぜ☆(^~^) レイヤー番号か、ステート名は無いのかだぜ☆?"); }
    
            // 最初の名前[0]は、レイヤーを検索する。
            AnimatorStateMachine currentMachine = null;
            foreach (AnimatorControllerLayer layer in ac.layers)
            {
                if (nodes[0] == layer.name) { currentMachine = layer.stateMachine; break; }
            }
            if (null == currentMachine) { throw new UnityException("見つからないぜ☆(^~^)nodes=[" + string.Join("][", nodes) + "]"); }
    
            if (2<nodes.Length) // ステートマシンが途中にある場合、最後のステートマシンまで降りていく。
            {
                currentMachine = GetLeafMachine(currentMachine, nodes);
                if (null == currentMachine) { throw new UnityException("無いノードが指定されたぜ☆(^~^)9 currentMachine.name=[" + currentMachine.name + "] nodes=[" + string.Join("][", nodes)+"]"); }
            }
    
            return GetChildState(currentMachine, nodes[nodes.Length - 1]); // レイヤーと葉だけの場合
        }
    
        /// <summary>
        /// 分かりづらいが、ノードの[1]~[length-1]を辿って、最後のステートマシンを返す。
        /// </summary>
        private static AnimatorStateMachine GetLeafMachine(AnimatorStateMachine currentMachine, string[] nodes)
        {
            for (int i = ROOT_NODE_IS_LAYER; i < nodes.Length + LEAF_NODE_IS_STATE; i++)
            {
                currentMachine = GetChildMachine(currentMachine, nodes[i]);
                if (null == currentMachine) { throw new UnityException("無いノードが指定されたぜ☆(^~^)10 i=[" + i + "] node=[" + nodes[i] + "]"); }
            }
            return currentMachine;
        }
    
        private static AnimatorStateMachine GetChildMachine(AnimatorStateMachine machine, string childName)
        {
            foreach (ChildAnimatorStateMachine wrapper in machine.stateMachines)
            {
                if (wrapper.stateMachine.name == childName) { return wrapper.stateMachine; }
            }
            return null;
        }
    
        private static AnimatorState GetChildState(AnimatorStateMachine machine, string stateName)
        {
            foreach (ChildAnimatorState wrapper in machine.states)
            {
                if (wrapper.state.name == stateName) { return wrapper.state; }
            }
            return null;
        }
    }
    
    

    一応確認

    201701230746a23b.png
    ステート名は そもそも一意なのか 名前を入れていて確認したところ、一意のようだ。
    同じステートマシンの中で 重複した名前を付けようとすると 後ろに「 0」「 1」とカウンターが増えていくようだ。

    別のステートマシンでは、そのまた別のステートマシンで使っている同じ ステート名 を置くことができる。 すると逆にそのことで、
    201701230746a25b.png
    別のステートマシンに置いてある 同名のステートと区別する方法がなくなるようだ。ネームハッシュも同じ。

    そこで、ステート名は 一意にするように運用で カバーすることとする。

    せっかく パスで リソースを名指しする仕組みを作ったところだが、肝心のトランジションが、別のステートマシンに置いてある 同名のステートを区別できないようでは仕方ない。

    全検索するよりは、パスを与えて検索する方が 早いぐらいのメリットはあるかもしれない。

    トランジションのプロパティーを編集するには?

    矢印の根元の ステート名 と、羽先の ステート名 を指定すれば選べるのではないだろうか?
    201701230746a27b.png

    実行してみると……、
    201701230746a26c.png
    あっ、いらんことを してしまった!

    もう一回。今度は 線をつなげられるか試してみよう。
    201701230746a28b.png
    ステート へのパスを2つ書いて 左に書いたステートから 右に書いたステートへ 線をつなげるようにする。

    201701230746a29b.png
    さあ、試してみよう。

    201701230746a30.png
    よし! これで いけそうだ。 さっき作ったプログラムに足していく。

        [MenuItem("(^_^)Menu/Add Transition 1")]
        static void AddTransition()
        {
            // アニメーター・コントローラーを取得。
            AnimatorController ac = (AnimatorController)AssetDatabase.LoadAssetAtPath<AnimatorController>("Assets/Resources/AnimatorControllers/AniCon@Char3.controller");
    
            AinmatorControllerOperation.AddTransition(ac, "Base Layer.JMove.Tamesi1 0", "Base Layer.JMove.Tamesi1");
        }
    
        #region トランジション
        /// <summary>
        /// パスを指定すると トランジションを返す。
        /// </summary>
        /// <param name="path">"Base Layer.JMove.JMove0" といった文字列。</param>
        public static AnimatorStateTransition LookupTransition(AnimatorController ac, string path_src, string path_dst )
        {
            AnimatorState state_src = LookupState(ac, path_src);
            AnimatorState state_dst = LookupState(ac, path_dst);
    
            foreach (AnimatorStateTransition transition in state_src.transitions)
            {
                if(transition.destinationState.name == state_dst.name)
                {
                    return transition;
                }
            }
            return null;
        }
    
        /// <summary>
        /// パスを指定すると トランジションを返す。
        /// </summary>
        /// <param name="path">"Base Layer.JMove.JMove0" といった文字列。</param>
        public static void AddTransition(AnimatorController ac, string path_src, string path_dst)
        {
            AnimatorState state_src = LookupState(ac, path_src);
            AnimatorState state_dst = LookupState(ac, path_dst);
    
            state_src.AddTransition(state_dst);
        }
        #endregion
    

    ステートを検索するメソッドを作っていたおかげで、トランジションを編集するのは 簡単なものだ。

    単数形を複数形にしていく

    201701230746a32b.png
    今まで、これを オートでつないで欲しくて 挑戦していたわけだ。

    1対1 のトランジションは つながった。今度は これを N対N にしていく。
    201701230746a31b.png
    こういうのを作っておくと あとで役に立つだろう。

    201701230746a33d.png
    前に作った ステート検索が ここでも役に立つ。

    201701230746a34b.png
    さあ押してみよう。

    201701230746a35.png
    うむ、コーヒーがうまい。 よくみると 自分に自分をくっつけているところもある。全部で25本引いているわけだが 50回ぐらいクリックしなければいけないところだった。これが半自動になってくれれば だいぶ楽になりそうだ。

    次回からは 条件を指定することで 当てはまるすべての個所に線を引く仕組みを考えていきたい。