AfterEffectsで作ったものをUnity用に(半)自動変換する

ここでは、AfterEffectsにて作成したアニメーションをどうUnityで再生するか? について技術部平山が書いてみます。

コードは実際の製品で使っているものを、ほぼそのままgithubに公開しておりますが、 あくまでサンプルとお考えください。作った人間にしかわからない制限事項やクセが多くあり、ドキュメントもありません。

何を作ったか

まず下の動画をご覧ください。

f:id:hirasho0:20190121173700g:plain

ひどい素人動画ですね。これは元々AfterEffects上で作ったアニメーションを、 今回紹介するツールにて変換してUnityで動くようにしたものです。 「ただ最初から最後まで再生するだけで良く、対応機能の範囲であれば」という条件はつきますが、 変換作業は数分でできます。 この例では動くものが数個しかありませんが、 これが数十個になっても、アニメーションが1分を超える大作になっても、 さほど手間は変わりません。

では、なぜこんなものを作ろうと思ったのかをお話します。

動機

以下の動画をご覧ください。

f:id:hirasho0:20190121173750g:plain

弊社東京プリズンのバトル開始アニメーションです。 炎に彩られたエンブレムが左右から現れ、「VS」の表記が中央に現れ、 バトルに参加するキャラクター3人づつが左右から入り、 クルクル回るコインが登場して、先攻後攻が定まり、 最後に勝利条件が下からせり上がってきます。

さて、これをどうやって作りますか?というお話です。

元々の素材はAfterEffects上でアーティストによって作られています。 これをどうにかしてUnityに持ってこなくてはなりません。

「最初からアーティストがUnity上で作ればいいじゃん」 とお思いでしょうか? なるほど、それができる現場、それが適している現場もあると思いますが、 そうでない現場もあります。

それに、「アートのツールは手に馴染んだものを使う方が良い」と考えています。 学習コストの話ではありません。 結局ゲーム開発のコストの多くは 絵素材やアニメーションの量産にかかり、かつ、そこの品質は決定的に重要です。 効率の意味でも、アーティストは気持ち良く使えて手に馴染んだツールを使うのが良く、 それがAfterEffectsだと言うのであればAfterEffectsを、 Flashだと言うのであればFlashを使うのが良いように思えます。 あとのことはプログラマが技術によってどうにかすればいいのです。 ゲームの規模にもよりますが、現代的な量産規模を考えると その方が安いことが多いと考えます。

また、Unity上だと何でもできてしまうのが、ちょっと気になります。 複数人でシーンをいじった時の衝突問題もありますし、 スマホ向け開発ではアセットバンドルにコードを入れられない、という制限もありますから、 あまり何でもできてしまうと事故の原因になります。 性能的に望ましくない作り方や、管理上良くない作り方もあるでしょう。 その意味でも、外部ツールで作ってから変換するアプローチならば安全性を担保しやすいのです。 使われたくない機能は「ごめん、それ対応してない」と言えば済みますし、 変換ツールが検査ツールも兼ねていれば、例えば「オブジェクト多すぎる」「容量多すぎる」 といった警告を出すこともできます。

手動か?自動か?

そういうわけで他ツールで作ったものを持ってくる方法ですが、大きく分ければ選択肢が二つあります。

  • 動画を見ながらUnity上で手動で再現する
  • 何か素敵なツールを作って自動で変換できるようにする

それぞれに長所や短所があるのでまとめてみましょう。

手動の長所は初期コストと柔軟性、そして性能です。 凝った仕掛けを作る時間は必要なく、いきなり UnityEditor上でモノを置いていけます。 動きは、AnimationなりTimelineなりDOTweenなり、 好きな手段を使えば良いでしょう。 私は最初はDOTweenでやっていました。先輩がそうしていた、 というのが理由ですが、コードなら何でもできる、という点も大きいです。 Unityの旧Animationには「木構造をいじると参照が外れる」という辛い制約があり、 どうにも使いにくかった、ということもあります。 また、プログラマはアーティストよりも性能面に詳しいはずですから、 より高速に動作し、容量も少ない実装になる期待が持てます。

