前提知識: Unity Animator の基本的な使い方
docs.unity3d.com
タイトルの通りです。
「過程なんざどうだっていい!結果だけ教えろ!スクリプトをよこせ!」という方は解決方法の項まで飛ばして下さい
問題設定
例えば次のようなAnimationClipを作ったPlayerがいるとします。すべてLoopアニメーションです。
- Idle (棒立ち)
- Run (走る)
- GrabPull (モノを押す)
- GrabPush (モノを引く)
- IdleGrab (モノを持って立つ)
- Sleep (寝る)
これに対して、プログラムの側で「プログラムのここを通ったらIdleに変更!」「ここを通ったらRun!」と指定出来るとします。
↑のUnityのドキュメントでは、なんだか賢そうなステートマシンを作って、走る速度だとか攻撃してるかだとかをParameterにして、自動的にAnimationが切り替わるようにしなさいみたいな書き方をしていますね。
ただ我々プログラマーとしては、そんな自動でアニメーションが切り替わっては困ります。
なんせプロなのです。 プログラムがIdleと言ったら即座にIdleに、Runと言ったら即座にRunになって欲しいのです。
つまり、我々が欲しいステートマシンはこうです。
美しいですね。全部AnyStateからTransitionを繋げているので、状態に依らずにアニメーションを指定できそうです。これで
mAnimator.SetTrigger("Run");
としたら、いかなる時でもRunになってくれそうです。が、なりません。実演は面倒なので省きますが、コードの都合で
mAnimator.SetTrigger("Idle");
...
mAnimator.SetTrigger("Idle");
...
mAnimator.SetTrigger("Run");
という順に呼ばれ、かつ AnyState->Idle のTransitionが Can Transition To Self == false の場合、なんとアニメーションはIdleになります。
実際にやってみると分かりますが、AnimatorのTriggerパラメーターは、Transitionが解決されるまでは値がOnのままになっているので、
- Idle -> Idle(IdleのTriggerは使われずOnのまま) -> Run -> 即座にIdleに戻る
という動作になります。うーん、困った。
冷静に考えなおすと、「排他的であって欲しいアニメーションの指定を排他的でない変数でコントロールしている」ことが原因と言えそうです。
ということで、integer の Prameterを使います。
こんな感じで、それぞれの Transition に Integer, Equalsで Conditions を設定してやれば、
mAnimator.SetInteger("AnimIdx", 2);
などとすることで、
というシンプルな(しかし、使いやすく確実な)AnimatorController が用意出来ます。
もう少しまともなプログラムを書くならenumを使って
public class PlayerDefine {
public enum Idx {
GrabPull = 0
,GrabPush = 1
,Idle = 2
,IdleGrab = 3
,Run = 4
,Sleep = 5
,SIZE = 6
}
}
mAnimator.SetInteger("AnimIdx", (int)PlayerDefine.Idx.Idle);
とするとよりGoodですね。
解決方法
これで、AnimatorControllerに設定するべきPrameterとTransitionは分かりました。
これくらいなら自動化出来そうですね。しました。
次のコードを適当な名前で.csスクリプトにしてAssets/Editor 以下に放り込むと、メニューバーにKuuというのが現れます。
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
public class KuuAnimationEditorWindow : EditorWindow {
[MenuItem("Kuu/Animation/Init Animator Transition")]
public static void InitAnimatorTransition()
{
int num = 0;
foreach (Object obj in Selection.objects)
{
if (obj is UnityEditor.Animations.AnimatorController)
{
Undo.RecordObject(obj, "Init Animator Transition");
AssetDatabase.StartAssetEditing();
var controller = (obj as UnityEditor.Animations.AnimatorController);
while(controller.parameters.Length > 0)
{
controller.RemoveParameter(controller.parameters[0]);
}
List<string> triggers = new List<string>();
for (int i = 0; i < controller.layers.Length; ++i)
{
var sm = controller.layers[i].stateMachine;
while( sm.anyStateTransitions.Length > 0 ) {
sm.RemoveAnyStateTransition(sm.anyStateTransitions[0]);
}
const string cParamName = "AnimIdx";
if (sm.states.Length > 0) {
controller.AddParameter(cParamName, AnimatorControllerParameterType.Int);
}
foreach (var sta in sm.states)
{
while (sta.state.transitions.Length > 0)
{
sta.state.RemoveTransition(sta.state.transitions[0]);
}
var triggerName = sta.state.name;
if (triggerName.Contains("."))
{
triggerName = triggerName.Substring(0, triggerName.LastIndexOf('.'));
}
controller.AddParameter(triggerName, AnimatorControllerParameterType.Trigger);
var tran = sm.AddAnyStateTransition(sta.state);
tran.AddCondition(UnityEditor.Animations.AnimatorConditionMode.Equals, triggers.Count, cParamName);
if( sm.defaultState == sta.state)
{
controller.parameters[0].defaultInt = triggers.Count;
}
triggers.Add(triggerName);
tran.canTransitionToSelf = !sta.state.motion.isLooping;
}
}
num++;
AssetDatabase.StopAssetEditing();
if (triggers.Count > 0)
{
var path = AssetDatabase.GetAssetPath(obj);
var className = RemoveControllerSuffix(System.IO.Path.GetFileNameWithoutExtension(path)) + "Define";
var directory = System.IO.Path.GetDirectoryName(AssetDatabase.GetAssetPath(obj));
var fileName = className + ".cs";
string outStr = "";
outStr += "public class " + className + " {\n";
outStr += "\tpublic enum Idx {\n";
for (int i = 0; i < triggers.Count; ++i)
{
outStr += "\t\t";
if (i > 0) { outStr += ","; }
outStr += triggers[i] + " = "+i.ToString()+"\n";
}
outStr += "\t\t,SIZE = " + triggers.Count.ToString() + "\n";
outStr += "\t}\n";
outStr += "}\n";
System.IO.File.WriteAllText(directory + "/" + fileName, outStr, System.Text.Encoding.UTF8);
}
break;
}
}
Debug.Log(num.ToString() + " Animator Initialized");
}
public static string RemoveControllerSuffix(string objName)
{
return System.Text.RegularExpressions.Regex.Replace(objName, "Controller", "", System.Text.RegularExpressions.RegexOptions.IgnoreCase);
}
}
あとは、使う予定のステートが追加されたAnimatorController( ここでは PlayerController.controller )を選択した状態で、「Kuu/Animation/Init Animator Transition」を押します。↓
すると…↓
はい!出来ました!3秒クッキングですね! スクリプトからAssetsを編集したり追加したりしているので、AnimatorWindowへの反映とXXXDefine.cs がプロジェクトにロードされるには、一回UnityIDEをアンフォーカスが必要など若干ラグがあります。
あとは前述の通り
mAnimator.SetTrigger("AnimIdx", (int)PlayerDefine.Idx.Run);
などとしてやれば、晴れて「用意したアニメーションを直接コードから指定して再生する」ができちゃいます!
コードはコピペ改変ご自由に~。
動作確認
制作中のゲームでPlayerさんを実際に動作させたのがコチラ。ステートマシンの遷移は↑で想定した通りで、シンプルで分かりやすいですね。
youtu.be
余談
プログラムの作り方次第かもしれませんが、普通に書いていれば、コードの文脈から「今どのアニメーションを再生するべきか?」は分かることは多いので、
複雑なモデルでなければこれくらいシンプルに操作出来た方が捗るかと思います。
今回は2Dスプライトにせよ3Dモデルにせよ、アセットはある前提の話でした。
一からモデリングをして、Unityでキャラクターをアニメーションさせるまでのワークフローも、書いてみたら面白いかもしれませんね。
ではでは~