ゲーム開発チーム「丸ダイス」の開発ブログです! 公式サイトはこちら

【Unity】NCMB3.0.0 インポート時にiOSビルドのリンカエラー

前回(【Unity】NCMB インポート時のAndroidビルドエラーを読み解く - 丸ダイスの卓上開発日誌Android版でのビルドエラーを解決したら、今度はiOSのCloud Buildでビルドエラーが。

※結局、2.2.0か3.0.0かはエラーと無関係だったので、新しい方を使っています。

原因

ログを見てみると、Unityのビルドは通って、xcodeのビルドの一番最後にこんな出力。

       [xcode] Undefined symbols for architecture armv7:
       [xcode]   "_OBJC_CLASS_$_UNUserNotificationCenter", referenced from:
       [xcode]       objc-class-ref in NCMBAppControllerPushAdditions.o
       [xcode] ld: symbol(s) not found for architecture armv7
       [xcode] clang: error: linker command failed with exit code 1 (use -v to see invocation)

obj-cは書いたことないですが、「NCMBAppControllerPushAdditions.oの中で参照されてる_UNUserNotificationCenterってシンボルがUndefinedやぞコラ!」っつってるんだから、リンカエラーでしょう多分。

まさにNCMBの.oで参照されてるようなので、今起きたエラーと見てよさそうですね。

解決方法

原因は読み解けたので、_UNUserNotificationCenterが含まれてるライブラリがどっかにあるじゃろうと踏んでググったらあっさりヒット。

stackoverflow.com

UserNotifications.framework を追加すればいけるぜって言ってますね。欲しいUndefined symbolsが含まれてそうな名前です。

Macを起動するまでもなくiOS のポストプロセッサにframeworkを追記してcommit。そのままCloud Build復活!

緑のチェックマークが並ぶと気持ちいいですね!

..
    [PostProcessBuildAttribute(0)]
    public static void OnPostprocessBuild(BuildTarget buildTarget, string pathToBuiltProject)
    {
..
        PBXProject proj = new PBXProject();
..
        // Add framework
        proj.AddFrameworkToProject(target, "UserNotifications.framework", false);
..
    }
}

※ポストプロセッサはこのあたりを参考に前から使ってるものです。UnityでXcodeの設定を自動化する方法まとめ - スマゲ

【Unity】NCMB インポート時のAndroidビルドエラーを読み解く

「NCMBのインポートの仕方」という記事ではないです(公式にやり方載ってますし…)。「よく分かんないエラーが出た時に、ググって出たAnswerをそのまま試すより、ログをしっかり読み込んだ方が早く解決出来たよ!」という記事です。

NCMBのインポート

リリースページ(Releases · NIFCLOUD-mbaas/ncmb_unity · GitHub)から、 v2.2.0 を選んでzipを解凍。

中身の.unitypackageをダブルクリック。表示されたインポートダイアログで「Import」をクリック。うんうん。普通ですね。

ここで、「Assets/Plugins/Android」以下に.jar, .aar ファイルがたくさん放り込まれているのが分かります。 「Unity Plugins フォルダ」なんかでググると、どうやらプラットフォームごとのネイティブプラグインを入れるフォルダのようです。つまり、NCMBではコア部分の実装がネイティブプラグインで提供されているわけですね。

Unity Editor 上では何事もなくコンパイルが通ります。

ビルド失敗!

ネイティブプラグインは、実際にそのプラットフォームでビルドするまで動作を検証出来ません。Android版をビルドしてみると、見事にビルドエラー。

Unity上での表示は「Unable to convert classes into dex format. See the Console for details.」Consoleを見ろって言ってますね。どれどれ。

Consoleを見てみる

CommandInvokationFailure: Unable to convert classes into dex format.
...

この後数千行続きます。長っ!とひるむところですが、ここで”ググりません”。少なくともUnityは「ビルドが失敗した理由はコレだよ!」と言っているのですから、付き合ってあげようじゃありませんか。