そして短所は、面倒くささ、再現度の低さ、そして変更への弱さです。 普通プログラマはアートに関しては素人です。 動画を見て、「位置や回転、拡縮の数字をどういじれば同じ動きになるのか?」 と考えるのは結構大変ですし、細やかな違いもわかりません。 プログラマ的に「こんなもんだろ」と思っても、アーティストから見たら全然ダメで、 「ここの動きはもっとシュッとしてバーンって感じなんだよ!!」みたいなフィードバックが 何度も来るかもしれません。手間が増えます。 そしてこの手間は、アーティストがアニメをいじる度に発生します。 現実には納期や工数が問題になりますので、もっとかっこいい動きに直したい、 と思ってもそう簡単には直せないでしょう。 つまり、本質的に高コストで、しかも品質も低くなりがちだということです。

さて、自動化するとどうなるか。単純にこの逆です。

初期コストがかかり、柔軟性が失われ、 よほど良くできていない限り、プログラマが最適なものを作るよりは性能が落ちます。 そのかわり、ツールによる自動変換であれば再現性の問題は解決し、 変換作業が十分に楽になっていれば、 アーティストが何度いじってもさほどの手間なくUnity側にも 反映させることができます。

では、平山が今回取った方法を紹介いたしましょう。

今回の手法の概要

結論から言えば、手動と自動の間です。

変換ツールは作りましたが、それで何でもできる完成度までは持っていきませんでした。 マスクに対応していない、シェイプに対応していない、 アニメーションが線形補間しか対応していない、 描画順がUGUIと異なると面倒くさい手作業が発生する、 等々の制限を敢えて残しています。 現場でUI実装タスクをこなす合間に作っていたので、 それらの機能まで作り込む時間はかけられなかったからです。

変換ツールによって生成されるのは、コードと、シーン上のオブジェクトです。 コード主体なので柔軟性は確保できます。 例えば「動的に画像をさしかえる」「ある画像がある場所に別のプレハブをはめこむ」 といったことは容易にできます。 Animationのアセットを生成する方が性能は出るでしょうけれども、 今回は柔軟性が欲しかったのでコードとしました。 上の動画の例でも、キャラの絵やコインの行き先、エンブレム、炎の色、 などは動的に変更されており、これは単にコードを書き加えることで実現しています。

ただし、コードですのでアセットバンドルには入れられません。 つまり、アプリ更新なしで新しいアニメーションを追加することはできない、ということです。 東京プリズンでは、アセットバンドルだけで追加したい場合は Spineを使っています。例えばエフェクト類です。 できればAfterEffectsからのデータも アセットバンドルに入れられるようにしたかったのですが、 時間もないし、ツールを保守するコストも払えないので、 そこはSpineに任せることにしました。

再現度については、動きをそのまま持ってこられますので、 対応機能の範囲であれば完全に再現できます。 どれだけ複雑で物が多くアニメが長くても作業にかかる手間は変わりません。 アーティストの変更に対応するコストも完全な手作業よりはかなり抑えられます。

では、作業の流れを軽くご覧いただきましょう。

実際の作業の流れ

ライブラリをUnityにインポート

事前に、再生側ライブラリ をUnityに入れておきます。これは一回だけです。

AfterEffectsでスクリプト実行

変換を始めます。 AfterEffectsのプロジェクト(サンプルにも入っています)を開き、

f:id:hirasho0:20190121173657j:plain

用意したスクリプト(AfterEffectsToUnityCodeConverter.jsx)を実行します。

f:id:hirasho0:20190121173822j:plain

ここで「実行」を押すと、保存するファイル名を聞かれ、保存すると、 C#のコードが出てきます。 出てきたファイルがこちらです。 これをUnityにインポートします。

