SS6Player使ってみたよレポ② 実行時のセル差し替え

前回の記事でSS6Playerは良い感じだよという話をしたんですが、一件紹介し忘れていた技術ネタがあったので投下しておこうと思います。

対局開始時のテロップもSpriteStudioで製作したんですが、「東一局」等の表示はプログラム側で制御できるのではないかと思い、マニュアルを漁ったところAPIはあるようだが説明がない。とはいえそんなに難しいものではなさそうなので、IntelliSenseを頼りにがんばってみることにしました。
f:id:dnasoftwares:20180418114219p:plain
あんまりもったいぶるもんではないので、とりあえずソースコード全文いきます。

KyokuStartTelopController.cs

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using DNASoftwares.Mahjong;
using UnityEngine;

namespace DNASoftwares.Mahjong
{
    public class KyokuStartTelopController : MonoBehaviour
    {
        private Script_SpriteStudio6_Root _ss;
        private int[] _kyoku_cell = new int[4];
        private int[] _wind_cell = new int[3];
        private int[] _honba_cell = new int[10];
        private int[] _yourwind_cell = new int[4];

        private int _kyoku_sprite;
        private int _wind_sprite;
        private int[] _honba_sprite=new int[2];
        private int _honba_footer_sprite;
        private int _yourwind_sprite;

        //セルの名称
        public string[] KyokuWindCellNames = {"wind_east", "wind_south", "wind_west"}; //○一局 の場風の部分
        public string KyokuCellNameFormat = "kyoku_{0}"; // 東○局 の漢数字
        public string HonbaCellNameFormat = "honba_{0}"; // ○○本場 の数字
        public string[] YourWindCellNames = {"yourwind_east", "yourwind_south", "yourwind_west", "yourwind_north"}; //「あなたは○家です」の表示

        //各部位のスプライトパーツ名(SpriteStudio側で決めたのを転記)
        public string KyokuWindSpriteName = "kyoku_wind";
        public string KyokuNumberSpriteName = "kyoku_num";
        public string[] HonbaSpriteNames = {"honba_1","honba_10"};
        public string HonbaFooterSpriteName = "honba_foot";
        public string YourWindSpriteName = "wind";

        // Use this for initialization
        void Start ()
        {
            _ss = GetComponent<Script_SpriteStudio6_Root>();
            Library_SpriteStudio6.Data.CellMap c = _ss.DataGetCellMap(0);

            //都度参照だと重そうなのでチップ名からIDを引いておく
            for (int i = 0; i < _kyoku_cell.Length; i++)
            {
                _kyoku_cell[i] = c.IndexGetCell(string.Format(KyokuCellNameFormat, i + 1));
            }

            for (int i = 0; i < _wind_cell.Length; i++)
            {
                 _wind_cell[i] = c.IndexGetCell(KyokuWindCellNames[i]);
            }
            for (int i = 0; i < _honba_cell.Length; i++)
            {
                _honba_cell[i] = c.IndexGetCell(string.Format(HonbaCellNameFormat,i));
            }

            for (int i = 0; i < _yourwind_cell.Length; i++)
            {
                _yourwind_cell[i] = c.IndexGetCell(YourWindCellNames[i]);
            }

            //パーツ名も都度参照は重そうなんでIDを引いとく
            _wind_sprite = _ss.IDGetParts(KyokuWindSpriteName);
            _kyoku_sprite = _ss.IDGetParts(KyokuNumberSpriteName);
            for (int i = 0; i < _honba_sprite.Length; i++)
            {
                _honba_sprite[i] = _ss.IDGetParts(HonbaSpriteNames[i]);
            }

            _honba_footer_sprite = _ss.IDGetParts(HonbaFooterSpriteName);
            _yourwind_sprite = _ss.IDGetParts(YourWindSpriteName);

            //卓の様子をみて表示を直す
            //一旦再生させてからセルを変更しないとリセットされちゃう
            _ss.AnimationPlay(0,-1,1);
                
            //局(Taku.Instance.CurrentWind には場風が入ってる 0=東 1=南 2=西)
            if (_wind_sprite >= 0)
            {
                _ss.CellChangeParts(_wind_sprite, 0, _wind_cell[(int) Taku.Instance.CurrentWind],
                    Library_SpriteStudio6.KindIgnoreAttribute.NON);
            }
                // Taku.Instance.CurrentKyoku は何局目かが入ってる(1~4)
            if (_kyoku_sprite>= 0)
            {
                _ss.CellChangeParts(_kyoku_sprite, 0, _kyoku_cell[Taku.Instance.CurrentKyoku-1],
                    Library_SpriteStudio6.KindIgnoreAttribute.NON);
            }

            //~~本場(Taku.Instance.Honba には積み棒の数が入ってる)
            // 0本場なら関係するスプライトを全部隠す
            if (Taku.Instance.Honba == 0)
            {
                foreach (var i in _honba_sprite)
                {
                    _ss.HideSet(i, true);
                }

                _ss.HideSet(_honba_footer_sprite, true);
            }
            else
            {
                //1本以上積まれてるなら~~本場を表示
                var tmp = Taku.Instance.Honba;
                for (int i = 0; i < _honba_sprite.Length; i++)
                {
                    var num = tmp % 10;
                    tmp = tmp / 10;
                    if (num==0 && tmp == 0)
                    {
                        _ss.HideSet(_honba_sprite[i], true);
                    }
                    else
                    {
                        _ss.CellChangeParts(_honba_sprite[i], 0, _honba_cell[num],
                            Library_SpriteStudio6.KindIgnoreAttribute.NON);
                    }
                }
            }

            //プレイヤーの自風("あなたは○家です")
            //人間が操作している席を取得し、その席の自風を得て、自風のenumをintにキャスト(0,1,2,3が東南西北に対応)
            Seat seat = Taku.Instance.Seats
                .FirstOrDefault(x => x.Algorithm == Seat.AlgorithmType.HumanInterface) ?? CameraViewController.Instance.TargetSeat; 
            _ss.CellChangeParts(_yourwind_sprite, 0, _yourwind_cell[(int)seat.MyWind], 
                Library_SpriteStudio6.KindIgnoreAttribute.NON);

        }
    }
}

パーツを操作する前にAnimationPlayメソッドをまず呼ぶというところさえ間違わなければ多分そんな難しいところはないです。

public bool CellChangeParts(int idParts, int indexCellMap, int indexCell, Library_SpriteStudio6.KindIgnoreAttribute ignoreAttribute);

CellChangePartsメソッドでスプライトパーツのチップを差し替えられます。

  • idParts ……パーツID。名前で引きたかったらIDGetParts(name)で名前からIDを取得すること
  • indexCellMap ……セルマップID。セルマップ1つだけなら0で可、-1にすると今パーツが使っているセルマップを指定
  • indexCell ……セルID。名前で引きたかったらIndexGetCell(name)でID取得。-1にするとアニメーションに元々設定されていたセルを指定
  • ignoreAttribute ……これがいまいちわからんのだが「Library_SpriteStudio6.KindIgnoreAttribute.NON」としておけばとりあえず何事もない

実行時のセル指定変更は色々な応用が利くのですがマニュアルが間に合ってないのはとても残念なのでここだけ説明しました。参考になれば幸いです。

SS6Player使ってみたよレポ

