3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Unity Editor拡張で状態遷移をボタン一つで作成

Posted at

前の記事から

もっと便利にボタン一つでスクリプトの追加したいなぁ
Unityを利用する上でかなり応用が利く話です。
例えばクラスが複雑になってきて状態遷移で機能を管理したいと思った時にボタン一つでAnimatorの追加からスクリプトのアタッチまで一括で行います。

やりたいこと

ボタン一つでなんでも実装したい。
Animation.gif

実際にはボタン2つで実現している。
1つ目のボタン

  • Animatorコンポーネントの追加
  • StateMachineBehaviourを継承したクラスを自動作成

2つ目のボタン

  • Animator Controllerの作成、アタッチ
  • Animation Clipの作成、セット
  • AnimatorのParametersの設定
  • 遷移条件の設定

これを2回のボタンで実現している。

前提知識

Inspector上でボタンを表示させるにはこれを参考

使い方

ボタン設定したうえでスクリプト作成、アタッチ。各項目を設定する。
image.png
AnimCreFolder:AnimatorControllerとAnimationClipを配置するパスを設定。パスはコピーパスから
image.png
このパスの下にObject名のフォルダが作成され、その中に書くファイルが作成される。
State Names : 状態の数と名前。数は自動で設定される。プラスボタンで状態をついかしていく。
image.png
ScriptNameSpace:スクリプト作成するときに型を動的に作成しているめNamespaceを使用している場合はフルパス記載する必要がある。
具体的には

Type behaviourType = Type.GetType(scriptNameSpace + "." + behaviourClassName);

この部分詳しくは各自調べて。namespaceを使用していない場合は設定不要

すべて設定した状態
image.png

StateCreateInitとStateCreateStartを順に押す。
image.png
スクリプトが作成され、状態遷移が自動で設定される。
image.png

スクリプト解説

using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.Animations;
using System.IO;
using System;


/// <summary>
/// CreateState 
/// </summary>
public class CreateState : MonoBehaviour
{
    [Button("StateCreateInit")]
    public bool stateCreateInit;
    [Button("StateCreateStart")]
    public bool stateCreateStart;



    [SerializeField] private string AnimCreFolder; //AnimatorControllerとAnimationClipを配置する場所
    [SerializeField] private string[] stateNames;//状態の数、名前
    [SerializeField] private string scriptNameSpace = "Assets._00_Project._01_Script._90_BaseUtil._01_State";//スクリプトが配置されるnamespace

    /// <summary>
    /// メンバのNullチェック
    /// </summary>
    /// <returns>void</returns>
    private bool IsNullMenberCheck()
    {
        // animator作成するフォルダを作成
        if (string.IsNullOrWhiteSpace(AnimCreFolder))
        {
            Debug.LogErrorFormat("AnimCreFolderのPathが設定してない");
            return true;
        }
        for (int i = 0; i < stateNames.Length; i++)
        {
            if (string.IsNullOrWhiteSpace(stateNames[i]))
            {
                Debug.LogErrorFormat("stateNamesが設定してない");
                return true;
            }
        }

        if (string.IsNullOrWhiteSpace(scriptNameSpace))
        {
            Debug.LogErrorFormat("scriptNameSpaceが設定してない");
            return true;
        }
        return false;
    }

    /// <summary>
    /// AnimatorComponentの追加、Behaviourスクリプトの作成
    /// </summary>
    /// <returns>void</returns>
    public void StateCreateInit()
    {
        if (IsNullMenberCheck())
        {
            return;
        }

        //Animatorの追加
        var animator = gameObject.GetComponent<Animator>();
        if (animator == null)
        {
            gameObject.AddComponent<Animator>();
        }
        // StateScriptの作成

        var scriptFolderPathAdd_States = string.Format("{0}/{1}" + "/Script", AnimCreFolder,gameObject.name);

        if (!Directory.Exists(scriptFolderPathAdd_States))
        {
            Directory.CreateDirectory(scriptFolderPathAdd_States);
        }
        for (int i = 0; i < (stateNames.Length); i++)
        {
            var scriptPath = string.Format("{0}/{1}{2}Behaviour.cs", scriptFolderPathAdd_States,gameObject.name, stateNames[i]);

            File.WriteAllText(scriptPath, StateCreaterCode.CodeGene(gameObject.name + stateNames[i]));
        }

        //Script作成後一度更新する。C#の機能を使用しているためUntiyを更新しないとスクリプトが
        //読み込まれない。
        AssetDatabase.Refresh();

    }