エラー出力の構造を推測してみる

先ほどのConsoleの出力の最初数行

CommandInvokationFailure: Unable to convert classes into dex format.
C:/Program Files/Java/jdk1.8.0_111\bin\java.exe -Xmx2048M -Dcom.android.sdkmanager.toolsdir="C:/Program Files (x86)/Android/android-sdk\tools" -Dfile.encoding=UTF8 -jar "C:\Program Files\Unity\Editor\Data\PlaybackEngines\AndroidPlayer/Tools\sdktools.jar" -

stderr[
Uncaught translation error: java.lang.IllegalArgumentException: already added: Landroid/support/v4/accessibilityservice/AccessibilityServiceInfoCompat;
..

1,2行目を見ると、文章になっている1行目はエラーの概要、2行目はオプション付きの長々としたコマンドのようです。

その後に

stderr[
Uncaught translation error: java.lang.IllegalArgumentException: already added: Landroid/support/v4/accessibilityservice/AccessibilityServiceInfoCompat;
Uncaught translation error: java.lang.IllegalArgumentException: already added: Landroid/support/v4/accessibilityservice/AccessibilityServiceInfoCompat$AccessibilityServiceInfoIcsImpl;
..

と続くところを見ると、「2行目を実行した時に、1行目のエラーが出て、エラーの詳細はstderr の中身の通りだよ」ということのようです。

あれ?じゃあstderrの後は?と見てみると

]
stdout[
processing archive C:\home\(Unityプロジェクトのパス)\Temp\StagingArea\android-libraries\AmazonAppStore\libs\.\classes.jar...
processing com/unity/purchasing/amazon/AmazonPurchasing.class...
processing com/unity/purchasing/amazon/AmazonPurchasing$1.class...
..

とあります。あとは最後まで全部stdoutの中身のようです。processing の後はクラス名のようですから、なるほど、2行目のコマンドでビルドした時の標準出力がstdoutの中に出てるわけですね。 つまりエラー出力の構文は

エラー概要
実行したjavaのビルドコマンド
stderr[ エラー詳細 ]
stdout[ javaのビルドログ ]

ということのようです。

java.lang.IllegalArgumentException: already added を解決しよう

今回のエラーは 「java.lang.IllegalArgumentException: already added」でした。中のクラスはもちろん書いた覚えはないですし、クラス名をProjectビューで検索してもそんな.jarは表示されません。 .jarや.aarの中には複数のクラスがまとまって提供されています。

でも、上のようにエラー出力の構文が分かると、(Unityではなく)javaのビルドした時のビルドログにクラスのビルド順序が記載されています。「already added」 なので、同じクラスを2コ追加しちゃってるのかな?とアタリをつけてログ全体で「android/support/v4/accessibilityservice/AccessibilityServiceInfoCompat」を検索してみると…

processing android/support/v4/accessibilityservice/AccessibilityServiceInfoCompat.class

stdout[]の中にこの行が2つありました。フルネームが全く同じクラス名が2つ…なるほど、これではビルドできそうにありませんね…。 どうやら、今回のインポートで同じクラス定義をしている.jar, .aarが2つになってしまったようなので、それを取り除けば良さそうです。

ここで、さきほどの検索結果をもう一度見てみると、

processing archive C:\home\(Unityプロジェクトのパス)\Temp\StagingArea\android-libraries\support-v4-24.0.0\libs\.\classes.jar...
processing android/support/v4/BuildConfig.class...
processing android/support/v4/accessibilityservice/AccessibilityServiceInfoCompat.class...
..

ヒットしたクラス定義のすぐ上に、クラス定義ではなく.jarのファイルパスのようなものがあります。stdout[]全体を改めて眺めて見ると

processing archive (ファイルパス.jar)
processing (クラスフルネーム)
processing (クラスフルネーム)
..
processing archive (ファイルパス.jar)
processing (クラスフルネーム)
processing (クラスフルネーム)
..

が何度も繰り返されているようです。

なるほど!つまり(ファイルパス.jar)のアーカイブファイルに定義されているたくさんの(クラスフルネーム)を次々にビルドしているわけですね!

Tempの中に.jarを追加した覚えはないですが、.jarの固有名っぽい 「support-v4-24.0.0」には覚えがあります。UnityのProjectビューで検索してみると、見事ヒット! もう一つの方も、「processing (クラスフルネーム)」の上をたどって、含まれているarchiveは「android-support-v4.jar」だと分かりました。

NCMB.unitypackageをもう一度実行してみると、どうやらandroid-support-v4.jar は今回NCMBのインポート時に追加されたファイルのようです。

とりあえず新しい方のandroid-support-v4.jarを消してビルドしてみると、見事成功!※正確には、別の似たエラーが出たのでそちらも同じように解決しています。

めでたしめでたし。

ログ読みのススメ

こうやってログの中身をしっかり追ってみると、実はエラーの理由は人の目で追える形でしっかりと書かれていることが分かりました。

「よく分からんエラー」に遭遇した時は、エラー文を検索窓に突っ込んで、「こうしたら直りました」を実行するだけで直ることもあります。

でも、問題が起きた時に答えだけを求めるやり方より、一から仕組みを理解するようにした方が、似たケースが起きた時にすぐに対処出来て自分のプロジェクトをより自在に扱えるようになるのかなぁと、今更ながらに思った次第でした。

実際、※の別エラーは本当にあっという間に解決出来ました。

ログを読みましょう!Qiitaには嘘を書けますが、ログは嘘を付きません!

Skyboxを一発でテクスチャセットとして出力するPhotoshopプラグイン

Skyboxを一発でテクスチャセットとして出力するPhotoshopプラグイン

作りました

f:id:marudice:20170328025222p:plain こんな感じの画像(4096x3072px) から f:id:marudice:20170328025336p:plain こんな感じにSkyboxで必要なテクスチャをsuffix付きで同じフォルダに出力します。

Photoshopプラグインを入れる方法は君自身の手で見つけ出してくれ!

// OutputSkyboxTexes.jsx
var size = 1024;

function saveOne(doc, suffix, x_idx, y_idx)
{
  var clone = doc.duplicate();
  clone.crop([x_idx*size, y_idx*size, (x_idx+1)*size, (y_idx+1)*size])
  docName = doc.fullName.fsName.toString();

  fileName = docName.substr (0, docName.length - 4) + "_" + suffix;
  pngSaveOpt = new PNGSaveOptions();
  pngSaveOpt.interlaced = false;

  saveFileObj = new File(fileName);
  //保存する
  try {
      clone.saveAs(saveFileObj, pngSaveOpt, true, Extension.LOWERCASE );
      clone.close(SaveOptions.DONOTSAVECHANGES);
  } catch(msg) {
      alert(msg + " 保存できませんでした");
  }
}

function main()
{
  var doc = activeDocument;
    try {
        //ファイル名を取得
        docName = doc.fullName.fsName.toString();
    } catch(msg) {
        alert(msg + " 一度どこかにセーブしてから実行して下さい");
        return;
    }


  saveOne(doc, "up", 2, 0);
  saveOne(doc, "left", 3, 1);
  saveOne(doc, "front", 2, 1);
  saveOne(doc, "back", 0, 1);
  saveOne(doc, "right", 1, 1);
  saveOne(doc, "down", 2, 2);
}

//本体実行
main();

ココが便利!

フィルターとか色調補正とか、Skyboxを調整してはUnityで確認して…の作業が格段に早くなります! いっそAssetフォルダ内にpsdごと放り込んでもいいカモ。

More

↑は、1枚のSkybox用Textureが1024x1024ですが、sizeをいじれば512x512とかも出来ます。 saveOne(doc, "up", 2, 0); ... の繰り返しのとこを変えれば、サイコロ展開図の位置関係が多少違っても対応出来ます。

おわり!

【新作リリース!】Calc Blocks

記事書くのが遅くなってしまいましたが、新作アプリ「Calc Blocks」をリリースしました! >∀<

iOS: goo.gl

Android: goo.gl

頭使う系パズルが好きな人にオススメです! 頭使うのがキライな人にはオススメ出来ません!(正直)

ゲーマーよ、頭を使おう!

f:id:marudice:20170101151505j:plain:w200

画面上のブロックをドラッグすると、それがそのまま計算されます!で…

f:id:marudice:20170101151525p:plain:w200  f:id:marudice:20170101151538p:plain:w200

ひたすら目標数を作っていきます! 左右のバーがなくなったらゲームオーバー!急げ急げ!

数字は目標数を作っていると大きくなります。まずは10を目指して頑張れ!

死ぬまでにどこまで到達出来るか!

レコード記録&ネットランキングがあるので、自分の計算力を試してみて下さい!

f:id:marudice:20170101151558p:plain:w200  f:id:marudice:20170101153158p:plain:w200

通勤通学のヒマつぶしなんかにいいかもしれないですね~。

あ、課金すると広告が消えて&1コ機能が追加されます。 別にしなくても遊べますけどネ。

【Unity】シンプルかつ軽量なオブジェクトプールの実装

UnityのInstantiateが遅い。皆さん言ってますね。実際、モバイルとかでエフェクトや弾をガンガン出していると目に見えて遅くなっていきます。 というわけで、「使い終わったオブジェクトは貯めておいて(あるいは最初にまとめて生成して)、必要になったら取り出す」という「オブジェクトプール」の方法が良く取られます。

実装の方法はいろいろあるようですが、軽く調べたところ今回実装した方法はスマートかつ無駄なオーバーヘッドも少ない感じがしたので、上げておこうかなという次第です。

Githubに公開しておきました(被使用・使用側の実装、サンプルシーン) github.com

実装

using UnityEngine;
using System.Collections.Generic;

public abstract class PoolObj<T> : MonoBehaviour {
    private static GameObject mOriginal;
    private static Stack<T> mObjPool = new Stack<T>();

    public static void SetOriginal(GameObject origin) {
        mOriginal = origin;
    }

    public static T Create()
    {
        T obj;
        if (mObjPool.Count > 0)
        {
            obj = Pop();
        }
        else
        {
            var go = Instantiate<GameObject>(mOriginal);
            obj = go.GetComponent<T>();
        }
        (obj as PoolObj<T>).Init();
        return obj;
    }

    private static T Pop()
    {
        var ret = mObjPool.Pop();
        return ret;
    }

    public static void Pool(T obj)
    {
        (obj as PoolObj<T>).Sleep();
        mObjPool.Push(obj);
    }
    
    public static void Clear()
    {
        mObjPool.Clear();
    }

    public abstract void Init();
    public abstract void Sleep();
}

これだけです。こいつをPoolObj.csなどとしてプロジェクトに放り込めばスクリプト単体で動作します。

使用時の最小コード - Hoge オブジェクトをプールする

  • プールされるオブジェクトは Init(); Sleep(); で初期化時、休眠時の処理を実装する(しなければコンパイルエラーになる)
//Hoge.cs プールされるオブジェクト
using UnityEngine;

public class Hoge : PoolObj<Hoge> {
    public override void Init()
    {
           //gameObject.SetActive(true);
           //初期化時の処理を実装
    }
    public override void Sleep()
    {
           //gameObject.SetActive(false);
           //休眠時の処理を実装
    }
}
//Hogeを使用する側 (呼び出し位置は全て任意. SetOriginal() が 最初のCreate(); より先なら良い)
    public GameObject originalPrefab; // Hoge.cs が付いている
...
    Hoge.SetOriginal(originalPrefab); //元となるプレハブを指定
...
    Hoge hoge = Hoge.Create(); //インスタンスを取得
...
    Hoge.Pool(hoge); //インスタンスを休眠

所感

どうも、プールする親玉もMonoBehaviourにしてListで管理するみたいな実装が多い印象を受けたのですが、今回の実装のようにすれば任意のコードから操作出来ますし、Pool(); は自分で呼んでも他の誰かから呼ばれてもOKです。 (そもそも、オブジェクトの管理者にUpdate(); などMonoBehaviourの機能は必要ありません)

初期化時・休眠時の処理についても、よくある実装では gameObject.SetActive(); を使うことが多い印象です。確かに一番典型的な例ではこれで構わないでしょうね。

が、巨大なオブジェクトでは SetActive(); はInstantiate(); ほどでないにせよ遅いことが分かっていますし、後のBulletでの使用例のように、変数初期が必要な場合もあります。

つまり、オブジェクトごとに最適な休眠状態は違うが、オブジェクト指向に則った実装では、そのオブジェクトの使用者はそれを意識する必要がない。 ということです。

こんな理由で上のような実装になりました。注意点としては

  • シーンをまたいだ際にClearしないと、nullがプールされた状態のままになる

といったところでしょうか。最後にサンプルソースを。(Githubと同じです)

使用例 - クリックされた位置に Bullet を発射するシンプルなシーン

サンプルコード

//Bullet.cs プールされるオブジェクト

using UnityEngine;

public class Bullet : PoolObj<Bullet> {

    int mCount = 0;
    private Vector3 mVelocity;
    // Update is called once per frame
    void Update () {
        if( mCount > 120)
        {
            Bullet.Pool(this);
        }
        transform.Translate(mVelocity);
        mCount++;
    }
    public void Shoot(Vector3 pos, Vector3 velocity) {
        mVelocity = velocity;
        transform.position = pos;
    }


    public override void Init()
    {
        mCount = 0;
        gameObject.SetActive(true);
    }

    public override void Sleep()
    {
        gameObject.SetActive(false);
    }
}
//Game.cs 使用者

using UnityEngine;

public class Game : MonoBehaviour {

    public GameObject mBulletPrefab;

    void Awake()
    {
        Bullet.SetOriginal(mBulletPrefab);
    }

    void Update()
    {
        // ボタンが押されたらその方向に弾を撃つ
        if (Input.GetMouseButtonDown(0))
        {
            Bullet bullet = Bullet.Create();

            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            float speed = 1;
            bullet.Shoot(ray.origin, ray.direction * speed);
        }
    }
}

【Windows 10】右クリックでエクスプローラーがクラッシュする時の対処方法

概要

シェル拡張 (Shell Extension)とは

7Zipなんかのソフトをインストールすると、ファイルやフォルダを右クリックしたときに「7Zipで解凍/圧縮」といったメニューが表示されることがあります。

また、Dropboxを使っていると、Dropbox内のファイル・フォルダが同期済み/同期中かをアイコンで表示してくれたり。

こういう機能を総称してシェル拡張(Shell Extension)と言います。アプリごとによく使う機能をWindowsの Explorerと統合して使いやすく出来るんですね。

解決方法

エクスプローラー上の右クリックで出るメニュー(Context Menu)が表示されずにエクスプローラーが落ちるということは、明らかにここが怪しいです。

ということで、行き当たったこちらのページに従って、

  • ShellEx Viewなるソフトをダウンロード・実行。 

  • Options->Hide All Microsoft Extensions にチェック。

  • Context Menu に該当するシェル拡張(Type が Context Menu となっているもの。右クリックした時の追加項目です。)のうち、必要なもの(私の場合, Tortoise SVN) 以外を全てDisableに。

以上で無事解決しました。Windowsエクスプローラーが不安定/クラッシュする際は、同ソフトでシェル拡張を無効化するのを試してみては。

余談

git のクライアントはTortoise Git はやめて、Source Tree を使うことにしました。

以前、Windows10 でtortoise svnのアイコンが正しく表示されない問題

を解決する際にレジストリをいじっていたので、レジストリのアイコンオーバーレイ(Tortoise SVN とかでファイルフォルダに✓/!マーク が付くアレです) に関する項目を見に行ってみました。
するとTortoise Git では同じ内容のレジストリキーが違う命名規則で名付けられていて(上記問題に対応するためと思われます)、↑のクラッシュ問題も含めて混在させるのがなんだか怪しそうな気がしたからです。

ちなみに、Win+R -> regedit で見られるアイコンオーバーレイのレジストリキーはここ↓です。レジストリの編集は危険なので くれぐれも安易に編集しないように

\HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindowsCurrentVersionExplorerShellIconOverlayIdentifiers

再生するAnimationを全部プログラムで指定する時のAnimatorControllerを一発で編集するやつ

前提知識: Unity Animator の基本的な使い方 docs.unity3d.com

タイトルの通りです。 「過程なんざどうだっていい!結果だけ教えろ!スクリプトをよこせ!」という方は解決方法の項まで飛ばして下さい

問題設定

例えば次のようなAnimationClipを作ったPlayerがいるとします。すべてLoopアニメーションです。

  • Idle (棒立ち)
  • Run (走る)
  • GrabPull (モノを押す)
  • GrabPush (モノを引く)
  • IdleGrab (モノを持って立つ)
  • Sleep (寝る)

これに対して、プログラムの側で「プログラムのここを通ったらIdleに変更!」「ここを通ったらRun!」と指定出来るとします。

↑のUnityのドキュメントでは、なんだか賢そうなステートマシンを作って、走る速度だとか攻撃してるかだとかをParameterにして、自動的にAnimationが切り替わるようにしなさいみたいな書き方をしていますね。

ただ我々プログラマーとしては、そんな自動でアニメーションが切り替わっては困ります。 なんせプロなのです。 プログラムがIdleと言ったら即座にIdleに、Runと言ったら即座にRunになって欲しいのです。

つまり、我々が欲しいステートマシンはこうです。

f:id:marudice:20160827030225p:plain

美しいですね。全部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を使います。

f:id:marudice:20160827032932p:plain

こんな感じで、それぞれの Transition に Integer, Equalsで Conditions を設定してやれば、

     mAnimator.SetInteger("AnimIdx", 2);

などとすることで、

  • 最後に指定したアニメーションに必ず遷移する

というシンプルな(しかし、使いやすく確実な)AnimatorController が用意出来ます。

もう少しまともなプログラムを書くならenumを使って

// PlayerDefine.cs
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というのが現れます。

// KuuAnimationEditorWindow.cs
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」を押します。↓

f:id:marudice:20160827040324p:plain

すると…↓

f:id:marudice:20160827040622p:plain

はい!出来ました!3秒クッキングですね! スクリプトからAssetsを編集したり追加したりしているので、AnimatorWindowへの反映とXXXDefine.cs がプロジェクトにロードされるには、一回UnityIDEをアンフォーカスが必要など若干ラグがあります。

あとは前述の通り

   mAnimator.SetTrigger("AnimIdx", (int)PlayerDefine.Idx.Run);

などとしてやれば、晴れて「用意したアニメーションを直接コードから指定して再生する」ができちゃいます! コードはコピペ改変ご自由に~。

動作確認

制作中のゲームでPlayerさんを実際に動作させたのがコチラ。ステートマシンの遷移は↑で想定した通りで、シンプルで分かりやすいですね。

youtu.be

余談

プログラムの作り方次第かもしれませんが、普通に書いていれば、コードの文脈から「今どのアニメーションを再生するべきか?」は分かることは多いので、 複雑なモデルでなければこれくらいシンプルに操作出来た方が捗るかと思います。

今回は2Dスプライトにせよ3Dモデルにせよ、アセットはある前提の話でした。

一からモデリングをして、Unityでキャラクターをアニメーションさせるまでのワークフローも、書いてみたら面白いかもしれませんね。

ではでは~