テラシュールブログ

旧テラシュールウェアブログUnity記事。主にUnityのTipsやAR・VR、ニコニコ動画についてのメモを残します。

Unity5 MecanimのStateMachineBehaviourと戯れる

Unity5で唯でさえややこしいMecanim(アニメーション管理システム)に新しくStateMachineBehaviourが追加されました。

f:id:tsubaki_t1:20150501020124p:plain

この機能はMecanimのステート管理機能の各ステートに振る舞いを設定できる機能です。各ステート毎に振るまいを設定することで、コンポーネント側から現在のステートを確認するといった事をせず動かせるようになります。

目次

使い方

StateMachineBehaviourを継承したクラスを用意し、AddBehaviourボタンを使って振る舞いを登録するだけです。StateMachineBehaviourでは幾つかのコールバックがあります。

  • OnStateEnter:ステートに入った時に呼ばれる
  • OnStateUpdate:毎フレーム呼ばれる
  • OnStateExit:ステートから抜ける時に呼ばれる
  • OnStateMove:オブジェクトの座標を動かす物はココで呼ぶ
  • OnStateIK:IKを使う処理はココで呼ぶ

使って遊んでみる

実際に使って簡単なゲームを作成してみました。

プレイヤーは左のガイコツ(魔王)で、右の白い奴が後ろを向いている間に近づくといった感じの内容です。要するに「だるまさんがころんだ」です。

f:id:tsubaki_t1:20150501020152g:plain

下準備

 まずキャラクターを管理するAnimatorControllerのベースとなるものを用意します。魔王と白いやつの2種類です。アニメーションが散らばるのが嫌ですし作るのは空のアニメーションなので、Animatorに入れちゃいます。

UnityのAnimatorControllerにAnimationClipを内蔵する - テラシュールブログ

f:id:tsubaki_t1:20150501022011p:plain

ステートはこんな感じになります。上が移動する魔王氏、下が白いやつのステートです。

正直、ステート作るよりPlayやFadeでブレンドさせた方が良い気がしますが、今回はお試しです。

f:id:tsubaki_t1:20150501022244p:plain

f:id:tsubaki_t1:20150501022402p:plain

振る舞いの登録

WalkとIdleをキー入力で切り替える振る舞いを作ります。振る舞いを追加したいステーを選択してAddBehaviourボタンをクリック・スクリプト名を設定すると、自動的にスクリプトがAssets直下辺りに作られます。

f:id:tsubaki_t1:20150501023225p:plain

とりあえず「ステート選択時はキャラクターを歩かせる」振る舞いと、「指定のボタンを押している間は指定のBoolパラメータをenableにする」振る舞いを作成します。ちなみにanimator.GetComponentでアニメータを持つオブジェクトへのアクセスが可能なので、ステート間の共通変数保たせたりパラメータ弄ったり色々出来ます。

gist.github.com

BooleanSwitchByKeyは魔王用のIdleとWalkのステートに設定しておきます。また、キー判定はSpaceKey、変更する値はIsWalk(bool)としておきます。

Walkは歩く振る舞いなのでWalkステートに設定します。

f:id:tsubaki_t1:20150501024541p:plain

ついでに、視界チェックを行います。視界チェックは単純に「見られてる時に動いたら死ぬ」なので「白いやつのステートがLookの時に魔王のWalkステートが入ったら死ぬ」とします。実装は「”対象のAnimatorの持つState名がLookの場合、DeadTriggerを入れる”振る舞いをWalkに入れる」ような感じです。

ここで面倒くさいのが、StateMachineBehaviourはシーンにあるオブジェクトへの参照をエディタ側で設定できない事です*1。なので、スクリプト側で参照先を取得します。

gist.github.com

上がWalkステート、下がIdleステートについてる振る舞いです。

f:id:tsubaki_t1:20150501024822p:plain

f:id:tsubaki_t1:20150501024839p:plain

白いやつの挙動は、正直面倒くさいので3秒毎にステートがスィッチするようにします。要するにHasExitTimeでサクッとします。