この際、必要ならばクラス名やファイル名を変えたり、名前空間のusingを足したりしてください。 名前空間に関しては、C#生成スクリプトのこのあたりを いじっておく方がいいと思います。 また、クラスはコンポジション(AfterEffectsの再生単位)ごとに生成されます。 プロジェクトに複数のコンポジションがあると、1ファイルに複数クラスのコードが入ってしまうので、 必要なものを切り出す必要があります(ファイルを複数書くのが面倒くさかったのです)。

絵素材をUnityにインポート

必要な絵素材をUnityのプロジェクトにインポートします。残念ながら手動です。 今回の例では、「人.png」「背景.png」「鳥.png」「ヒット.png」の4枚を用意しました。

シーンにgameObjectを生成、コンポーネントの設定

シーンの中にこのアニメのインスタンスを生成しましょう。 Canvasの下にテキトーなGameObjectを作って、 さっき出てきたスクリプトをつけます。

f:id:hirasho0:20190121173824j:plain

そして、Inspectorに絵素材を設定します。 大抵は同じ名前のところに入れますが、それは場合によります。 これが面倒くさいのでどうにかしたいのですが、 「この画像だけは縮小版を使う」「この画像は以前からゲームに入っているのでそっちを使う」 「この画像はダミーで実際はこっち」といったことが頻繁にあり、 現状は手動です。

f:id:hirasho0:20190121173829j:plain

オブジェクト階層を生成、テスト再生

エディタ拡張でつけた「オブジェクト生成」ボタンを押します。 すると、GameObjectの階層と、絵素材がセットされたImageコンポーネントが自動生成され、 GameObjectの名前がクラス名になります。

f:id:hirasho0:20190121173835j:plain

実行してInspectorの「初期化」ボタンを押し、「再生」ボタンを押せば、再生できますが、 実際に使う時はプレハブにするのが良いでしょう。 どこかでInstantiateして、AfterEffectsAnimation.Play()を実行すれば再生されます。

f:id:hirasho0:20190121173728g:plain

加えて、必要であればコードをいじっていろいろします。先に紹介したバトル開始画面は いくつかの部品に分かれていて、それぞれをプレハブにし、 また、動的に置き換えるテキストはImageをTextに置き換える改造を手動でやっています。 これもどうにかしたいところですね。

実装

AfterEffects上のツール

AfterEffectsで実行する部分についてはJavaScriptで実装する必要があります。 JavaScript未経験であれば覚えないといけませんが、 JavaScriptは「とりあえず動くものを書くだけ」なら簡単な言語です。 普段C#で書けている人なら書けるでしょう。 C#やJavaのように型システムが厳格な言語ばかりやっていると だいぶ感覚が違って面白いので、世界を広げる上でもやっておいて損はないと思います。

なお、あまり新しい書き方をするとAfterEffects上で動かないので、 「今時のJavaScript」を覚える必要はないかと思います。「クラス?何それ?」です。

AfterEffectsからデータを抜くAPIについては、 公式文書 を読んで試しながらやれば、一日で感じは掴めると思います。 私のサンプルコードがお役に立てるかもしれません。

開発の段取りについて

AfterEffects上での実行はデバッグが大変です。 そこで、AfterEffectsに依存した部分と、依存しない部分に分け、 依存した部分を最小にする、というのはいかがでしょうか。

今回の実装では、 拡張子がjsxのこのファイル がAfterEffects上でしか動かない部分で、 拡張子がjsのこのファイル が他でも動く部分です。

まず、「AfterEffectsからありったけの情報を引っこ抜いてJSONにする部分」は 絶対にAfterEffects上でしか動かないので、これはjsxに書きます。 そしてJSONファイルが得られた後は、どこで実行してもかまいません。 私はchrome上で実行、デバグしていました。