ウェブテクノロジ製のスプライトアニメーションツール、OPTPiX SpriteStudioは公式でUnity用プレイヤーを用意しているのが特徴ですが、一時このUnity用プレイヤーが超クセモノということで若干騒がれました。
既に古い内容なので当時の記事等にリンクはしませんが「再生の忠実さを求めるあまりパフォーマンスが出ない(特にモバイル)」「セットアップが面倒」「uGUI/nGUIと相性が悪い」という点が大きかったようです。

で、SpriteStudioがVer6になって、この公式プレイヤーの作りが大きく見直され、曰く「fps評価で1.5〜2倍程度の速度向上」「uGUI/nGUIとの相性がよくなった」らしいのでちょうど当時進行中だったうちのプロジェクトで試してみたのでした。

「うちのプロジェクト」

とりあえず現状(3月末時点)の動画をごらんください。
※まだBGMと効果音は入れてませんので無音は仕様です


4/3人打ち麻雀「New Standard Mahjong」(WIP)


何の変哲もない4or3人打ち麻雀ゲームです。まだWebページの一枚すらない。
SpriteStudioを使っている部分としては

  • メインメニュー左上のヘッダ表示
  • 「対局開始」などのテロップ類
  • 「ポン」「リーチ」「ロン」などの発声文字

と大きく分けて3箇所あります。
テロップ類は調整→確認の周期が短ければ短いほどよい、ということでこういうアニメーションツールは絶対欲しい所でした。
デザインとプログラムを分離しておけばいざという時にはデザイナーさんにお願いもし易いことでしょう。

SpriteStudio側での作業