f:id:tsubaki_t1:20150501025058p:plain

ですが実際は「数秒経ったら振り返る」といった感じの振る舞いを設定するのが良いかもしれませんゲーム的には。

gist.github.com

 そういえば「GameObject.Find("Canvas/Enemy");」ってできたんですね。知りませんでした。

アニメーションの設定

後は作成したAnimatorControllerの「歩く」「アイドル」「死ぬ」「振り向く」にアニメーションを割り当てます。上でやっていたようなAnimatorのセットアップを繰り替えすのは馬鹿らしいので、1個作ってAnimatorOverrideControllerでアニメーションを差し替えるようにします。

作成するにはProjectブラウザ>Create>AnimatorOverrideController を選択します。後は該当のAnimator Controllerに含まれるClip一覧が表示されるので、上書きしたいクリップを登録していきます。

f:id:tsubaki_t1:20150501025805g:plain

ちなみにレイヤー分けてステートだけ使う…みたいなのもOKだと思います。ただ、ステートマシンとして使うなら正直もっと良いアセットがあるんじゃないかなと思います。ケースバイケースで。

*1:Animatorは再利用される可能性があるため

UnityのGameObjectを非アクティブにするとAnimatorがリセットされる挙動、その対処法

新しい対処法はこちら

 

tsubakit1.hateblo.jp

---

 

マニュアルに書いてあったか記憶が微妙なのですが、UnityのAnimatorはGameObjectが非アクティブになるとリセットされます。

ここで言うリセットとはアニメーションが最初に戻るだけ等の生易しい物ではなく、パラメータの持つ値やトリガー状況、アニメーションのステート、さらにはアニメーター初期座標までもがリセット・初期化される糞っぷりです。

f:id:tsubaki_t1:20150205095903g:plain

これはUnityの「非アクティブなオブジェクトは削除されたものとして扱う」的な思想で考えればまあ理屈は通るのですが、UIにもTweenではなくAnimatorで実装するワークフローを目指しているように見える以上、余りにも片手落ちです。

 

ちなみにアニメーター初期座標とは、アニメーターが持つ各要素の初期値の事を指します。Animatorは何もキーフレームが無い状態ならば、Animatorがアクティブになった時の座標を利用するようになっています。

下は「最終的な座標」のみを指定したアニメーションとアニメーター初期座標を利用したアニメーションをブレンドしたものです。実行した位置が初期座標となり、最終的には左下へ移動します。

f:id:tsubaki_t1:20150205101008g:plain

対処法

一番手っ取り早い対処法は、Animatorを持つGameObjectを非アクティブにしないことです。つまり、Animatorを含む全てのコンポーネントをDisableに設定して、オブジェクトの挙動を停止させます。

実はGameObjectのActive/Inactiveもソコソコのコストがかかるのでパフォーマンス的にはアリな選択肢ではあるのですが(コレを推奨している記事等もありました)rigidbody等のdisable化出来ない*1コンポーネントの存在や、初期状態でDisable化してあるコンポーネント等の存在も考えると、少し面倒な部類に入ります。

 

そこで、実行時にパラメータや状況を保持してしまう方法を考えます。

まずアニメーションの再生状況は以下のコードから取得出来ます。

float time = animator.GetCurrentAnimatorStateInfo (currentLayerCount).normalizedTime;

後はココで取得したtimeをAnimator再起動時にanimator.Playと一緒に呼んでやれば、アニメーションを再開する事ができます。

 

パラメータは面倒な所です。何故かと言えばプロパティ名をランタイム時に取得することが出来ません。ですので、エディタで事前に取得してしまいます。

まずAnimatorControllerをUnityEditorInternal.AnimatorControllerとして取得し、GetParameterで参照用ハッシュコードとパラメータタイプを事前に取得しておきます。後は実行時にその値を取得・流し込んでやればOKという訳です。

1点注意点として、AnimatorはInactiveになった瞬間パラメータを放棄するので、Inactive前にパラメータの収集が完了している必要があります。

 