今回のツールの開発は、現場で日々UIの実装要求が来る中で勝手に(=スクラムのタスクに計上せずに) やりましたので、まとまった時間は取れません。そこで、段階をかなり分割して開発しました。

  • まずJSONに吐くところまで。それを見ながらDOTween.SequenceのInsert()の時刻と値を手打ち
    • この段階で再現度の問題は解決します。
  • ランタイムライブラリのDOTween版を用意
    • Imageの拡張メソッドでInsertできるようにして簡単に書けるようにした、というだけ。
  • JSON内のアニメからDOTweenのInsertを生成する部分だけブラウザ用に開発。
    • この段階でキーを見ながら打ち込む手間が消滅
  • DOTweenでは速度の問題があるので、キーフレームアニメーション版のランタイムを用意
    • DOTweenだと再生時に毎度データを生成するのでGC Allocがヤバい。
  • これを使うようにJSONからC#を生成するJavaScriptをブラウザ用に開発。
  • 「オブジェクト生成ボタン」をエディタ拡張で用意
    • 元の木構造を手で再現する手間が消滅
  • ブラウザ実行部分をAfterEffects上で一気に実行できるように統合
    • JSONを吐く機能もデバグ用に残してある

こんな具合です。各段階で、それ以前よりも楽になるようにしないと、 現場でのツール開発は難しいなと感じます。 途中で中断しても、そこまでで使えるようにしておくのが良いでしょう。 各段階は「なんとなく動く」にせいぜい1日、あとは隙を見てバグをつぶしたり 機能を足したり、という感じでした。

チーム内、あるいはチームをまたいだ形でツール作成の専門家がいる、 という体制もあるかと思いますが、 現場固有のニーズに応えるのが困難だったり対応が遅くなったりしますので、 必ずしもそれが良いとは言えないでしょう。

Unity側

Unity側のランタイム は以下のようなクラス構成です。

  • AfterEffectsAnimation
    • アニメーションインスタンスの基底クラス。MonoBehaviour。再生などのエディタ拡張とコールバック類。
  • AfterEffectsInstance
    • アニメーションインタンス再生処理の実装部分を受け持つ。gameObjectと個別のキーフレームアニメーションを接続して再生する。
  • AfterEffectsResource
    • アニメーションにつき一つだけあるデータ。リソースあるいはアセット。AfterEffectsCurveSetを持つ。
  • AfterEffectsCurveSet
    • Vector2、float、boolの時間推移(Curve)を複数束ねたクラス。
  • AfterEffectsCurve
    • Vector2、float、boolそれぞれの時間推移をキーフレーム配列として持ち、時刻を与えると値を返す。

ツールが生成するC#はAfterEffectsAnimationの派生クラスであり、 AfterEffectsResourceを生成するコードも一緒に生成されます。

public static void CreateResource()
{
    _resource = new AfterEffectsResource(30f);
    _resource
        // layer1_人_png
        .AddPosition(
            "layer1_人_png_position",
            new int[] { 0, 90, 107, 117, 131, 147, 162, 169, 179, 299 },
            new float[] { 52f, 242f, 282f, 308f, 366f, 438f, 474f, 508f, 532f, 678f },
            new float[] { 276f, 294f, 188f, 158f, 148f, 206f, 278f, 246f, 290f, 270f })
        .AddRotation(
            "layer1_人_png_rotation",
            new int[] { 0, 90, 108, 131, 169, 178 },
            new float[] { 20f, 0f, -43f, -12f, 0f, 13f })
        // layer2_鳥_png
        .AddPosition(
            "layer2_鳥_png_position",
            new int[] { 0, 54, 90, 120, 139, 209, 299 },
            new float[] { 660f, 489.861051392424f, 412f, 349f, 205.589212215198f, 115f, 23f },
            new float[] { -20f, 71.5331734089565f, 109f, 120f, 103.796352593026f, 141f, 273f })
        .AddScale(
            "layer2_鳥_png_scale",
            new int[] { 0, 54, 90, 119, 139, 209, 299 },
            new float[] { 10f, 30f, 60f, 100f, 50f, 30f, 10f })
        .AddRotation(
            "layer2_鳥_png_rotation",
            new int[] { 120, 299 },
            new float[] { -18f, 3600f })
        // layer3_ヒット_png
        .AddScale(
            "layer3_ヒット_png_scale",
            new int[] { 120, 121, 125, 132 },
            new float[] { 100f, 200f, 100f, 40f })
        .AddRotation(
            "layer3_ヒット_png_rotation",
            new int[] { 120, 132 },
            new float[] { 26.8260869565217f, 101.8261f })
        .AddOpacity(
            "layer3_ヒット_png_opacity",
            new int[] { 0, 119, 120, 131 },
            new float[] { 0f, 0f, 100f, 0f })
        // layer6_背景ルート
        .AddPosition(
            "layer6_背景ルート_position",
            new int[] { 0, 299 },
            new float[] { 320f, -320f },
            new float[] { 200f, 200f })
        .AddCut("", 0, 300);
}

