前の記事から
もっと便利にボタン一つでスクリプトの追加したいなぁ
Unityを利用する上でかなり応用が利く話です。
例えばクラスが複雑になってきて状態遷移で機能を管理したいと思った時にボタン一つでAnimatorの追加からスクリプトのアタッチまで一括で行います。
やりたいこと
実際にはボタン2つで実現している。
1つ目のボタン
- Animatorコンポーネントの追加
- StateMachineBehaviourを継承したクラスを自動作成
2つ目のボタン
- Animator Controllerの作成、アタッチ
- Animation Clipの作成、セット
- AnimatorのParametersの設定
- 遷移条件の設定
これを2回のボタンで実現している。
前提知識
Inspector上でボタンを表示させるにはこれを参考
使い方
ボタン設定したうえでスクリプト作成、アタッチ。各項目を設定する。
AnimCreFolder:AnimatorControllerとAnimationClipを配置するパスを設定。パスはコピーパスから
このパスの下にObject名のフォルダが作成され、その中に書くファイルが作成される。
State Names : 状態の数と名前。数は自動で設定される。プラスボタンで状態をついかしていく。
ScriptNameSpace:スクリプト作成するときに型を動的に作成しているめNamespaceを使用している場合はフルパス記載する必要がある。
具体的には
Type behaviourType = Type.GetType(scriptNameSpace + "." + behaviourClassName);
この部分詳しくは各自調べて。namespaceを使用していない場合は設定不要
StateCreateInitとStateCreateStartを順に押す。
スクリプトが作成され、状態遷移が自動で設定される。
スクリプト解説
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を作成してる。
animatorController.AddParameterで別のParametersにすることもできる。
Stateの作成:
状態遷移を作成し作成したものにスクリプトをアタッチしている。
state.AddStateMachineBehaviour(behaviourType);
Transitionの作成:
状態遷移の矢印設定
トリガーの細かい設定もしている。hasExitTime、Durationなど好きな設定にできる
transition.hasExitTime = false;
transition.duration = 0;
セーブ :
EditorUtility.SetDirty(animatorController);
変更点をEditor上、変更したとしない設定。米印がつかなくなる
スクリプト補足
2つのボタンがある理由
スクリプトの作成はC#のファイル出力で行っているため、ファイルを出力してからUnity上で更新をかけないとスクリプトが追加されたと認識されない。その理由から2回ボタンを押す必要がある。
~Behaviourが設定できません
そのAssemblyからクラスが認識することを確認すること、名前空間も含めてフルパスでアクセスする。
追加するソースについてハードコーディングとなっている
テキストデータを読み込んで実装したほうがスマートだと思うが、とりあえずべた書きで実装してある。
気になる人は修正して
もっと使いやすくできるのでは?
追加する機能が増えるほど設定がふえる。スクリプトも複雑になるためバランスをとっている。
気になる人は自分でカスタマイズして
十分なテストはしていない
フォルダやファイルをスクリプト上で作成しているので思わぬ上書きや、変更があるかも。使用する時には注意すること
使いどころ
手間をかければスクリプト上でUnityEditorが出来ることを実装できる。ゲームオブジェクトのコピーであればPrefabで事足りるが、それ以外の自動化したいときに使う。
思ったこと
サクッとEditorの機能を実装できる人は次々自動化したほうが作業効率もあがると思うが、たいていは動きを確認しながらとなるので今回のように設定する機能が多いと自動化するまでかなりの時間がかかる。
そのせいでゲームを作る時間が無くなっては本末転倒になってしまう。
Qiitaで公開されているテンプレやAssetを賢く使っていくほうが時間を無駄にせずすむのではないかと思った。
実装していく中であの機能もほしい、あれも追加したほうが便利になるときりがないし、そもそもUnity上で同じコピーだけど設定値を各自作成して作る、超Prefabのような機能があればいいな。
いや、PrefabのVriantをうまく使えばできるのか?これは次の課題にしよう。
参考記事