コード

今回も上記の対処を行ったコードを用意しました。

使い方は以下のとおりです。

 

まずScriptableObjectをAssetsファイルとして出力する汎用スクリプトを使用してAnimatorParameterをAssetsファイルとして出力します。ScriptableObjectToAssetの使い方はScriptableObjectの使い方

f:id:tsubaki_t1:20150205104120p:plain

作ったファイルを選択し、AnimatorControllerと同じ階層に移動させます。

次にAnimatorParameterのAnimatorControllerに解析したいAnimatorControllerをドラッグ&ドロップしてSetupを押します。

これでAnimatorControllerのパラメータハッシュ値がキャッシュされます。(ついでに名前も変わります)

f:id:tsubaki_t1:20150205110227p:plain

f:id:tsubaki_t1:20150205110414p:plain

 

次にリセット時にも復帰したいAnimatorを選択して、ResumeAnimatorを追加します。その後、ResumeAnimatorに先ほど作成したScriptableObjectアセットを登録します。

f:id:tsubaki_t1:20150205110736p:plain

 

最後にオブジェクトをInactive化する前に、下のコードを呼ぶようにします。

        var target = gameObject;
        //target(及び子)オブジェクトのResumeAnimatorでパラメータキャッシュを実行
        ResumeAnimator.RestoreAnimator(target);

        target.SetActive(false); //targetをInactive(非アクティブ)

f:id:tsubaki_t1:20150205111349g:plain

ちなみにAnimatorParameterの更新は自動的に行うように作っていないので、Animatorのパラメータを更新する度にSetupボタンを押すか、AssetPostProcesserで変更があった際に自動的に呼ぶような設計にしてやる必要があります。

ScriptableObjectToAssetはScriptableObject名をラベルに登録するので、l:AnimatorParameterでプロジェクト内の全AnimatorParameterを簡単に見つけることが出来ます。ラベル管理便利

f:id:tsubaki_t1:20150205112127p:plain

f:id:tsubaki_t1:20150205112150p:plain

Unity Editorで特定のフォルダからファイルを検索する - テラシュールブログ

ラベルを利用したアセットの管理について - テラシュールブログ

 

コードはこちら


AnimatorParameter.cs

 

関連

単純に初期値だけなんとかしたいなら、こちら

tsubakit1.hateblo.jp

*1:代わりにIsKinematic

ScriptableObjectについて

ScriptableObjectとは何かと色々と悩んできましたが、答えは簡単でした。データを格納するオブジェクトです。もう少し言えば、Unityがデータをシリアライズ・デシリアライズしパラメータを格納するための仕組みです。色々な事が出来る割に用途が分かりにくいのでけっこう悩んでしまいました。

 

f:id:tsubaki_t1:20140724014803j:plain

 

ScriptableObjectの使い方

 

ScriptableObjectの用途

基本的にはScriptableObjectはパラメータや参照を格納する用途で使用します。

 

例えば、沢山の敵が居たとします。その敵は大量に配置する予定のため、オブジェクトプールのように「再生成」ではなく「パラメータ初期化して再登場」させる物とします。その場合、何処かに「初期化時のパラメータを保持する場所」が必要です。

f:id:tsubaki_t1:20140724022431j:plain

この時、初期化時のパラメータを敵各々が持つのは少し勿体無いです。1体が1MB分のパラメータを初期化に使うなら100体いたら100MB。概ね同じパラメータなら汎用データで何とかしたいのが心です。

f:id:tsubaki_t1:20140724022459j:plain

しかしstaticを使うのも少し考えどころです。static変数はゲーム内で絶対に開放されないパラメータを指します。その為、他のステージ等では余計な資産となります。また、少しパラメータが違う敵と同時に登場させたい場合、敵の数だけスクリプトが必要になります(もしくはパラメータのリスト化が必要)。

 