日本語が混ざってますが、これはAfterEffectsのレイヤ名がそうだからです。 コードに日本語が入ることが許せない人は、アーティストさんに名前のつけ方を変えてもらうか、 自力で直すことになりますが、私はどうでもいいと思います。 昔のC++と違ってUnicodeなので、文字コード由来でバグることはありませんし、 所詮自動生成コードなので、手でメンテナンスすることはあまりないからです。

さて、「layer1_人_png」の例で言えば、時刻0でxが52、時刻90でxが242、といった具合のアニメーションデータを AfterEffectsResourceに設定しています。 コードなので、「位置ずらそう」と思ったらここを書き換えればいいわけです。勝手に透明度のアニメを足したりもできます。

これで「何何という名前のノードに位置のこんなアニメがついている」 という情報が定義できたわけですが、再生する時には「それをどのgameObjectに適用するか」が わからないといけません。このためのコードも自動生成されます。

protected override void InitializeInstance(AfterEffectsInstance instance)
{
    var layer1_人_png_transform = _layer1_人_png.gameObject.GetComponent<RectTransform>();
    var layer2_鳥_png_transform = _layer2_鳥_png.gameObject.GetComponent<RectTransform>();
    var layer3_ヒット_png_transform = _layer3_ヒット_png.gameObject.GetComponent<RectTransform>();

    instance
        // layer1_人_png
        .BindPosition(layer1_人_png_transform, "layer1_人_png_position")
        .BindRotation(layer1_人_png_transform, "layer1_人_png_rotation")
        // layer2_鳥_png
        .BindPosition(layer2_鳥_png_transform, "layer2_鳥_png_position")
        .BindScale(layer2_鳥_png_transform, "layer2_鳥_png_scale")
        .BindRotation(layer2_鳥_png_transform, "layer2_鳥_png_rotation")
        // layer3_ヒット_png
        .BindScale(layer3_ヒット_png_transform, "layer3_ヒット_png_scale")
        .BindRotation(layer3_ヒット_png_transform, "layer3_ヒット_png_rotation")
        .BindOpacity(_layer3_ヒット_png, "layer3_ヒット_png_opacity")
        // layer6_背景ルート
        .BindPosition(_layer6_背景ルート, "layer6_背景ルート_position")
    ;
}

AfterEffectsInstanceという、再生インスタンスが個別に持つオブジェクトに対して、 BindXXXを呼びます。第一引数はアニメをつけるインスタンスの指定(Transform、CanvasGroup、GameObject等)で、 第二引数は名前です。これでgameObjectとアニメデータを紐付けます。 ここもコードなので、不要な動きをコメントアウトで消したり、勝手に足したりできます。

Curveの実装

次に、「時刻0で4、時刻10で10」のようなキーフレームデータの格納と再生についてです。 これを司るのはAfterEffectsCurveです。なんとなくstructにしていますが、classでも大差ないでしょう。

