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の再生エンジン使わせていただきたい……なんとか……