そこでScriptableObjectとしてパラメータを外出しオブジェクトから参照するように設定する事で、全体として単一のインスタンスで済み、なおかつ少し異なるパラメータを持つ敵を出したい場合は、参照先を変えるだけで済むという訳です。

f:id:tsubaki_t1:20140724022526j:plain

 

また、ScriptableObjectはパラメータだけでなくアセットへの参照する機能も保持しているため、同じコードから生成したScriptableObjectであっても、異なる機能を持たせる事も可能です。

例えば敵の攻撃方法が「ScriptableObjectに登録されているGameObjectをInstantateする方式」の場合、ScriptableObjectに登録するスキルを変更するだけで、単純な弾・ホーミング・レーザーといった切り替えが可能となります。

これは複数人で開発する際やエディタを拡張した際、少ない手順で様々なパラメータの敵を量産する際に便利です。逆に、複数種類の敵に同じ攻撃プレハブを渡したいケースでも参照関係を構築したScriptableObject1個渡せば済むので、楽に参照関係を構築出来ます。

f:id:tsubaki_t1:20140724022723j:plain

 

さらにScriptableObjectは所謂ポリモーフィズムも可能で、クラスをオーバーライドして動作自体を上書きし、特殊な挙動を行う敵を用意する…といった事も可能になっています。

ドラッグ&ドロップが面倒な場合は、Resourcesフォルダから取得するような形で実装すると、登録の自動化等も可能です。ApplicationSettingsのような形で使いたい場合は、こちらの方が良いかもしれません(どこからでも呼べるし)

作成したScriptableObjectを呼び出す

スクリプタブル オブジェクト / ScriptableObject

AIを試作してみる日記

 

他のパラメータ格納方法との違い

しかしパラメータを格納・取得するだけならば他の方法でも可能です。パッと思いつくだけでも以下の3つがあります。

GameObjectとの違い

まず単純に思いつく物としてGameObjectとの比較があります。正しくは「GameObjectコンテナに格納されたコンポーネントから値を取得する方法」との違いです。

 

GameObjectはシーンに所属するオブジェクトの為、複数のシーンやエディタ拡張に密接に接続することが出来ません。それはつまり、GameObjectの持つコンポーネントも複数シーンやエディタに密接に関わりにくい事を意味します。

ではプレハブはと言えば、プレハブをデータの格納に使用するのはTransformが無駄です。また専用のコールバックが不足しており、ScriptableObjectと比べて若干使いにくさがあります。

またGameObjectのインスタンス化は結構色々な事をしているっぽいので、量が増えてくるとScriptableObjectによる単純インスタンス化の方がパフォーマンス的に有利になるケースが出るかもしれません。

 

とは言え、とりあえず使うならGameObjectコンテナをプレハブ化したものでも問題はありません(どうせReadOnlyな使う方ですし、イベントとか無くても特に問題も)。複数のコンポーネントを詰め込める特徴もあるので、AssetStoreで販売するアセット等ではコチラのほうが都合が良いケースもあるかもしれません。

コンポーネントとプレハブの紐付けを自動化する

 

XMLJSONCSVとの違い

テキスト・バイナリベース問わず、通常のシリアライザとの違いは、リソース参照の有無にあります。

ScriptableObjectやコンポーネントのSerializeFieldでシリアライズしたオブジェクトは、シーン内の大抵のアセットに対する参照を持つ事ができます。しかし汎用シリアライザの場合、こういった参照を持つ為にはResourcesから取得するコードを経由して呼び出す必要があります。

通常はそれで問題ありませんが、階層や参照の参照といったデータ構造を持ちたい場合、ScriptableObjectのUnityに特化した参照能力が役に立ちます。

またScriptableObjectはエンジン内部(C++)で実装されており、割と高速にデータを読み込めます。このため、EXCEL等の読込負荷が厳しくランタイムで読むのが難しいデータをScriptableObjectへ変換するワークフローを用意しておき、入力はExcel・読込は高速で効率的なScriptableObjectとするのは割と良い手だと思います。

Excelでデータを管理してUnity iOS/Androidで使うワークフローをノーコーディングで使えるようにした