public struct AfterEffectsCurveFloat
{
    private short[] _times;
    private float[] _values;
    public AfterEffectsCurveFloat(
        IList<int> times,
        IList<float> values,
        bool isPercent = false,
        bool removeLeadTime = false)
    {
        Debug.Assert(times.Count == values.Count);
        int timeOffset = (removeLeadTime) ? -times[0] : 0;
        int count = times.Count;
        _times = new short[count];
        _values = new float[count];
        int prevTime = -0x7fffffff;
        for (int i = 0; i < count; i++)
        {
            Debug.Assert((times[i] >= -0x8000) && (times[i] <= 0x7fff));
            Debug.Assert(times[i] >= prevTime, "times must be sorted in ascending");
            _times[i] = (short)(times[i] + timeOffset);
            prevTime = times[i];

            _values[i] = values[i];
        }
        if (isPercent)
        {
            for (int i = 0; i < count; i++)
            {
                _values[i] *= 0.01f;
            }
        }
    }
    public float Get(float time)
    {
        int index = AfterEffectsUtil.FindLargestLessEqual(_times, time);
        float ret;
        if (index < 0)
        {
            ret = _values[0];
        }
        else if (index >= (_times.Length - 1))
        {
            ret = _values[_times.Length - 1];
        }
        else // 補間するよ
        {
            float t0 = (float)(_times[index]);
            float t1 = (float)(_times[index + 1]);
            float v0 = _values[index];
            float v1 = _values[index + 1];
            float t = (time - t0) / (t1 - t0); //[0, 1]
            ret = ((v1 - v0) * t) + v0;
        }
        return ret;
    }

時刻と、それに対応する値を単に配列に入れて初期化し、 再生時にはFindLargestLessEqual() という二分検索関数でデータを見つけて補間します。 例えば時刻3と時刻7のデータがあって、時刻が5であれば、 時刻3のデータと7のデータを半々に混ぜて返します。

ここでの作り方にはいくつかの選択肢があります。