実は試用ライセンスが切れててスクショ撮り損ねたんですけども(
SpriteStudio側の作業は基本大したことをしていません。パーティクルエフェクトとか使えるものは色々使っています。メッシュはようわからんかった。

Unity用プレイヤーに読ませるにあたっては別にSpriteStudio側で専用のエクスポートを行ったりする必要はありません。SpriteStudio側では普通にプロジェクトを保存して終わりです。

Unity側での作業

GitHubのSS6PlayerForUnityのリポジトリから最新版プレイヤーを取得します。
github.com

unitypackageを入れるとUnity Editorのメニューに「Tool」→「SpriteStudio6」と項目が生えてインポーターが動作します。

インポートの際注意したいのがアセットの保存場所指定です。

「One Column Layout」では、ツリー上でそのまま選択すれば選択状態になります。
一方「Two Column Layout」では、ツリー上で親フォルダを選択した後、右のペインに表示されているフォルダを選択することで選択状態になります。

データのインポート手順 · SpriteStudio/SS6PlayerForUnity Wiki · GitHub

Two Column Layoutを常用してると右ペインで選択を忘れて怒られることが多いです。注意したいところ。

あとは.sspjを指定すれば勝手にプロジェクト内のアニメーションやチップ類を変換してPrefabにしてくれます。ここは楽。
どうせならここで変換設定をScriptableObjectあたりでくるんでくれたら次回からの変換楽なのになーと思ったり。(バッチ機能はあるんですけど「バッチリストを書くのがめんどうだった」というクズい理由で使わないままズルズルと……)
ScriptableObjectのインスペクタに「この設定で変換」みたいなボタンおいといてボタン一発ドーン、だとわたしは嬉しいです。

おいといて、変換されたPrefabはもうそのまま利用できるので、

  • Canvasを置く
  • UIカメラを設定する
  • 生成されたPrefabをCanvasの中に置く
  • Prefabのレイヤー指定をカメラのCulling maskと合わせる

以上4つの設定をすれば画面に出ます。最初の2つはこれに限らずやることだと思うので、実質2手ぐらいで普通にアニメーションが画面に出てくれます。

f:id:dnasoftwares:20180331141743g:plain

実際の組み込みではカメラにCulling maskを使ってると思いますので、ちゃんと表示したいカメラに合わせてレイヤー設定は変えましょう。レイヤー=Defaultのまま作業してUIカメラに絵がでねえぞ????とか散々やりました。

あと、Prefabの場所ですがマニュアルWikiから引用。

・制御用プレハブを作成した場合
インポート時に「Create Control-Prefab」をチェックしていた場合は、インポート時に指定したアセットフォルダの直下に「ssae名_Control」という名前のプレハブがあります。

・制御用プレハブを作成しなかった場合
インポート時に「Create Control-Prefab」をチェックしていない場合は、インポート時に指定したアセットフォルダの下に「PrefabAnimation」というフォルダがあります。
その中にssaeと同じ名前のプレハブがあります。

アニメーションの再生 · SpriteStudio/SS6PlayerForUnity Wiki · GitHub

フォルダ「PrefabAnimation」の下にあるPrefabがアニメーションの実体です。

もうちょっと実用的にする

とりあえずスプライトは出ました。出ましたがこれではあんまり実践的ではありません。

ループ一回で自動消滅してほしい

生成直後のPrefabは無限ループ再生の設定になっていますが、大抵は1ループして消えるとかしてほしいわけです。
ループの設定は生成されたスプライトの制御パラメタで変更できます。
フォルダ「PrefabAnimation」の下にあるPrefabをインスペクタで覗くと「Script_Sprite Studio 6_Root」なるスクリプトが下がっているのがわかります。

f:id:dnasoftwares:20180331163236p:plain
Script_SpriteStudio6_Root のインスペクタ

一番下に「Number of Plays」という項目があり、「(1: No Loop / 0: Infinite Loop)」と説明もついてます。初期値は0なので無限ループになります。
ここを1にすると再生は1回きりとなりますが、最後のフレームが出たままになってしまいます。
どうせなら最後のフレームを再生し終わったら自動で消えてほしいところです。これは数行のコンポーネントで実装できます。

SS6_AutoDestroy.cs
using UnityEngine;

[RequireComponent(typeof(Script_SpriteStudio6_Root))]
public class SS6_AutoDestroy : MonoBehaviour {

  // Use this for initialization
  void Start () {
    var rt = GetComponent<Script_SpriteStudio6_Root>();

    rt.FunctionPlayEnd += PlayEndFunction; // 再生終了時のコールバック関数を登録
  }

  public bool PlayEndFunction(Script_SpriteStudio6_Root scriptRoot, GameObject objectControl)
  {
    return false; //falseを返すと消滅
  }
}

このコンポーネントを「Script_Sprite Studio 6_Root」と一緒に張り付けておくと、再生終了時に自動で自分自身をDestroyするようになります。
コントロールPrefabを通した場合でもちゃんとコントロールPrefabごと消えるので安心。

イントロ→ループ→アウトロをやりたい

単なる無限ループではなく、イントロ→ループ型のループ再生もやってほしいと思うのです。本作では「対局開始」の表示やタイトル画面のヘッダ部分などがイントロ→ループの構造になっています。ついでにループ脱出して消滅するアニメ(アウトロ)もやりたい。

アニメーションにラベルを仕込んでおいてラベル通過をコールバックで検知できたら、と思ったらそれはできないようなので、結局今回はアニメーションをイントロ・ループ・アウトロに分割し、イントロの再生終了後はループのアニメーションを延々再生、アウトロへの移行は外部から関数呼び出しで、という処理にしました。
(Script_SpriteStudio6_Root.AnimationPlayの引数で再生開始・終了位置をラベルで指定できるのに今気づいたのですが、これ使えばこんな回りくどいことしなくてよかったような……)

SS6_IntroLoop.cs
using System;
using UnityEngine;

[RequireComponent(typeof(Script_SpriteStudio6_Root))]
public class SS6_IntroLoop : MonoBehaviour
{
  public string TrackNameLoop = "_loop"; // ループ部分のアニメーション名
  public bool UseOutroAnimation = false; // アウトロアニメーションがあるのか?
  public string TrackNameOutro = "_outro"; // アウトロのアニメーション名
  [NonSerialized] public bool ExitLoop = false; // trueにすると今のループを再生しきった後アウトロに移行、または消滅

  public enum LoopState
  {
    Intro,
    Loop,
    Outro
  };
  private LoopState _state = LoopState.Intro;
  private Script_SpriteStudio6_Root _rt;
  private int _idxLoopAnimation;
  private int _idxOutroAnimation;

  // Use this for initialization
  void Start () {
    _rt= GetComponent<Script_SpriteStudio6_Root>();

    _rt.FunctionPlayEnd += LoopBackFunction; 
    // 先に各アニメーションの名称からインデックスを引いておく
    _idxLoopAnimation = _rt.IndexGetAnimation(TrackNameLoop);
    if (UseOutroAnimation)
    {
      _idxOutroAnimation = _rt.IndexGetAnimation(TrackNameOutro);
    }

    //初期状態はイントロ
    _state = LoopState.Intro;
  }

  private bool LoopBackFunction(Script_SpriteStudio6_Root scriptroot, GameObject objectcontrol)
  {
    //各アニメーションの末尾に来た時、次のアニメーションを決定する
    switch (_state)
    {
      case LoopState.Intro:
      case LoopState.Loop:
        //イントロ,ループ→ループ(orアウトロ)
        if (ExitLoop)
        {
          //ExitLoop==trueならアウトロへ
          if (UseOutroAnimation)
          {
            _rt.AnimationPlay(-1,_idxOutroAnimation,1); // アウトロ再生
            _state = LoopState.Outro;
            break;
          }
          else
            return false;
        }
        else
        {
          _rt.AnimationPlay(-1,_idxLoopAnimation,1); //ループアニメの再生
          _state = LoopState.Loop;
        }
        break;
      case LoopState.Outro:
        //アウトロが終わったら消滅
        return false;
      default:
        break;
    }
    return true;
  }

  public void ExitLoopNow()
  {
    //呼んだ時点で強制的にアウトロに移る or 消去
    if (_state == LoopState.Outro) return;
    if (UseOutroAnimation)
    {
      _rt.AnimationPlay(0,_idxOutroAnimation,1);
      _state = LoopState.Outro;
    }
    else
    {
      if(_rt.InstanceGameObjectControl!=null) //コントロールPrefabがある場合はInstanceGameObjectControlにその参照がある
      {
        Destroy(_rt.InstanceGameObjectControl); //これでコントロールPrefabごと消える
      }
      else
      {
        Destroy(gameObject);
      }
    }
  }
}

ループ処理をScript_SpriteStudio6_Rootに任せず全部自力でやり、コールバック関数で次のアニメを指示する、という仕組みがキモです。
実際に使うときは、

  • SS6_IntroLoopをScript_SpriteStudio6_Rootと一緒のPrefabにアタッチ
  • Script_SpriteStudio6_Rootの方は
    • 再生回数を1にしておく
    • 「Animation Name」(再生するアニメーション名)をイントロ部分のアニメーションにする
  • SS6_IntroLoopのインスペクタでは
    • TrackNameLoopにループ部分のアニメーション名をセット
    • アウトロのアニメがある場合は、UseOutroAnimationをチェックし、TrackNameOutroにアニメーション名を指定

とすればイントロつきループができます。
ループを脱出してアニメーションを消すときは次のどちらかで。

  • GetComponent.ExitLoop=trueみたいにすれば今のループを再生しきってからアウトロへ移行または消去
  • GetComponent.ExitLoopNow()とすればすぐにアウトロへ移行または即消去

これで意外と便利に使えてます。まあループ構造をデフォルトで世話してくれるととても楽でいいんですけど、自力でもそこまで難しくはないです。

アニメーションのスケールがうまくいかない?

Script_SpriteStudio6_Rootは内部でTransformをいじっている(アニメーションにおけるRootパーツのアニメーションが反映される)関係で、Script_SpriteStudio6_RootのあるGameObjectのスケーリングなどはスクリプトから触ることができません。
アニメーション全体をスケール・回転させたい場合は、マニュアルの制御プレハブの役割についてにあるように、制御プレハブの段で変形させましょう。これに気づかず半日ぐらいハマってました。

結論

SS5PlayerForUnityは使っていなかった(そのときは自前エンジンに自前でSpriteStudioプレイヤー入れてましたが……)ので旧Verとくらべてどうか、とは語れないのですが、uGUIとの親和性も良く、2つほど自作コンポーネントをつけるだけで実用上もほぼ問題なく使えるようになりました。

ただ、現状だとSpriteStudioのIndieライセンスはSS6に適用できません
SpriteStudio5相当の機能しか使えないとかでもいいんでSS6の再生エンジン使わせていただきたい……なんとか……

DoGA-L3で「取り急ぎ」作ったカッコいいモデルをもっと使いやすくする

前の記事ではDoGA-L3PLAY Animation importer for Unityを用いて、DoGA-L3のモデルをUnityにインポートし、それを動かすところまでやりました。
dnasoftwares.hatenablog.com

f:id:dnasoftwares:20170312192549p:plain

ただ、現状のモデルはゲームで使うには一抹の不安が残ります。手修正やエディタ拡張アセットを利用して、もうちょっと良い感じにモデルを修正してみましょう。

マテリアルをStandardに揃える

インポート直後は全てのマテリアルにLegacy Diffuseのシェーダーが割り当ててあります。折角Unity5なんですからStandardにしましょう。Assetsフォルダ直下に、l3pファイル名と同じ名前のフォルダができており、そこにマテリアル、テクスチャ、メッシュが入っていますのでここのマテリアルをいじります。
f:id:dnasoftwares:20170312192625p:plain

Metalicのパラメタなどはお好きにどうぞ。
また、「yellg」など「~~lg_uv_material」で終わる名称のマテリアルは本来自己発光のマテリアルです。こちらでも光らせましょう。
AlbedoからEmissionに色をコピー(Emissionのスポイトを押してAlbedoの色を拾う)します。ちょっと明るさを1以上にして、カメラのHDRレンダリングを有効にしてBloomのイメージエフェクトを適当に盛ったらこんな感じ。
f:id:dnasoftwares:20170312192816p:plain

テクスチャの濃度設定とか密度設定とかは無視されている

DoGA-L3ではテクスチャの濃度設定ができました。薄く模様を張るといった表現ができたんですが、Unityへのインポート時にこのパラメタは無視されてしまいます。
気になる場合は自力でテクスチャを薄くしてしまいましょう。テクスチャを複製して適当なエディタで白を重ねればそれっぽくなります。
このモデルでも濃度設定を使っていたので自分で白を載せました。

f:id:dnasoftwares:20170312193745p:plain

同様に密度設定も無視されますのでこちらも注意が必要です。

メッシュを統合する

1モデルのために10以上のGameObjectがぶら下がる構図ははあまり良い感じがしません。メッシュがまとまっていないことでメッシュコライダーの作成ができないなどの弊害もあります。
メッシュをBakeできるツールで統合してしまうのがいいでしょう。ついでにマテリアルも統合してしまえばSetPassの低減につながります。
ウチではPro Draw Call Optimizerを使っています。

ローポリな表現がお好きで、テクスチャがいらない場合はMesh Materializerでも面白いです。マテリアルの色設定やテクスチャの色を元に頂点カラーに変換しつつメッシュを統合してくれます。
ついでにスムージングも切ったり入れたり自由自在。往年の「生ポリゴン(もはや死語ですね……)」な感じが出せます。

さてここではPro Draw Call Optimizer(以下Pdc)で進める事にします。Pdcのウィンドウで当該GameObject以下を指定します。

f:id:dnasoftwares:20170312195514p:plain

また、Settingsで「Generate Prefabs for objects」を選ばないとモデルデータをシーンの外に出すことができません(メッシュデータがファイルとして生成されない)。使い回すモデルの場合はチェックを忘れずに。

f:id:dnasoftwares:20170312195626p:plain

あと、モデルが原点以外にある場合はかならず一度原点に戻してください。そのままだと原点がズレます。
ということで諸々設定してBakeしたら統合済みのモデルに置き換わり……

f:id:dnasoftwares:20170312200305p:plain

……なんでか色設定が飛んでしまいました。どういうことでしょうか。ほかにもいくつか怪しい点があります。

Pro Draw Call Optimizerの罠

テクスチャと色設定を合成できない

Pdcは可能な限りマテリアルをシェーダ単位で結合し、テクスチャも1枚のアトラスに統合してくれます。テクスチャを使わない単色のマテリアルは単色テクスチャを生成してアトラス内に統合してくれるのですが、ここに罠がありまして、「テクスチャと色指定が同時に設定されている場合はテクスチャのみ考慮して色設定が破棄される」という仕様になっております。本来ならテクスチャと色設定を合成(普通は乗算ですね)して着色済みテクスチャを作ってほしいところです。

これだけは手作業でどうにかするのはだいぶ厳しい、ということでエディタ拡張を作りました。他の罠にも対処してから作業してほしいので、エディタ拡張の話は後に回します。

テクスチャのあるマテリアルとテクスチャのないマテリアルで別のメッシュになる

これはつまりテクスチャのないマテリアルにもテクスチャを設定してしまえばいいので、白の適当な(16x16ぐらいでいいはず)テクスチャを用意してテクスチャのないマテリアルのAlbedoに割り付けます。

Emissionはアトラスに反映されない

これはPdcの仕様と思われるので、一度Bakeした後、アトラス画像をいじってEmission用のアトラスを作る方向でなんとかします。

テクスチャに色設定を焼き付け(Bake)るエディタ拡張作った

さて、「テクスチャと色設定を合成できない」問題への対処としては「事前にテクスチャと色情報を合成して新しいテクスチャに置き換えておく」ということになるのは先ほど申し上げた通りですが、手動でどうにかするのはちょっと無理があります。そんなわけで、自動的に色を合成してくれるエディタ拡張を自作しました。当該エディタ拡張はGithubで公開しております。

github.com

普通はUnityPackageだけダウンロードすれば大丈夫。

プロジェクトにimportするとWindowメニューに項目が増えます。「D.N.A. Softwares」→「Material Texture Baker」を選びます。

使い方はPdcに近い感じに揃えました。(あくまで感じなのでデザインとかは違いますが……)

f:id:dnasoftwares:20170315023540p:plain

同じように親のGameObjectを選択し「+Children」を押すと子のメッシュを全選択してくれます。
あとは「Bake!」を押せば勝手にテクスチャに色が合成され、メッシュのマテリアルが更新されます。

処理前と処理後でテクスチャと色の設定が変わっていることを確認してください。
(Bake!の下のリストを展開するとテクスチャと色のペアを確認できます)

Bake前↓
f:id:dnasoftwares:20170315023548p:plain

Bake後↓
f:id:dnasoftwares:20170315023551p:plain

これで準備OK、今度こそPdcでメッシュをまとめます。こんどは綺麗に色も残るはずです。

仕上げ

あとはEmission用のAtlasを作りましょう。Atlasになったテクスチャを複製して、光らせたい色以外を真っ黒に塗りつぶすだけですので簡単。Pdcの出力一式はAssetフォルダ直下の「(編集中のシーン名)-atlas」のフォルダに保存されています。
Albedo用のAtlasでは逆にEmissionに該当する部分を黒にします。

f:id:dnasoftwares:20170315030458j:plain

あと、Metalic,Smoothnessの設定が飛んでしまうのでこちらも要再調整。
場合によってはAlbedoのAtlasをさらにコピーして他の要素用のマップを作ってもいいでしょう。

これでできあがり。
両方でSetpass Callsを比べると59→27と32減。めでたしめでたし。

Before↓
f:id:dnasoftwares:20170315024450p:plain

After↓
f:id:dnasoftwares:20170315024457p:plain

まとめ

というわけでちょっと手はかかりますが、工夫次第で十分実用できるモデルが作れます。
最適化作業はちょっとややこしいですが、慣れればルーチンワークです。(マテリアルに白テクスチャをセットするあたりもエディタ拡張で自動化できますね……)
ライセンス的にも個人制作にはかなり優しいのが嬉しいです。
また、パーツデータは自作してインポートすることもできます。パーツの一部だけ自分でモデリングして、あとは既存パーツと合わせたりすればかゆいところにも手が届くでしょう。ローポリ系アセットと組み合わせても違和感なく繋がりますので是非活用してください。

DoGA-L3で「取り急ぎ」かっこいいモデルを作ってUnityで動かして遊ぶ

!ライセンスについてDoGA様から返答がありましたので追記しております。(2/6更新)

気がついたら前回の投稿から1年が経とうとしていて焦っているDNAです。Twitterでは18年前に作ったゲームをリメイクできないかなーとちょこちょこと進捗(してない)動画を上げていたりしましたが、その中で愛用しているDoGAさんの「PLAY Animation Importer for Unity」が昨年12月に更新されて、大変使い勝手がよくなったので、というか私の要望に応えてバージョンアップしてくださいましたので、お礼になるかわからないのですがDoGA-L3を改めて紹介してみたいと思います。

DoGA-L3とは

DoGA-L3はDoGA-LシリーズというCGアニメーション入門ツールの一つです。
Webサイト(CGA入門キット DOGA-L シリーズ)の紹介によれば、

DOGA-Lシリーズは、まったく初心者の方でも、 手軽にCGアニメーション (以下CGAに略す) の楽しさを体験していただくことを目的に開発された、CGA入門システムです。

……だそうです。実際機能もかなり絞られていますが、3DCGアニメを制作するのに最低限必要な機能は揃っています。

f:id:dnasoftwares:20170129002223g:plain
たくさんある半完成のパーツを組み合わせてモデルを作り……

f:id:dnasoftwares:20170129002318p:plain
モデルを配置してシーンやアニメーションを作ります。

f:id:dnasoftwares:20170129002404j:plain
Direct3Dでリアルタイムプレビューできるほか専用レンダラーで高画質出力もできます。

f:id:dnasoftwares:20170129002759p:plain:w300 f:id:dnasoftwares:20170129002935p:plain:w300 f:id:dnasoftwares:20170129002937p:plain:w300 f:id:dnasoftwares:20170129002938p:plain:w300
パーツはカテゴリごとに整理されており総数はそこそこあります。発想次第で色々作れます。
元々15年以上前のツールなだけに、そのころのスペックに合わせたポリゴン予算でパーツが作られています。どのパーツも良い感じのローポリ感があります。
プラモデルで既製品のパーツを借りてディテールアップしたり別のモデルを作ることをキットバッシング(Kitbashing)というそうですが、まさにキットバッシングを3DCGでやるツールといえるでしょう。

で、このモデルを作る部分(パーツアセンブラ)のデータをUnityにインポートできるようにするのがDoGA謹製のPLAY Animation Importer for Unityになります。

DoGA-L3他、系列の各製品のライセンスを購入すると各製品のパーツアセンブラで製作したモデルデータをUnityにインポートできるようになります。ちょっとクセのある出力になっていますが、他のエディタ拡張アセットと組み合わせたりなんだりで綺麗にまとめることができますので、併せて紹介したいと思います。

本稿では画面写真や操作はDoGA-L3を前提に進めます。(DNAが持ってるのがDoGA-L3のライセンスなので……)本当はDoGA-L1,L2,L3とステップアップする設計なんですが、Unityが扱える方ならDoGA-L3もすぐ慣れるでしょう。詳しい説明はしませんのでDoGAのサイト等を見ながら進めてください。分からないと思った場合はより初心者向けのとてかんCGで進めてもいいかもしれません。いずれにしろシェアウェアなので試用して問題なければライセンスを購入してください。
なお、通常のライセンスは非商用利用に限っての許諾となっており、商用、対価を取る用途には使えません。
ただし、DoGA-L3に限り商用利用可能なプロフェッショナルライセンスが設定されているほか、同人ゲームにおいてはパブリッシャーを通さない範囲での頒布は通常ライセンスの購入で許諾されるとのことです。パブリッシャーを通しての販売を行う場合には別途お問い合わせくださいとのことでした。(DoGA様ご返答ありがとうございました。)
DoGAの掲示板ではインポーターを使ってインポートしたモデルの使用について見解が示されています。

http://doga.jp/2010/reading/diary/201407.html#20140730kama
> このプラグインによって、L3等の形状データをUnityで自作の
> ゲームに利用するだけでなく、L3等で作ったデータを
> Unity アセットストアで販売することが可能になります。

日記でかまださんがこう書いている通り、
商用ライセンスは不要でUnityアセットストアでの販売が可能です。

これまでも L シリーズのデータを使って同人ソフトの開発・販売は認めてきましたがその方針を維持しています。
(明らかに同人の規模を超える、また商業ベースだと商用ライセンスの導入をお願いしています)

ということで取り急ぎ作ったモデルに愛着が沸いたらそのまま完成まで使いましょう。

試す

前述の通りUnityへのインポートの条件として製品の購入が必要ですが、購入前にとりあえずインストールして手触りを確かめるのがいいでしょう。
CGA入門キット DOGA-L シリーズのページの「DoGA-L3」内「ダウンロード」でダウンロードページに進みます。お目当てはモデリング部分であり、動画を出力するわけではないので「新規インストール 最小限」のEXE一つで大丈夫。
バージョンが2008とかなっててちょっとびっくりしますが、Windows10でも特に問題なく動作するはずです。何か挙動不審があったらDOGA-L3 βから2010年のβ版も入れるといいかもしれません。

動作に問題が無ければライセンスを購入して登録してください。

とりあえずモデル作る

というわけでモデルを作ります。パーツアセンブラでもっそもっそパーツを盛りましょう。色・テクスチャもインポーターは読み取ってくれるので気兼ねなく。ただし表面材質の設定は追従しない(すべてLegacy Diffuseでインポートされる)ので、ここはインポート後にどうにかします(次の記事で書きます)。

f:id:dnasoftwares:20170129194802p:plain
ここでは弊サークルで以前作ったモデルを使います。上のGIFアニメで出てるやつですね。いまちびちびリメイクしようとしてるの自機(のデータがなくなったので、当時のレンダリング画像を見て再度作ったもの)です。ちょうどUnityインポートで引っかかる色々な要素が混ざってるので好適かなと。

モデルの保存時など特別なことをする必要はなく、普通に保存して問題ありません。DoGAのデータファイルの場所は、標準では「(DoGA-L3のインストールフォルダ)\data」です。ファイルセレクタではどのフォルダでも指定できますので、たとえばUnityプロジェクトのフォルダにしてまとめてバージョン管理下に置くことも一応できます。

Unityのプロジェクトを仕込む

ここでは新規のプロジェクトを作ります。もちろんインポート先のプロジェクトがすでにあるならそっちを開いていいでしょう。
DoGAPLAY Animation Importer for Unityから、最新版のunitypackage(20161223)をダウンロードしてインポートします。
Asset Storeにもあるのですが、こちらはまだバージョンが上がってないようです。

インポートとコンパイルが終わったら「Window」メニューに「PLAY Animation (TotekanCG) Importer」が増えていることと、それを選んだら開くインポーターのウィンドウで
「Installed DoGA Products」の欄に登録したツールが表示されていることを確認してください。
f:id:dnasoftwares:20170129201026p:plain
f:id:dnasoftwares:20170129201139p:plain

インポートしてみる

エクスプローラDoGAのデータフォルダを開けます。中にある拡張子.l3pのファイルをUnityのインポーターのウィンドウにドラッグ&ドロvップ。「Import tasks」リストにファイルが追加されるはずです。
f:id:dnasoftwares:20170206004936p:plain
Scaleの値がやたら小さかったりするのが気になるかもしれませんが、あまり気にせず「Import All」でインポートを開始してしまいましょう。材質の多いモデルだと10~20秒程度時間がかかります。オブジェクトはシーンの原点に生成されます。
f:id:dnasoftwares:20170206005008p:plain

また、モデルのPrefabがAssetsフォルダの直下に生成されます。ただし、このモデルには極めて変なクセがついています。後述。

使う

折角飛行機のモデルなんですから飛ばしてみましょう。毎度おなじみテラシュールブログにちょうどいい記事がありますので参考にします。
tsubakit1.hateblo.jp

基本的にはこの記事通りに進行すればモデルを空に飛ばせます。ただし上の記事通りに作業を進めようとすると数点引っかかると思います。

メッシュがマテリアル単位で別のGameObjectに分割されている

何かしらのメッシュ統合アセットを使う必要があります。メッシュコライダの作成がうまくいかないと思いますのでとりあえず近似のBoxやCylinderなどを組み合わせましょう。

謎の回転と拡大がついている

角度は親も子も全部0にすれば元通り、拡大も親子とも1倍に戻せば元通りになります。割と謎。

両方設定して適当に床を置いてモデル位置を調整、カメラは手抜きですがとりあえず飛行機の子にして勝手に追従させましょう。
背景が寂しいのでMaruchuさんのTekitouCityGeneratorを使います。
many.chu.jp

パラメータをわちゃわちゃ調整しつつできあがったのがこちら。

とてかんCGインポーターで作ったモデルを飛ばす(1)

とりあえずとしてはこれでいいんですが、もうちょっと色々できますので以下次号。

Projectorでデカールもどき

突然地味なUnityネタが沸いてくるD.N.A.のおぼえがきですいかがお過ごしでしょうか。

※今回の記事では利用例のテクスチャにハムコロ様STG素材を使用しています。

いきさつ

STGの試作を延々続けておりますが、地味に困ってるのが「Terrainの上の地上物の破壊跡の処理」です。
板ポリゴンにテクスチャを張っただけでは、Terrainにめり込んだりして見た目がよろしくありません。

f:id:dnasoftwares:20160221182338p:plain

そういうときはDecal(オブジェクトの表面に沿って別のテクスチャを張る)を使うのですが、
Decalを実現できるアセットというといくつかあるものの……

と、やることに対して$がかかりすぎる感じだったり微妙な物足りなさがあったり……と悩んでいたのですが、
良く考えればUnity標準機能のProjectorがあるじゃない、ということでProjectorでなんとかしてみようと試みたのでした。

シェーダーいじり

Standard Assetで入手できるProjector用のシェーダーは、

  • Multiply
  • Light

の2つだけです。MultiplyもLightもテクスチャの色をそのまま貼り付けられないので、これらを元にテクスチャの色をブレンドせず貼り付けられるシェーダーを書きます。以下、ProjectorLight.shaderを元に改造したコード。

リスト ProjectorDecal.shader

Shader "Projector/Decal" {
	Properties {
		_Color("Main Color", Color) = (1,1,1,1)
		_ShadowTex ("Cookie", 2D) = "gray" {}
	}
	Subshader {
		Tags {"Queue"="Transparent"}
		Pass {
			ZWrite Off
			ColorMask RGB
			Blend SrcAlpha OneMinusSrcAlpha //ブレンドを通常のアルファブレンドに
			Offset -1, -1

			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#pragma multi_compile_fog
			#include "UnityCG.cginc"
			
			struct v2f {
				float4 uvShadow : TEXCOORD0;
				UNITY_FOG_COORDS(2)
				float4 pos : SV_POSITION;
			};
			
			float4x4 _Projector;
			float4x4 _ProjectorClip;
			
			v2f vert (float4 vertex : POSITION)
			{
				v2f o;
				o.pos = mul (UNITY_MATRIX_MVP, vertex);
				o.uvShadow = mul (_Projector, vertex);
				UNITY_TRANSFER_FOG(o,o.pos);
				return o;
			}

			fixed4 _Color;
			sampler2D _ShadowTex;
			
			fixed4 frag (v2f i) : SV_Target
			{
				fixed4 texS = tex2Dproj (_ShadowTex, UNITY_PROJ_COORD(i.uvShadow));
				texS.rgb *= _Color.rgb;
				// texS.a = 1.0-texS.a; //デカールの場合はアルファ値はそのままでいいので削除

				fixed4 res = texS;

				UNITY_APPLY_FOG_COLOR(i.fogCoord, res, fixed4(1,1,1,1));
				return res;
			}
			ENDCG
		}
	}
}

いじったのは

  • FallOffの設定はいらないのでパラメータごとバッサリ削除。
  • Blendをアルファブレンドに。
  • アルファ値を反転する処理がいらないので削除(このままだと透明と不透明が反転してしまう)

の3点。これでうまくいきました。

配置してみる

実際にシーンに置いてみます。Projectorを配置して地面に向け、下記のようにプロパティを設定。

f:id:dnasoftwares:20160221182426p:plain

Orthographicを有効にして地面との距離によってデカールが変化しないようにします。Materialには先ほどのシェーダを指定したマテリアルを用意して割り当て。Cookieとして破壊跡の絵を用意。Ignore Layersで背景以外全部を指定し変なところに跡がつかないように。テクスチャの方もWrap Modeを「Clamp」にするのを忘れずに。

実際にはこんな感じになります。
f:id:dnasoftwares:20160221182953p:plain

ちゃんとTerrainに沿っていることがわかります。
f:id:dnasoftwares:20160221183002p:plain

まとめ

簡易的なデカール表現としてはこれでいいでしょう。ただ、板ポリならShader次第でなんでもありだった凝った表現ができないのと、ライティングを一切考慮していないままなので、当然ライティングに一切追従せず、使いどころは極めて限定されます。やっておいてなんですが基本は板ポリでしのぎつつ、必要なところだけProjectorにするといった方がいいでしょう。

あと、誰かTerrainにDecal張れるアセットご存じでしたら教えてください。(Asset Storeの説明文だとそのへんがわからないヤツが多くて突撃しづらい……)

Arbor: State Diagram Editorを試す - 縦STGの敵機を作ってみる

ケットシーウェアさんのArbor: State Diagram Editorというアセットを試食してみたのでその記録。

前置き

Unityで2.5D縦STGを作ろうという実験を半年以上超スローペースで続けているのですが、以前からずーっと困っているのが敵キャラの挙動です。単純な地上敵ならパス+スクリプトで弾を適当に撃つ、ぐらいで済むわけですが、中型機やボスキャラのように手の込んだものが必要となるとスクリプト直書きはちょっときついし増産にも向かない、ということで、ビジュアルスクリプティングでどうにかならんものか、とアレコレしていました。

Arborの前にはド定番のPlayMakerでいくつか作ってみたのですが、

  • ステートにぶら下げるアクションがデフォルトで凄く揃ってる
  • アクションのパラメータを変数で与えられるので外部パラメタの導入もしやすい

その一方で、

  • 全部アクションで書いちゃうと冗長になって後から何やってたのか読めなく
  • ありもののアクションの組み合わせばかりだとステート遷移が増えがちで見通しがドンドン悪くなる
  • かといって新規にアクションを作るのも面倒(独自のお作法が多い)

ということでちょっと使いにくいと感じていたところでした。

ちょうどそこにキャンペーンでArborが試せるということで、試食してみた次第です。

……本当は8月中に上げるはずだったんですが……

導入

アセットストアで購入してインポート。難しい事はありませんが、更新時は必ずArborのフォルダを削除してからインポートするようにと指示があります。
また、1.7.4での話ですがPro Draw call Optimizer Lightと一部クラスが名前衝突していてコンパイルが通らなくなっていました。
Pro Draw call Optimizer Lightを削除すれば問題なし。

試食

下ごしらえ

まず敵機を用意します。

f:id:dnasoftwares:20150905112240p:plain

どこにでもある中型機です。DoGA-L3で組み立ててPLAY Animation Importer for Unityでインポートしました。doga.jp

Unity5にまだ対応してないので、Unity4.6でインポート→スケール調整→まとめてunitypackageに→Unity5に持ち込み と作業します。

で、コンポーネントをペタペタと。オレオレコンポーネントがありますが本筋ではないので下記説明程度で。

f:id:dnasoftwares:20150905112544j:plain

敵機の基本的な動作は専用コンポーネントに分離。Arborで動かすのはあくまで移動と攻撃だけに絞ります。

ステートマシンをつくる

よくある中型機として、出現→攻撃→逃走 の流れを作ります。今回は攻撃1種だけでは単純すぎるのでライフの残量をみて発狂フェイズに移行するようにしました。
先に完成図を挙げます。

f:id:dnasoftwares:20150905125129j:plain

「開始ステート」になっている左上の状態からスタートして、コード内で状態遷移が発生したら矢印の先に進みます。
ステートごとに1つ以上の「挙動」を追加できます。挙動1つ=MonoBehavior1つ みたいなノリです。実際はStateBehaviorというクラスの派生クラスになります。
スクリプトの作成は普通のコード作成同様にプロジェクトビューで。プロジェクトビューの右クリックメニュー、「Create」の内側に「Arbor」が増えており、そこからStateBehaviorの雛形を新規作成できます。
開始ステートはAppearとあるように出現処理です。EnemyMid2Appearというコードをひっつけています。
独自のコントローラを使ってるのでアレですが説明を入れてるのでそういう処理をしていると適当に理解いただければと思います。

EnemyMid2Appear.cs
using UnityEngine;
using System.Collections;
using Arbor;
using DG.Tweening;

public class EnemyMid2Appear : StateBehaviour
{
    //次のステート(エディタで設定する)
    public StateLink NextState;

    //コントローラへの参照
    private EnemyController _ctrl;
    // Use this for initialization
    void Start()
    {
    }

    // Use this for enter state
    public override void OnStateBegin()
    {
        transform.eulerAngles=new Vector3(0,0,0);
        _ctrl = GetComponent<EnemyController>();
        _ctrl.LocalVelocity = new Vector3(0, 0, -58.0f); //この機体に画面下に向けて降りてくるベクトルを設定
        StartCoroutine(StateThread()); //以下コルーチン任せ
    }

    IEnumerator StateThread()
    {
        while (transform.localPosition.z>=80.0f) //この機体が画面上1/4ぐらいのラインに下ってくるまで
            yield return new WaitForFixedUpdate(); //待機
        Transition(NextState); // 次ステートへ遷移
    }

    // Use this for exit state
    public override void OnStateEnd()
    {

    }

    // Update is called once per frame
    void Update()
    {

    }
}

ジャンプ先のステートを格納するpublic StateLink NextState;と実際に遷移を実行するTransition(NextState);がキモかと。
StateLinkの変数を定義するとArbor Editorにリンクとして表示され、次のステートへD&Dの操作で矢印を引けるようになります。

普通にStartCoroutineが呼べるのでかなり書きやすいです。

図で飛び先となっているLoopステートの挙動はこちら。

EnemyMid2Loop.cs
using UnityEngine;
using System.Collections;
using Arbor;
using DG.Tweening;

public class EnemyMid2Loop : StateBehaviour
{
    public StateLink GoMadState;    //発狂モード突入時の飛び先(エディタで設定)
    private EnemyController _ctrl;  //コントローラへの参照
    public GameObject[] Firepoint;  //敵弾の発射地点を示すGameObject(エディタで設定)
    public GameObject BulletObject; //敵弾として吐き出すGameObject(エディタで設定)
    [Range(0,100)]
    public float GoMadLifePercent=30.0f; //発狂モードに突入するライフ%値(0~100)

    private Coroutine _cFireController;

    // Use this for initialization
    void Start () {

    }

    // Use this for enter state
    public override void OnStateBegin() {
        _ctrl = GetComponent<EnemyController>();

        DOTween.To(() => _ctrl.LocalVelocity.z, x => _ctrl.LocalVelocity.z = x,
            0, 1.0f); //前ステートでかかっていたzベクトル(縦方向速度)を1秒かけて0にする
        _cFireController = StartCoroutine(FireControllerThread()); //攻撃制御開始
    }

    private IEnumerator FireControllerThread()
    {
        yield return new WaitForSeconds(0.4f); //最初の1セットの前に0.4秒待つ
        while(true)
        {
            Transform player = GameEnvironment.GetNearestPlayer(transform.position); //自分から近い方のプレイヤーの場所を得る
            for (int i=0;i<3;i++)
            {
                //FirePointそれぞれから……
                foreach(GameObject fp in Firepoint)
                {
                    //プレイヤーに向けて3Way弾を撃つ
                    float angle = DNASoftwares.Unity.MathEx.AimProjected
                        (fp.transform.position, player.position); //近い方のプレイヤーの角度を得る
                    _ctrl.CreateBulletAbsAngle(BulletObject, fp.transform.position, angle, 110); 
                    _ctrl.CreateBulletAbsAngle(BulletObject, fp.transform.position, angle-22.5f, 110);
                    _ctrl.CreateBulletAbsAngle(BulletObject, fp.transform.position, angle+22.5f, 110);
                }
                for (int j = 0; j < 8; j++) yield return new WaitForFixedUpdate(); //8フレームおき
            }
            yield return new WaitForSeconds(1.0f); //1セット撃ったら1秒待つ
        }
    }
    
    // Use this for exit state
    public override void OnStateEnd() {
        //遷移時には今の攻撃スレッドを停止させる
        StopAllCoroutines();
    }
	
    // Update is called once per frame
    void Update () {
        //ライフがしきい値を超えたら(_ctrl,LifePercentは自分の残ライフを百分率で返す)
        if (_ctrl.LifePercent <= GoMadLifePercent)
        {
            //発狂ステートへ
            Transition(GoMadState);
        }
    }

    // このステートの間動き続ける
    void FixedUpdate()
    {
        //横方向にゆらゆら動く動作
        _ctrl.LocalVelocity.x = Mathf.Sin(_ctrl.TimeElapsed/2.0f*Mathf.PI)*10.0f;
    }
}

FixedUpdateも書けば普通に動いてくれます。MonoBehaviorとまったく同じ挙動です。
また、StateLinkに限らずpublic変数はインスペクタ同様に表示・編集できますしアトリビュートもバッチリです。新しい事を覚える必要がほとんどないので取り組みやすいと思います。

発狂後の方はやってること自体は弾撃ってるだけなので本筋と外れてしまうので省略。

常駐ステートで割り込みをかける

一定時間で逃げる、という処理は色々書き方もあるかと思いますが、今回は「常駐ステート」を使ってみることにしました。

図の右下の緑色のステートが「常駐ステート」で、このステートの挙動は文字通り他のステートと別に常駐して動作します。
ここにタイマーを仕込んでおけば出現から一定時間(ここではSeconnds=12なので12秒)経過すると指定ステートに割り込みで遷移してくれるという手はずです。

あくまで今のステートに割り込む形ですので、常駐ステートの先も並列実行になるわけではありません。

逃げる挙動はAppearの逆でしかないので省略。

動かしてみる

実際に上図ステートマシンを動かした様子がこちらのツイートにあります。
オレンジ色のステートがそのとき動いているステートです。


所感

ということで非常に取り組みやすいなと思いました。デフォルトで付属してくる挙動スクリプトの量はPlayMakerには遠く及びませんが、自力でコードが書けるなら大した問題ではありませんし、
MonoBehaviorと同じノリでコードが書けますのでむしろ取り組みやすいと思います。
ステートをまたいだ変数の扱いなどが悩むところですが、専用にコンポーネントを作ればなんとかなるかと思います。RequireComponent属性で紐付けるとかすれば確実でしょう。

今回は手抜きして状態遷移とアクションを全部1スクリプトに押し込んでしまいましたが、状態遷移に関わるスクリプトと、実際になにか行動を起こすためのスクリプトは分割すると使い回しが効いていいと思います。

あと申し上げたいこととしては、チュートリアルが全編動画になっているのが個人的にはツラいです。動画だと自分のペースで進められないのと、Arborの場合はエディタの説明やら常駐ステートの説明などが動画の中に埋まっているため引用とかしづらい……(この記事書くのに困った)
ただリファレンスマニュアルは別途しっかりまとまっているので1回通してしまえば後の苦労は無いかと思います。

定価$95のPlayMakerと比べてArborなら定価$19.99とほぼ1/5のお値段で必要十分な機能が揃えられるので、自分でコードが書ける上で状態遷移の部分をスッキリさせたいという向きには十分にオススメできると思います。

Unity5のStandardシェーダのパラメタをスクリプトからいじろうとして丸一日潰れた話

Unity遊びの記録を書かずにまごまごしていたらUnity5になってしまって大変申し訳ない気分でいっぱいです。

お詫びの印じゃないんですが今やっている別ネタで発生した小ネタについて書き残しておきます。誰か既にやってると思ったら誰も書いてない、という……

Unity5を触った方ならご存じかと思いますが、Unity5からはだいたいの材質は物理ベースの統合シェーダ一つで表現されるようになりました。

docs.unity3d.com

このパラメータの中には自己発光を表す「Emission」が含まれています。
Emissionの値をいじることでモデルが自ら光るような表現ができるわけですが、これを応用してSTGでの与ダメージ表現をやってみようと思ったわけです。

ざっくり実装手順を。

スクリプトからマテリアルのパラメータをGet/Setするために、まずパラメータの内部名称を調べます。調べたいシェーダーを指定しているマテリアルをインスペクタで開き、インスペクタ右上隅の歯車マークをクリック。「Select Shader」を選びます。

f:id:dnasoftwares:20150319032129p:plain

そうするとシェーダーの情報が出ます。

f:id:dnasoftwares:20150319032134p:plain

左の赤枠、「_」から始まっている文字列がパラメータの名前です。
同じ行に型と、インスペクタでの表示名が出ています。表示名を参考に、いじりたいパラメタはどの名称かを調べます。

ここではエミッションをいじりたいので_Emissionなんたらというのを探しますが、
どうやら_EmissionColorが探していたもののようです。

じゃあこの_EmissionColorなるパラメータをいじればよさそう、ということで下記のようなコードを試したのですが……

  //適当なMonoBehaviorの内側……
  Renderer r = GetComponent<Renderer>; //Rendererコンポーネントを取得(Material取得のため)
  r.material.SetColor("_EmissionColor", new Color(1,0,0); //とりあえずEmissionで真っ赤にしてみる

ところがこれだとうまくいきません。最初からEmissionが指定されているマテリアル以外では色がつかないのです。

これの理由がわからなくてオロオロしてたのですが、マニュアルやらシェーダーのソースやらを掘り返してようやく理由がわかりました。

docs.unity3d.com

キモの部分だけ一部引用

Often it is convenient to keep most of a piece of shader code fixed but also allow slightly different shader “variants” to be produced. This is commonly called “mega shaders” or “uber shaders”, and is achieved by compiling the shader code multiple times with different preprocessor directives for each case.

In Unity this can be achieved by adding a #pragma multi_compile or #pragma shader_feature directive to a shader snippet. At runtime, the appropriate shader variant is picked up from the Material keywords (Material.EnableKeyword and DisableKeyword) or global shader keywords (Shader.EnableKeyword and DisableKeyword).

全訳はしませんが、"#pragma multi_compile"あるいは"#pragma shader_feature"がC#でいういわゆる#defineみたいなもので、キーワードの有効・無効を切り替えることでシェーダーのコンパイルを分岐できるということのようです。んで、スクリプトからキーワードを有効、無効にする手段があってそれがMaterial.EnableKeywordMaterial.DisableKeywordだと。

シェーダーのソースはUnityのダウンロードページで「ADDITIONAL DOWNLOADS」から「Built in shaders」を選べば入手できます。

Standard.shaderの一部引用。

#pragma shader_feature _NORMALMAP
#pragma shader_feature _ _ALPHATEST_ON _ALPHABLEND_ON _ALPHAPREMULTIPLY_ON
#pragma shader_feature _EMISSION
#pragma shader_feature _METALLICGLOSSMAP 
#pragma shader_feature ___ _DETAIL_MULX2
#pragma shader_feature _PARALLAXMAP

「#pragma shader_feature _EMISSION」どうやらこのあたりらしい、ということで下記のように書き直してみたところ、期待通りに赤色に光ってくれました。

  //適当なMonoBehaviorの内側……
  Renderer r = GetComponent<Renderer>; //Rendererコンポーネントを取得(Material取得のため)
  r.material.EnableKeyword("_EMISSION");
  r.material.SetColor("_EmissionColor", new Color(1,0,0); //とりあえずEmissionで真っ赤にしてみる

例として1秒おきにEmissionを切り替える→元にもどす、を繰り返すコードを用意しました。
コルーチン使ってますけど別にいいよね?(1秒おきに云々とかUpdateで書くのめんどくさいし)

動く方のコード
using UnityEngine;
using System.Collections;

[RequireComponent(typeof(Renderer))]
public class MaterialChangeTester : MonoBehaviour
{
    private Renderer _renderer;

    // Use this for initialization
    void Start ()
    {
        _renderer = GetComponent<Renderer>();
        StartCoroutine(BlinkerCoroutine());
    }

    IEnumerator BlinkerCoroutine()
    {
        //こちらは動く例
        //変更前のマテリアルのコピーを保存
        var originalMaterial = new Material(_renderer.material);
        for (;;)
        {
            _renderer.material.EnableKeyword("_EMISSION"); //キーワードの有効化を忘れずに
            _renderer.material.SetColor("_EmissionColor", new Color(1, 0, 0)); //赤色に光らせる
            yield return new WaitForSeconds(1.0f); //1秒待って
            _renderer.material = originalMaterial; //元に戻す
            yield return new WaitForSeconds(1.0f); //また1秒待ってくりかえし
        }
    }
}
動かない方のコード
using UnityEngine;
using System.Collections;

[RequireComponent(typeof(Renderer))]
public class MaterialChangeTesterBadExample : MonoBehaviour
{
    private Renderer _renderer;

    // Use this for initialization
    void Start ()
    {
        _renderer = GetComponent<Renderer>();
        StartCoroutine(BlinkerCoroutine());
    }

    IEnumerator BlinkerCoroutine()
    {
        //こちらは悪い例(変化しない)
        //変更前のマテリアルのコピーを保存
        var originalMaterial = new Material(_renderer.material);
        for (;;)
        {
            _renderer.material.SetColor("_EmissionColor", new Color(1, 0, 0)); //赤色に光らせたい
            yield return new WaitForSeconds(1.0f); //1秒待って
            _renderer.material = originalMaterial; //元に戻す
            yield return new WaitForSeconds(1.0f); //また1秒待ってくりかえし
        }
    }
}

それぞれSphereに割り付けて、適当なStandardシェーダーをマテリアルにして実行したら下のような感じになります。
f:id:dnasoftwares:20150319095404g:plain
応用すればこんな表現に。(というかこれがやりたかった)

というわけで参考になれば幸いです。