Excelでデータを管理してUnity iOS/Androidで使うワークフローをもう少し詳しく(修正版)

 

なおデータの更新はAssetBundleのような物が必要になるので、基本的に更新できるパラメータの場合はScriptableObjectではなく汎用フォーマットの方が向いてます。

またセーブデータのように更新するデータに関しても、実行時は読み取り専用として動作するScriptableObjectよりテキストやバイナリの方が向いています。

 

ソースコードにハードコーディングする物との違い

ソースコードにデータをハードコーディングする事はCやC++では良くある事だし構造的に簡単で酔い感じなのですが、限度を超えるとバイナリサイズを超肥大化させ起動時間を伸ばす事になるケースが有ります。特にデータベースの中身をC#コードとして出力するようなマクロを組んでいる場合は注意が必要です。

また、少し異なるパラメータのオブジェクトを作る場合、その度にスクリプトを作る必要があります。特定の機能に特化した物を作る場合には問題無いですが、派生していくと若干面倒になっていきます。

ただ全データをメタデータ化(ScriptableObjectへ出力)するのはソースコード的に非常に見難い所があるので、その辺りはケースバイケースで選択するのが良さそうです。

 

なお、ScriptableObjectのような参照の共有は、serializeableとクラスの共有で似たような事が可能です。ただ、Inspectorの見た目的に「違うオブジェクトの持つ異なるパラメータ」に見えてしまうので、その辺り注意が必要です。下の画像にある2つの異なるコンポーネントに付与しているHogeオブジェクトは完全に同一のオブジェクトです。

f:id:tsubaki_t1:20140724025130j:plain

Serialization Best Practices - Megapost

ld: Unable to insert branch island. No insertion point available. for architecture armv7

 

ScriptableObjectは静的なデータの変わりになるか?

エディタで操作するとScriptableObjectに登録したデータが反映し登録できているように見えてしまう為、ScriptableObjectはデータ保存の変わりに使えるように見えますが、結論を言うと出来ません。

ScriptableObjectのデータはゲームにビルドした際は静的な物であるため、ゲーム実行時にリセットされてしまいます。その為、セーブデータの保存には向きません。

 

ScriptableObjectを更新させない為にはOnEnableのイベントで

 

hideFlags = HideFlags.DontSave;

 

 を付けるのが良さそうとありますが、正直エディタの場合は1個レイヤーを挟みInstantateで独立化してしまう方が健全な気がします。その辺りはあんまりイケてないので、ゲームのデータを保持するような用途では使用せず、ゲーム内では静的なパラメータ置き場としたほうが良いかもしれません。

ScriptableObjectを更新させない

 

またセーブデータの管理にはPlayerPrefsが手軽ですが、これ本来は名前の通り「プレイヤー環境設定」を保存する為の物です。簡単なパラメータやステータス等ではこれで十分ですが、セーブデータを保存したい場合はIO機能を使って保存するのが良さそうです。

例えば、PreviewLabs様のPlayerPrefs実装や、データをJSONとして保存する等々。

Unityで永続させたいデータを取り扱う(ファイルの読み書き)

より優れたPlayerPrefsクラス

 

ScriptableObjectは正直無くても何とかなる物ですが、あると便利なケースがあるので覚えていても良いかな−程度の認識で良いかと。

 

ドット絵はえるるのだいあり様よりお借りしました。

ScriptableObjectを更新させない

最新版
tsubakit1.hateblo.jp


ScriptableObjectを使用する場合、ScriptableObjectのパラメータを更新するとベースの値も更新されてしまう。

対策として値を変更するScriptableObjectを使用する場合はGameObject.Instantiateで一回生成したものを使用する。


public class ScriptableObjectSample : ScriptableObject
{
public string msg;
public int score;
}

public class GetScriptableObject : MonoBehaviour
{

public ScriptableObjectSample prefabObject;


void Start ()
{
prefabObject = (ScriptableObjectSample)GameObject.Instantiate (prefabObject);
}
}