  • ソートした配列で二分検索
  • ソートした配列だが前の時刻を覚えておいて順に見る

ソートした配列で二分検索する場合、毎度log Nの計算量が必要なので、 速いかと言えば微妙です。ただし、格納に必要なメモリ量は最小で済みます。 なお、二分検索と言っても「ある値を持つデータを見つける」ではなく、 「ある値以下であるような最大のデータを見つける」ことが必要です。 前の例なら、時刻5のデータはないので、その直前の3を見つけねばなりません。 Dictionaryはこの用途では使えません。

もう一つ、前に来た時刻とデータの番号を覚えておいて、そこから見る、という工夫もありえます。 前から順番に再生している場合は、多くの場合同じキーか次のキーを使うため、 二分検索のO(log N)の手間が不要になり、O(1)になります。

しかし、覚えておくデータが増えますし、 「一定以上離れた時間が来たら二分検索に回す」という処理を入れないと キーが多い際に途中からの再生が遅くなります。 前述のように手間をかけられなかったこともあり、単純な二分検索としました。 今まで何度かこういう処理を書きましたが、 ここの二分検索の性能が問題になったことはなかった、ということもあります。

AfterEffectsのデータをRectTransformに入れる

あとは、AfterEffects上の位置やアンカー、回転などのデータを Unityの仕様に合わせる処理が必要です。 これはAfterEffectsUtil.Set()にあります。

public static void Set(
    RectTransform transform,
    float anchorX = 0f,
    float anchorY = 0f,
    float positionX = 0f,
    float positionY = 0f,
    float scaleX = 100f,
    float scaleY = 100f,
    float rotation = 0f)
{
    // OnValidateだとAwakeが終わっていなくて呼ばれないことがある
    if (transform == null)
    {
        return;
    }
    // 左上原点に変更
    transform.anchorMax = new Vector2(0f, 1f);
    transform.anchorMin = new Vector2(0f, 1f);
    // 基準点設定
    var size = transform.sizeDelta;
    float pivotX = anchorX / size.x;
    float pivotY = 1f - (anchorY / size.y);
    transform.pivot = new Vector2(pivotX, pivotY);
    transform.anchoredPosition = new Vector2(positionX, -positionY);
    transform.localScale = new Vector3(scaleX * 0.01f, scaleY * 0.01f, 1f);
    transform.localRotation = Quaternion.Euler(new Vector3(0f, 0f, -rotation));
}

AfterEffects上ではスケールや不透明度はパーセントであるため、 0.01を乗じる必要があります。 また、左上原点なのでanchorMaxやanchorMinも設定します。 さらに、AfterEffects上はY軸が下向きなのでY方向の移動は反転する必要があります。 回転角度も逆です。 一番面倒なのはAfterEffectsのアンカーと、RectTransformのpivotの関係です。 これに関して説明すると長くなるので、上記のコードをご参照ください。

SpriteRendererでやりたい時は?

UnityEngine.UIでなくSpriteRendererを使いたい、 ということもあるでしょう。エフェクトの類では特にそうだと思います。 しかしこれが面倒なのです。実は対応していた時期もあって関数も残っているのですが、 たぶん今は動かないと思います。

AfterEffectsのアンカー設定を反映させるために、余計なTransformが必要で、 「1オブジェクトにつきGameObjectが2個」という状態になります。 UnityのTransformに直接行列を設定できれば簡単なのですが、やる方法が見つからないので 現状放置している感じです。 実際的には、SpriteRendererでやるならば、そもそもGameObjectを生成しないアプローチで やる方が良い気がします。 直接Meshを生成してMeshRendererなりで描画するのです。おそらくその方が性能も出るでしょうし、 AfterEffects上の描画順を手作業なしで反映させたいと思った時にも、その方が楽かと思います。

描画順について

UnityEngine.UIは描画順が親子関係によって決まってしまいます。 しかし、AfterEffects上では描画順は親子関係と関係なく決められます。 そして、今回のツールはここのケアをしていません。 ですので、そのまま移しただけでは描画順が狂うことがよくあります。

アーティストさんには「こういう親子関係でこういう順序ならそのまま出る」 とは説明しますが、前述のように、本来アーティストさんは作りやすいように作った方が良いわけで、 そこはプログラマがどうにかすべきでしょう。

ですが、現状何もできていません。描画順が違うところはプログラマ(つまり私)が、 手動で親子関係をいじったりgameObjectを複製したりしています。

現状このツールに関してはこれが一番大きな問題でしょう。 完全に解決するには、

  • UnityEngine.UIを使わずSpriteRendererでソートオーダー指定する
  • Transformの親子関係をフラットにして描画順に並べ、自前で位置、回転、スケールを計算する

のいずれかになりそうです。前者はボタンなどのUI要素を後から足すのが困難になり、 後者は「このノードから下の透明度のアニメを後から加えるのでCanvasGroup足す」 というような改造がしにくくなって拡張性が落ちますし、 元の親子関係がシーンに反映されず保守性も落ちます。

もしかしたら、SpriteRendererを使って、ボタンやテキストなどのUIシステムを 丸ごと作った方が早いのかもしれません。 うまくEventSystemに乗せられる形にし、 プロジェクトで使う範囲の機能に絞ってやれば、 それほどの実装コストにはならないような気もします。

まとめ

何を作るか、誰が作るか、等々によって最適な解は違うと思うのですが、 今回は「現場で片手間でやれる範囲で自動化する」というアプローチで やってみることにしました。 完全に自動にならない、アセットバンドルに入れられない、 等の制限もありますが、妥協して運用しております。

しかし、製品の規模がもっと大きくなると、アセットバンドルに入れられないとか、 完全にアーティストだけで作業が完結しないとかいったことは受け入れられないでしょう。 また、AfterEffects使いがおらずFlash使いがいる現場ならば、データ元はFlashになるでしょう。

そういったこともあって、今回公開したツールがそのままお役に立てるとは思いませんが、 こういったツールを作る際の、動機、段取り、設計、 といったものについて何かしら参考になれば幸いです。