    /// <summary>
    /// AnimatorController、animationClipの作成。Transition、パラメータ、Scriptの設定
    /// </summary>
    /// <returns>void</returns>
    public void StateCreateStart()
    {
        if (IsNullMenberCheck())
        {
            return;
        }

        string AnimationsCreatePath = AnimCreFolder + "/" + gameObject.name;
        // 選択したオブジェクトをベースにする
        var obj = Selection.activeGameObject;
        if (obj == null)
        {
            Debug.LogErrorFormat("InfoObjがアタッチされたGameObjectを選択した状態で起動してください。");
            return;
        }

        // animator作成
        if (!Directory.Exists(AnimationsCreatePath))
        {
            Directory.CreateDirectory(AnimationsCreatePath);

        }

        var path = string.Format("{0}/{1}Anim.controller", AnimationsCreatePath, gameObject.name);
        if (File.Exists(path))
        {
            if (!EditorUtility.DisplayDialog("Overwrite Confirmation", string.Format("{0}はすでに存在します。上書きして続行しますか?", path), "OK", "Cancel"))
            {
                Debug.LogFormat("操作をキャンセル");
                return;
            }
        }

        // AnimatorController作成
        AnimatorController animatorController = AnimatorController.CreateAnimatorControllerAtPath(path);
        AnimatorStateMachine stateMachine = animatorController.layers[0].stateMachine;

        var states = new List<AnimatorState>();

        //Triggerの作成
        for (int i = 0; i <(stateNames.Length); i++) 
        {
            var triggerName = string.Format("{0}"+"_Triger", stateNames[i]);
            animatorController.AddParameter(triggerName, AnimatorControllerParameterType.Trigger);
        }

        // Stateの作成
        for (int i = 0; i < stateNames.Length; i++)
        {
            var clip = new AnimationClip();
            clip.wrapMode = WrapMode.Clamp;
            clip.name = string.Format("{0}", stateNames[i]);
            AssetDatabase.CreateAsset(clip, string.Format("{0}/{1}.anim", AnimationsCreatePath, clip.name));
            var state = stateMachine.AddState(clip.name, new Vector2(320, 0 + i * 70));
            state.motion = clip;
            state.writeDefaultValues = false;
            //BehaviourScriptの追加
            var behaviourClassName = string.Format(gameObject.name+"{0}Behaviour", stateNames[i]);
            //GetTypeはフルパスで指定する必要がある
            Type behaviourType = Type.GetType(scriptNameSpace + "."+ behaviourClassName);
            if (behaviourType == null || behaviourType.IsSubclassOf(typeof(StateMachineBehaviour ))== false)
            {
                Debug.LogErrorFormat(stateNames[i] + "Behaviourが設定できません");
                return;
            }
            state.AddStateMachineBehaviour(behaviourType);
            states.Add(state);
        }
        // Transitionの作成
        stateMachine.defaultState = states[0];
        for (int i = 0; i < stateMachine.states.Length - 1; i++)
        {
            var transition = stateMachine.states[i].state.AddTransition(stateMachine.states[i + 1].state);
            transition.AddCondition(AnimatorConditionMode.If, i + 1, string.Format("{0}_Triger", stateNames[i]));
            transition.hasExitTime = false;
            transition.duration = 0;
        }

        // 設定反映
        var animator = gameObject.GetComponent<Animator>();
        if (animator == null)
        {
            Debug.LogFormat("Animatorコンポーネントが追加できていません。");
            return;
        }

        animator.runtimeAnimatorController = animatorController;
        // セーブ
        AssetDatabase.SaveAssets();
        //変更の Undo エントリーを加えない
        EditorUtility.SetDirty(animatorController);

        Debug.Log("StateCreate完了");
    }
}
public static class StateCreaterCode
{
    public static string CodeGene(string tScriptName)
    {
        string tRtn;
        tRtn = @"
using System.Collections;
using UnityEditor;
using UnityEngine;
using UnityEngine.Events;

/// <summary>
/// " + tScriptName+ @"Behaviour 
/// </summary>
public class "+tScriptName+ @"Behaviour : StateMachineBehaviour
{
    public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        Debug.Log(animator.name + ""Idle_Enter"");
    }
    public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        Debug.Log(animator.name + ""Idle_Update"");
    }
    public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        Debug.Log(animator.name + ""Idle_Exit"");
    }

}

        ";
        return tRtn;
    }
}

IsNullMenberCheck : NULLチェック
StateCreateInit : AnimatorComponentの追加、Behaviourスクリプトの作成
string.Formatで作成するパスの文字列を作成
Directory.CreateDirectoryでフォルダ作成
File.WriteAllTextでテキストファイル.csファイルを作成。StateCreaterCodeで中身が書いてある。
tRtn = @"
で複数行にわたって文字列を格納

StateCreateStart :AnimatorController、animationClipの作成。Transition、パラメータ、Scriptの設定

Triggerの作成:Parametersを作成してる。
image.png
animatorController.AddParameterで別のParametersにすることもできる。
Stateの作成:
状態遷移を作成し作成したものにスクリプトをアタッチしている。
image.png

state.AddStateMachineBehaviour(behaviourType);

Transitionの作成:
状態遷移の矢印設定
トリガーの細かい設定もしている。hasExitTime、Durationなど好きな設定にできる

transition.hasExitTime = false;
transition.duration = 0;

セーブ :

 EditorUtility.SetDirty(animatorController);

変更点をEditor上、変更したとしない設定。米印がつかなくなる
image.png

スクリプト補足

2つのボタンがある理由

スクリプトの作成はC#のファイル出力で行っているため、ファイルを出力してからUnity上で更新をかけないとスクリプトが追加されたと認識されない。その理由から2回ボタンを押す必要がある。

~Behaviourが設定できません

image.png
そのAssemblyからクラスが認識することを確認すること、名前空間も含めてフルパスでアクセスする。

追加するソースについてハードコーディングとなっている

テキストデータを読み込んで実装したほうがスマートだと思うが、とりあえずべた書きで実装してある。
気になる人は修正して

もっと使いやすくできるのでは?

追加する機能が増えるほど設定がふえる。スクリプトも複雑になるためバランスをとっている。
気になる人は自分でカスタマイズして

十分なテストはしていない

フォルダやファイルをスクリプト上で作成しているので思わぬ上書きや、変更があるかも。使用する時には注意すること

使いどころ

手間をかければスクリプト上でUnityEditorが出来ることを実装できる。ゲームオブジェクトのコピーであればPrefabで事足りるが、それ以外の自動化したいときに使う。

思ったこと

サクッとEditorの機能を実装できる人は次々自動化したほうが作業効率もあがると思うが、たいていは動きを確認しながらとなるので今回のように設定する機能が多いと自動化するまでかなりの時間がかかる。
そのせいでゲームを作る時間が無くなっては本末転倒になってしまう。
Qiitaで公開されているテンプレやAssetを賢く使っていくほうが時間を無駄にせずすむのではないかと思った。

実装していく中であの機能もほしい、あれも追加したほうが便利になるときりがないし、そもそもUnity上で同じコピーだけど設定値を各自作成して作る、超Prefabのような機能があればいいな。
いや、PrefabのVriantをうまく使えばできるのか?これは次の課題にしよう。

参考記事

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?