『エア搬入T.T.』のPV撮影におけるCinemacineとUnity Recorderについて

GWの「エアコミケ」期間中にunityで『エア搬入TT』というレースゲーを作ったことを前回の記事でお話いたしました。
dnasoftwares.hatenablog.com
今週は「博麗神社例大祭」中止に伴う「エア例大祭」をやるぞー、ということで


大ボスからネタ(と資料)を振られてしまったので作っておりました。

紆余曲折七転八倒しましたが、当初の例大祭の設営搬入をやっているぐらいの時間帯に新バージョンを公開できました。
www.dna-softwares.com

……なぜかunityroomで起動しないバイナリになってしまったので、自分のサークルのサイト上での公開ですが。
今度naichiさんに聞かなきゃ……

Unity上でカッコイイ映像素材をつくる

ゲームが完成したらやはりPVを作りたいのです。というよりPV作らないと今のご時世、拡散されない可能性が高いです。
せっかくレースゲーなんで、理想は高くこんな感じで。

youtu.be

カメラワークの参考も兼ねて最近ずっとベストモータリング/ホットバージョンの動画ばっかり見てるんですよ、テレビ流し見感覚で見られるので良い。

幸いUnityには良い感じのカメラワークを作ってくれるパッケージと動画を撮るパッケージのそれぞれが公式で供給されています。それが表題のCinemacineとUnity Recorderです。

Cinemachine

unity.com
プロシージャルに特定の物体を追うカメラワークを作れる便利なパッケージですが、カメラをスイッチングさせるのはどうもTimelineあたりを使わせる前提のようです。
ClearShotを使えば「被写体が障害物で隠れたら別アングルにする」という処理は自動化できますが、イベント会場ってそんなに障害物があるわけじゃないので、一つのカメラに固定されがちになる可能性が高くあまり期待できません。
そこで、今回は自前でカメラをアクティベートするスイッチを作ります。

Unity Recorder

docs.unity3d.com
Gameビューを録画するだけでなく任意のレンダーテクスチャをキャプチャできるのが大きい。今回はこの機能に着目しました。

これら2点を組み合わせて、普通の通しプレイをCinemacineで追い、ゲームの裏でキャプチャするという仕組みをこさえることとしました。

プランニング

まずは録画に使うシーンを複製して新しいシーンをこさえます。色々付け足すので、ゲーム本体とは分離しましょう。

カメラを置く場所を固定しなければならないので、事前に走行ルートを決めてしまいます。ルートを決めたらチェックポイントやカーブの走行ラインを想定してVirtualCameraを配置。

f:id:dnasoftwares:20200517002248p:plain
緑線→想定ルート 白矢印→チェックポイント 赤丸数字→通過順 カメラと歯車のアイコン→CinemacineVirtualCameraの設置場所

あとはこれを車両が近づいたときアクティベートしていけば、それっぽいカメラワークができるはず、というわけです。

スイッチの準備

指定オブジェクトが特定の範囲に入る、はTriggerを使えば実現できそうです。こんなコードを用意しました。

using UnityEngine;

public class VCamActivationBoundary : MonoBehaviour
{
    [SerializeField] private Cinemachine.CinemachineVirtualCamera vCamTocativate; // この判定でON/OFFするVirtualCameraの参照
    [SerializeField] private bool activateOnlyOnce; // 一度きり?(falseだと何度出入りしてもスイッチングする。trueの場合は最初の一度だけ)

    private bool _activated; // 一度スイッチングした?

    private void Start()
    {
        vCamTocativate.enabled = false; // 初期状態はカメラOFFから
    }

    private void OnTriggerEnter(Collider other)
    {
        if (other.gameObject.layer!=LayerNames.Vehicle && other.gameObject.layer!=LayerNames.VehicleBody ) return; // ゲーム固有処理。自車両のオブジェクト以外がトリガーに触れてもなにもしない
        if (activateOnlyOnce && _activated) return; // 一度きり反応の設定がONで既に一度スイッチングしてるならなにもしない
        vCamTocativate.enabled = true; // VirtualCamera有効
        _activated = true; // 一度スイッチングした
        Debug.Log(vCamTocativate.name+" Activated");
    }

    private void OnTriggerExit(Collider other)
    {
        if (other.gameObject.layer!=LayerNames.Vehicle && other.gameObject.layer!=LayerNames.VehicleBody) return;
        vCamTocativate.enabled = false; // VirtualCamera無効
        Debug.Log(vCamTocativate.name+" Deactivated");

    }
}

これで、VirtualCameraがTriggerに入ったらON、出たらOFFの動作が実装できました。
実際の走行ルートを考えてカメラのスイッチング範囲を設定し、カメラをリンクさせていきます。いずれのカメラ範囲にも入っていない場合は自車両視点のカメラを適当に選んで貰うようにします。これにはClearShotが好適でしょう。
車両手前からルーズに捉えるビュー、車の真上から車両周辺を広く捉えるビュー、一人称視点を用意し、ランダムで選ばせます。プライオリティを最低にして常駐させておけば、他のカメラが全てOFFのときは自動的に車両追尾になるというわけです。
残りのVirtualCameraは適当にプライオリティを設定しますが、スイッチング範囲が重なったとき、基本的には古いカメラよりは新しいカメラの視点の方が優先されるべきだろうと思いますので、スイッチングの順番にVirtualCameraのプライオリティを上げていけば良いと思います。(最初のカメラ=最低、でそれから順番に1ずつ足す)

動作テストとキャプチャ

さて実装したらテストをしなければなりませんが、ゲームを遊びつつCinemacineの生成結果をプレビューする必要があるわけです。流石にCinemacineの画面だけ見てプレイするのはただのラジコン操作以上の難しさなので、ここではゲーム画面は元のまま残しつつ、Cinemacineの出力を小さくプレビューさせようと思います。
といってもやることは単純で

  • 出力したい動画の縦横と同じサイズのレンダーテクスチャアセットを新規作成
  • Cinemacineで制御する録画用カメラの出力先をレンダーテクスチャにする
  • そのレンダーテクスチャをUI Raw ImageでゲームUIと一緒に表示する

だけです。Unity Recorderではレンダーテクスチャをキャプチャ(録画)対象にできるので、これでUnity Recorderの準備も一緒にできたようなものです。
操作方法に従い適当に録画設定を作成。「Capture」を「Render Texture Asset」にして、先ほどのレンダーテクスチャを指定。あとはSTART RECORDINGを押してゲームを遊べば外部視点が勝手に録画されるという寸法です。
録画を始めずに単に実行すればテストになりますので、カメラワークや反応範囲を調整して、出来たと思ったら録画ONにして通しプレイ。事故や曲がるところの間違いに気をつけつつ走りきったらあとは適当な編集ツールで好きにしましょう。

実際にカメラが動いている様子をエディタビューごとキャプチャしてみました。スイッチング範囲も可視化されているのでカメラの切り替わりがわかりやすいかなと思います。

エア搬入T.T. PV(?)メイキング

まとめ

この方策、比較的元のゲームに手を付けずに実装できるのでとっかかりは楽です。ただ、如何せん毎回手操作が必要なので全く同じ動作が再現できないのが厳しいところ。
マジメにやるならリプレイシステムの実装も必要ですが、UnityでRigidbody絡んで可変FPSでリプレイとか地獄しか見えませんね……
ゲームのジャンルによりますが、簡易なPVであればこういった手法で素材映像を作るのも悪くないのではと思います。
ただ、Unity Recorderがちょっと重いらしく、音声を同時キャプチャすると高確率で動画とズレるのが大問題。PVで効果音がほとんど入ってないのはそこが理由です。(そもそもキャプチャの段階で音声を録音していない)
この辺はもうちょい正確な方法を探しています。

記事中ではエアコミケ用のコミケ会場を模した東京コースを使いましたが、同じ手法で静岡コースにカメラを配して作った新しいPVではさらに事後の編集も多めにしています。

【エア例大祭】エア搬入T.T. 静岡コースPV

  • ちょっと早回し(×1.1~×1.2)にして間延びを抑える
  • 長距離移動シーンはカッコイイ所だけ残す
  • スピード感あるシーンが欲しかったのでチェックポイントを一部無視
  • 普通のゲームプレイも録画して一部に混ぜる

最初のPVよりはまあまあメリハリある動画になったかなと思います。

というわけで課題はあるものの動画素材をそこそこ早く用意するにはいい手じゃないかと思っています。
3DゲームのPV作成で悩んだらCinemacine+UnityRecorderでコンピュータ任せのカメラワーク、それなりにお勧めです。

エアコミケの空気にあてられて一人ゲームジャムを敢行した話

最近は麻雀のバージョンアップを続けておりましたが、このSTAY HOME週間は唐突にこんなゲームを作っておりました。

【エアコミケ98】『エア搬入T.T.』プロモーション映像
ゲームはこっちから遊べます。
unityroom.com

連休ラストでもありますし、以前の1週間ゲームジャムのときのように振り返りをしてみようと思います。

きっかけ

耳にタコが出来るほどSTAY HOMEを呼びかけられて何かとテンションも下がりがちな連休()前・4月最終日の木曜深夜、こんな2つのツイートが流れてきました。

このツイートと動画と在庫の山を見ててふと、14年前の夏コミ(C70)で頒布したゲームのことを思い出しまして……

f:id:dnasoftwares:20060718083754j:plain
まわるめいどさんをねみぎ
nemigi.dna-softwares.com

ゲーム本体はまだおうちのPCにあるんですが残念ながら起動しなくなってました。さておきこのゲーム内のミニゲームコミケの島中や壁を模したコースを走って、指定したスペースに停める」というものがありました。
ミニゲームなのでカーブ一つ曲がってブレーキ程度のものだったんですが、「今だったらこれコミケ会場フル再現できるのでは?」とふと閃きます。

当初のコミケの日程は

  • 5/1 前日設営
  • 5/2 初日
  • 5/3 2日目
  • 5/4 3日目 (前日搬入はこの日が最後)
  • 5/5 最終日

ということで、エアコミケはこの日程で開催している体でアレコレツイートするから、ついでに他のユーザーも参加している体でツイートしてくれとのこと。
まあ、出かけることもできないなら腕試しとしてこの「車両搬入」をゲーム化してみようか、とチャレンジすることにしました。
リリースまでの道のりをダラダラと日記的に書き連ねてみます。

-1日目 (4/30深夜)

  • モデル作成
  • テスト用レベルの作成
  • トラックの挙動決定

閃いた時間が23時とかだったので取りあえず眠くなるまでやってみるべーとせこせこモデル作成開始。
一応イベント主催もやっておりますので親の顔より見た長机の寸法など調べるまでもありません。(え?今年の日程?……緊急事態宣言の先行き次第じゃないスかね……)
1800W×450D×700H、だいたいのイベントの長机はこの仕様ですので、パパッと作成します。

小物のモデリングですが、今回はAsset Forgeというのを使いました。
assetforge.io
f:id:dnasoftwares:20200506002031p:plain
(図は適当に作ったダンジョンの一室的なもの。3~5分あればこのぐらいはすぐ)
要するにDoGA-L3みたいな感じでパーツくっつけてモデルつくるヤツなんですが、パーツの種類にバリエーションが多く、多彩なジャンルのモデルを作れます。SFに限らず現代・中世などのモデルも可。
FBXを直接エクスポート可能、かつUnity向け設定プリセットがありモデリングが終わったらすぐさまUnityに持ち込めるのも強みか。

机の次は搬入の主役?たる段ボール。ピコ手から大手まで積み具合を変えて複数、さっくり完成。
情景パーツだけじゃなくて主人公たるトラックの方も作らねばなりませんが、こちらは素材をそのまま使ってしまう方向に。

Asset Forge作者のKenney氏はゲーム向け素材集も出しており、この中にちょうど手頃な車両モデルがあったため使うことにしました。
Kenney Game Assets 3
買うだけ買って全然使えてなかったのでいい機会でした。プロトタイピングやゲームジャムではこういうモデルが超役に立ちます。

さて、Unity上で長机を並べるわけですが、1.8mをびっしり並べるというのは若干めんどくさいのでアセットに頼ることにします。

コライダーに沿ったり物理に従って良い感じにオブジェクトを撒いたりできるツールなんですが、配置機能の一つに等間隔配置があり、コレなら島作るのほぼ一瞬じゃん!(島の端っこの回転だけは手作業だけど)ってことで採用。
これで適当に島を並べてイベント会場っぽくして、テストフィールドを準備。

この段階で既に複数のサークルに荷物を配達するゲームということだけは固まっていたので、先に トラックの挙動を作ってしまおうと思いました。ここは自作で良い感じに出来る気がしなかったので、最初からアセット買う気満々でした。
アーケード挙動のアセット複数が候補に挙がりましたが、とにかく「壁ヒットの判定が車両形状通りちゃんと取れる」かどうかを調べて(カジュアル向けの車両挙動のアセットは球を転がすことで近似してるようで、特に側面の判定が見た目通りになってないことがありました)たどり着いたのが下のアセット。

無料版もあったんですが、5$ならいいかーってノリで購入。変なマネージャーやらへの依存もなくこちらで用意したモデルへの組込はあっさり完了。パラメーターの決定にはやや苦労しましたが最終的には遊びやすい形に収まってくれました。ジャンプとかブーストとかいう機能がありましたが当然いらないので削除。代わりに完全停止できるよう「ブレーキ」を実装。
この手の車両コントローラってブレーキからそのまま滑らかにバックに移行するヤツばっかりで「完全に止まる操作」って意外と入ってないこと多いですよね……

あと停車目標によさげなオブジェクトを準備。クレイジータクシーみたいに地面から枠が伸びてる感じがよかったんですが、自前ではしっくりこなかったためこちらもアセットに頼る方向へ。


抽象的な方がいいかな?ってことでフラットポリゴンなこれ(例によって購入済みだったのに使いどころが略)を採用。ちょうど都合良く「Checkpoint」なるパーティクルループがあったので有り難く使うことに。他のエフェクトもすべてこのアセットで揃えています。

以上の情景パーツを並べて下のスクショのようになったのが午前5時。
f:id:dnasoftwares:20200506004545p:plain

いける、と判断してスクショをツイートして、スタッフの知人に「BS東館の資料とか持ってないですかね?」とDM飛ばして就寝。
ビッグサイトのWebサイトに図面があったんですが、気づいたのは寝て起きた後のことでした)

0日目(5/1)

  • 本番用マップの作成
  • トラックのPrefabでテスト
  • エフェクト類準備
  • HUDのモック作成
  • ゲーム内情報を格納するコードの作成

起きたら知人が過去イベントの図面を見せてくれました。これで456ホールの寸法がわかったのでモデリング可能に。
早速モデリングなんですが、マジメにやってたんじゃ終わるわけがないので「 ドア・お手洗い、壁面の看板、天井など全部無視」と、とにかく搬入に関係しないものは全部無視の方向で決定。
割り切ると決まれば簡単です。Adobe Illustratorで図面をトレスして壁と柱のパスを作り、SVG出力してMetasequoiaで読み込み。パスを押し出せばあっという間に東ホールのできあがりであります。

f:id:dnasoftwares:20200506013205p:plain
図面の上から壁・柱のパスを起こす(マゼンタのところ)
f:id:dnasoftwares:20200506013617p:plain
SVG出力してMetasequoiaに送った状態(暗いところは床用のポリゴン)
f:id:dnasoftwares:20200506013825p:plain
上に押し出せばごらんのとおり

これをUnityに持って行って、図面の寸法と一致するようにスケーリング調整。
で、サークル配置図を参考に島の数等を調べ、机を並べたのでひとまずTwitterに進捗をアップしたんですが、まだ違和感バリバリです。

短辺側の壁サークルと島が近すぎる気がするし、全体的に何かすごい違和感が……と思っていたらスタッフ経験者諸氏から「ホール出入口とシャッターの間の島間隔は他より広い」と指摘が。より正確なサークル配置図も見せて貰って、そういえばそうだった、ってことで各所微調整して下図の通りに。

これで机配置を固定して、作っておいた車のPrefabとかをおいて実際に走らせてみました。

走りにくいということもなさそうなのでこのまま続行。停車目標へのインタラクションから実装していきます。ここは特に新しいこともなし。デバッグでわかりやすいように、停車目標とリンクしている荷物オブジェクトの間に線を引くようにしたぐらいでしょうか。

f:id:dnasoftwares:20200506022057p:plain
停車目標に止まると矢印の先にあるオブジェクトがPopする(エディタ上では荷物は表示したまま)

Unityのフォーラムの「Did you ever wish there was a Debug.DrawArrow()?(意訳:Debug.DrawArrow()とか欲しくね?)」に書かれていたコードを使っています。なるほど便利だわ。オブジェクトのリンク漏れ・リンク間違いがすぐわかって良い感じです。

そのまま他のルールに関わるオブジェクトを作成。

  • 障害物にぶつかったときのエフェクト。火花っぽい見た目で、ペナルティであると明確にわかる色に。これはPolygon Arsenalの中ではピッタリ合う感じのエフェクトがなかったので、近いイメージのPrefabから改変して作成。
  • ゴール地点のエフェクト。これはわかりやすいのがあったのでそのまま使用。

で、ゲームルールに目処が立ったところでHUDのモックを作成。モックと言っても値の更新の実装を後回しにするだけでUnity上で配置します。
デフォルトの画像リソース使いたくないマンなので先日セールだったこれを動員。

定価99$ですが先日のセールで半額でした。アイコンは微妙ですがUIパーツとカラースウォッチが大変良い。WebでいうBootstrap的な基本配色にまとめられるので、なんとなくUIが引き締まります。
フォント類はmojimo-game。ゴール演出用のテロップなどもこのタイミングで作成しました。あと、レーダー。常時見られる地図がないと絶対難しい、ということでミニマップの導入は必須事項と思っておりました。

ミニマップはこちらのアセットを使用。取りあえず設定済みのprefabをCanvasに置いて、レーダーに表示させたいオブジェクトにコンポーネント一つ貼ればもう準備OK……と導入がむちゃくちゃ楽で、外観の変更も簡単だったので助かりました。
UIを作るとゲーム中通して保持すべき変数が自然と浮かび上がるわけで、ここでゲーム通して参照される変数を入れるシングルトンクラス(ウチではGameEnvironmentと呼称しています)を作成。
ウチのUnity作品ではほぼ必ず存在する由緒正しき存在です。シングルトンに加えDontDestroyOnLoadを入れており、タイトルおよびゲーム中を通して同じオブジェクトを参照できるという形にしてあります。シーンを跨ぐ変数やシーン間でのパラメータ渡しなどに使用しています。
static変数よりはマシかなーと思ってやってるのですがどうなんですかね。

ここまで出来たところで取りあえず進捗ツイート用のGIFアニメを撮ったんですが、

f:id:dnasoftwares:20200506022633g:plain

どうせならこれは実際の搬入時間帯にツイートした方が面白いなと思ってツイートせず就寝。(身内のDiscordとFacebookには貼った)

1日目(5/2)

  • 台車モデルの作成
  • ゲームシーケンスの作成
  • 「ご注意」画面の作成
  • 全体マップ画面&走行記録表示
  • タイトル画面
  • ツイート機能実装(途中)

開催!コミ!ケット!(300人のスパルタ人に任せた動画のアレ)ってなもんで盛り上がるハッシュタグを眺めながら作業。Discordで「前日搬入あるある」として「他社の搬入車両」「邪魔なチラシ台車」などのネタを得たので、まずは台車から。適当にモノタロウのサイトで一般的な台車の寸法を調べ、Asset Forgeでさっくり作成。

f:id:dnasoftwares:20200506023036p:plainf:id:dnasoftwares:20200506023043p:plainf:id:dnasoftwares:20200506023047p:plainf:id:dnasoftwares:20200506023050p:plain
台車色々

流石に台車がトラックとぶつかって動かないハズはあるめえよってことで、ぶつけたら滑る物体としてRigidbodyを設定。
Kenney Game Assets 3からトラック以外の車両アセットも引っ張り出して台車と一緒に障害物として設置、チェックポイントを設定。

このあたりからゲームの情報を格納するシングルトンクラスやらArborのステートマシンやらを用意して、一繋がりのゲームにするべくスクリプトを書きます。(エア)搬入時間帯にUIも動き出したので一度MP4でキャプチャして投稿。
ついでにネタ半分の「ご注意」画面を作成しこれも投稿。

これがエアコミケ公式にRTされてバズりました。反応も悪くなくモチベーションアップ。
ゲームとしては繋がったんですが、レーダーだけではまだチェックポイントがわかりにくいような気がしたため、開始前にブリーフィングを行うように決定。
さらに走行ルート自由というシステムなので、プレイヤーごとに走行ルートの違いをアピールできるよう、ゴールまたは時間切れの後にデブリーフィングとして走行したルートを表示するのを決定。

走行ルートの記録ですが難しい事はやっておりません。一定時間間隔でプレイヤーキャラのワールド座標と向きを記録。ほか、マップにプロットしたいイベントが発生したときも時刻と座標を記録するというだけです。
あとはその記録を最初から順番にプロットすればOK。

f:id:dnasoftwares:20200506111842p:plain
開始前、チェックポイントの位置を示すマップ
f:id:dnasoftwares:20200506111839p:plain
ゴール後は走行ルートとクラッシュ箇所を表示

こういった流れの途中でタイトル画面も作成。ゲーム中UIにテイストを合わせつつさっくりと。

一般公開にあたっては何らかの形で記録を競い合う要素がないと面白くないと思っておりましたが、ランキング実装はだいぶ厳しいのでやるならツイートと思っておりました。
ちょっと調べたら画像付きツイートができるというので、imgurを使うタイプのサンプルを使ってみることに。
github.com

エディタ上ではツイートもアップロードも無事動いたのを確認できたのでWebGL書き出してみたら……なんでかツイートダイアログがでない。(ログではNullReferenceだかを吐いて止まっている)
というところで意味がわからなくなって一旦ふて寝。

2日目(5/3)

  • 身内向けテスト
  • ブリーフィング画面やゲーム画面での負荷低減策
  • ツイート機能の続き
  • 効果音とBGMの選定

とりあえずツイート機能と効果音以外は無事動いたのでDiscordで身内テストを開始。

ツイートの方は結局原因がよくわからずじまいで、別の方のパッケージに切替えることに。
github.com

流石に心配になったので一度テストシーンを作成してWebGL書き出しもチェック。こんどは大丈夫ということで本組込。
「prefab(ylib > UnityTweet > Resources > Prefabs > GO_TweetImgur)をtweetしたいscene上に置く」という説明になっていますが、ほかのGameObjectの子に置くと正常に動きません。
ちょっと色気を出してGameEnvironmentの下に置いたらだめだったよ……

統合GPUのユーザーから重いと指摘があったので負荷低減策を検討。
Profilerにかけてみたところ

  • Batchesが2000とか3000とか
  • Setpassが数百回
  • 机のMaterialが2個に分かれててバッチングが全然できてない
  • 背景にライトマップとLightProbeを使っていたのだがLightProbeが細かすぎて参照するProbeが変わりすぎてバッチングができてない

という惨憺たる状態だったので

  • Staticの設定をマジメに実施しStatic BatchingやDynamic Batchingを入れた
  • Mesh Materializerでマテリアルを統合(本当はPro Drawcall Optimizerを使うつもりだったがいつの間にかDeprecatedにされてた……)
  • 机のLightMapも焼く(Lightmapの計算時間とサイズが爆上がりするのでやりたくなかったけどやむなし)

と言う風にして一応軽量化。絵作りに使っていたPostprocessing Stackを全OFF出来た方が良かった気がするが、ひとまずは乗せるエフェクトを減らすオプションまで。

さらにブリーフィングの画面も重い。そりゃそうですよカリングのできないホール全域のリアルタイム描画ですからね……ただ、こっちは明確に軽くする手段があって、ブリーフィングのマップではオブジェクトは止まったままなのだから一度レンダーテクスチャに焼き付けてしまえばいいのです。
考え方としては「背景とステージ上の各要素(①)と、マップの記号(②)のレンダリングを分ける」「レンダリングを分けた上で、①は1回だけレンダリングしてレンダーテクスチャに保存、あとは①のレンダー結果を敷いてその上に②をレンダリング」といったところです。説明が長くなりそうなので取りあえずは概念図と結果だけ貼っておきます。

f:id:dnasoftwares:20200506144857p:plain
f:id:dnasoftwares:20200506145725p:plain

ともかくこれで劇的に軽くなりました。まだ詰めるなら机全部のメッシュ統合は当然考えるべきでしょうが上記の通り普段使いしてたPro Drawcall OptimizerがDeprecatedにされておりひとまず見送り。(さっき別のアセットでBakeを試みました。結果は巻末で)

夕方からはライトマップの設定調整。512x512のテクスチャが20枚越えというのがアレだったので2048x2048に変更してパラメタ調整して云々。最終的には2枚に収まってはくれましたが、詰め込み過ぎの影響か机のフチに謎の輝きが発生したりと、焼き直し回数が多くなってしまいました。

なんとかライトマップを倒してようやく効果音とBGMの選定を開始。細かく言うことはないんですけど、アセットストアの効果音集って実際に聞く為に一度インポートしないといけないわけですが、2GBとかのMEGA BUNDLE的なヤツだとダウンロードから展開まで数分、さらにインポートで30分とかかかって心が折れそうでした。効果音系のアセットは買ったらまず空プロジェクトなどに展開しておくのをお勧めします……。実物を聞きながら選定したいときにメチャクチャ時間無駄使いしてしまうので。

3日目(5/4) 公開まで

  • 効果音組
  • 時間ができてしまったのでステージ増加
  • 公開

unityの効果音組込で毎度困るのが

  • 特定のGameObjectに紐付かないシステム系の音の取り扱い
  • シーンを跨いで鳴って欲しい音をどこで保持するか

じゃないかと個人的に思っています。
今回はメテオフォールで一躍時の人となって今は🦍の人として有名(?)なEIKI`さんのスクリプトをベースに実装しました。
eiki.hatenablog.jp

BGMも複数鳴らしたかったので下記の通りのコードに変更。

//--------------------------------------------------------------------------------
//	- AudioManager -
//--------------------------------------------------------------------------------
//
//	オーディオ全般
//
//--------------------------------------------------------------------------------

using UnityEngine;

namespace DNASoftwares.ComikeCarryIn
{
    public enum ESeID
    {
        _None,

        Decide,
        Cancel,
        Select,
        SignalCount,
        SignalStart,
        CheckPoint,
        CheckPointLast,
        Launch,

        _Max
    }

    public enum EMusicID
    {
        _None,

        Title,
        Briefing,
        Stage0,
        Stage1,
        Stage2,
        Finish,
        TimeOver,
        Result,

        _Max
    }

    //--------------------------------------------------------------------------------
    public class AudioManager : SingletonMonoBehaviour<AudioManager>
    {
        const int cSEMax = (int) ESeID._Max;
        const int cBGMMax = (int) EMusicID._Max;
        AudioSource mBGM = null;
        AudioSource[] mSEList = new AudioSource[cSEMax];
        AudioSource[] mBGMList = new AudioSource[cBGMMax];

        [SerializeField] private bool _noBGM; // BGMオフの設定はワンタッチで出来るようにしとく (動画素材録画用)

        //----------------------------------------------------------------------------
        void Awake()
        {
            base.Awake();
            DontDestroyOnLoad(gameObject);
            var t = transform;

            for (ESeID e = ESeID._None + 1; e < ESeID._Max; e++)
            {
                mSEList[(int) e] = GetComponentSafe<AudioSource>(t, e.ToString());
            }

            for (EMusicID e = EMusicID._None + 1; e < EMusicID._Max; e++)
            {
                mBGMList[(int) e] = GetComponentSafe<AudioSource>(t, e.ToString());
            }

        }

        //----------------------------------------------------------------------------
        public void PlaySE(ESeID id)
        {
            if (mSEList[(int) id] != null) mSEList[(int) id].Play();
        }

        //----------------------------------------------------------------------------
        public void PlayBGM(EMusicID id)
        {
            if (_noBGM) return;
            if (mBGM != null && mBGM.isPlaying) mBGM.Stop(); // 先に鳴っているBGMを止める
            mBGM = mBGMList[(int) id]; // 後で止められるように今鳴らし始めたBGMは覚えておく
            if (mBGM != null) mBGM.Play();
        }

        public void StopBGM()
        {
            if (_noBGM) return;
            if (mBGM == null) return;
            mBGM.Stop();
            mBGM = null;
        }

        //----------------------------------------------------------------------------
        // コンポーネントの取得
        private Type GetComponentSafe<Type>(Transform parent, string gameobject_name)
            where Type : Component
        {
            Transform temp = parent.Find(gameobject_name);
            if (!temp) return null;
            return temp.GetComponent<Type>();
        }
    }
}

効果音の選定は散々苦労しましたが実装はあっさり。
時間ができてしまったので難易度を下げた初級コースをちょいちょいと作成。もうちょっと説明を詳しく入れるとかしてもよかったんですけどそこまでは時間がなかった。
UI周りはArborであらかたなんとかなってしまうので助かってますが、モーダル的なものを実装するときはCanvas Groupの「Intractable」とか「Block Raycast」も変更したいところ、Arbor標準のBehaviorではここに手が届かないので取り急ぎ自作。

それでもなんとか3日目の搬入時間帯には間に合わせたのでした……。(動画撮影で手間取って予定から20分遅れたけど)

このあとPVを作ろうと思い始めてまた夜更かししてしまうわけですがそこは別記事にします。もうすでに長いし。

走り終わって

久しぶりの超短距離走ゲーム開発でしたが、最初から最後までかなりモチベーション高く走りきることができました。

  • 時間が無いことがわかりきっていた上、 「コミケ会場をトラックで疾走して荷物を搬入する」という見せたい構図がはっきりしてたのでそれに関係の薄い要素を全部切り捨てる勇気が持てた
  • オレオレマネージャなどを主張しない使いやすいアセットに恵まれた
  • Twitterに投稿するとけっこう反応が貰えた

おかげさまで多くの方に遊んで頂けたようで、中にはこちらの想像を超えるプレイングで最速をたたき出すプレイヤーも……


壁サー前をバックで疾走するトラックを想像するとジワジワ来ます。

なお、


……もうちょっとだけ続くんじゃ。

おまけ:Mesh Bakerで机モデルを統合したら

f:id:dnasoftwares:20200506171242p:plainf:id:dnasoftwares:20200506171247p:plain
左:ビフォー 右:アフター

Box Colliderが統合できないためGameObjectは減らせなかった(統合前モデルのMeshRendererだけ無効にした)のにこの数字。ウーン効果てきめん……あとでビルドしてアップデートします。

フォントバカとしてmojimo-gameがどれだけ神なのかを語る

今日(2018年12月6日)の18時ごろ、フォントワークス社からインディゲーム開発者向けフォント提供プランとして「mojimo-game」が発表されました。

元々デザイナー・イラストレーター・同人誌作家向けとして提供されていた「mojimo-manga」をゲーム制作者向けにパッケージしたもの、という感じですが、とにかくかつてない低価格なのにメチャクチャ緩いライセンスとあまりに至れり尽くせりなので勢い余って紹介エントリを書くことにしました。
ちなみに自分は2012年に開催された全ゲ連という勉強会で「あたしってフォントバカ」なる出落ちみたいな題名でゲーム組み込みのフォントについて紹介しております。残念ながら当時のスライドは見つかりませんでしたが、そのときの講演のメモを公開されている方がいらっしゃったのでリンクしておきます。このメモでスライドの内容をほぼ網羅してるいかと思います。

d.hatena.ne.jp

で、何がそんなに至れり尽くせりなのか

とりあえずmojimo-gameの使用許諾を読んで欲しいわけです。
注目は以下の部分。

フォントを画像化してコンテンツ内で表示 ○
フォントを使用したゲームを日本国内/海外で販売する ○
ゲームプレイヤーの文字入力が可能な部分で使用  ○
フォントから抽出したアウトラインデータのままで使用 ○
フォントファイル自体をゲームプログラムで使用 ○

キモは後半の三行です。プレイヤーの文字入力に使えるのみならず、アウトラインデータのまま使用すること、フォントファイル自体をゲームプログラムで使用することが許諾されるのは大変貴重といえます。これが年額4800円とかマジありえん。

主立った他の定額制フォントサービスと比べてみます。

ブランド名 費用(1PC/1年のとき) フォント数 ゲームでの利用
モリサワパスポート 初年49,800円、更新48,000円 1000以上 画像化可、実行時組み替え不可、ユーザー入力不可、フォント組み込み不可
Morisawa App Tools ONE 年額72,000円 341 画像化可、ユーザー入力可、フォント組み込みも可、
ただしAPP STOREⓇ、Google PlayⓇで配信されるものに限る
ダイナフォント DynaSmartV 年額41,500円 1882 画像化可、ユーザー入力不可、フォント組み込み不可
ダイナフォント DynaSmartV ゲーム拡張オプション DynaSmartVの契約
+年額30000円
1882 画像化、組み込み、ユーザー入力可
フォントワークスLETS(ゲーム業界プラン) 入会金30,000円
年額36,000円
日本語528+欧文5723
+その他言語100ぐらい
画像化可、ユーザー入力は場合により可、組み込み不可
フォントワークスLETS
(ゲーム業界プラン拡張ライセンス)
LETS(ゲーム業界プラン)の契約
+年額100,000円
LETSと同じ 画像化、組み込み、ユーザー入力可
フォントワークス mojimo-game 年額4,800円 12 画像化、組み込み、ユーザー入力可(LETS拡張ライセンスと同じ範囲の許諾)

年額7万以上が普通という中、mojimo-gameがいかに衝撃価格なのかがよくわかると思います。
特に昨今はゲームエンジンの台頭もあってUI用にフォントを組み込むのも一般的な選択肢になりつつありますし、フォントファイルそのものの組み込みが許諾されているのは非常に強いと思います。

使えるフォントはどうか

さて、いくらライセンスが緩くてもフォントの選択肢が狭ければ意味がありません。mojimo-gameで提供されるフォントは12種、他の定額制フォントサービスと比べると1桁2桁少なく微妙に見えるかもしれません。
しかしこのライセンスで特に許諾されることがらを考えると、12種類のフォントの「選ばれし者」感が判っていただけるのではないかと思います。ざっくり各フォントの見どころをご紹介します。

UD明朝-M / DB

ユニバーサルデザインとして可読性を高めた明朝体フォントワークスといえば筑紫明朝やリュウミンですが、特にディスプレイでの表示にも向くUDに絞っての収録です。ノベルゲームのテキストとしても好適でしょう。

ニューロダン-M / B

ゴナとか新ゴとかあのあたりに近い今時のゴシック体枠FF15の日本語テキストがこのフォントらしいです。メインのフォントとして使ってもよし、B(ボールド)なら加工してテロップなどにも使えるでしょう。

UD丸ゴ_スモール-M / B

丸ゴシック枠。元々フォントワークスLETSにはスーラという丸ゴシックがありますが、より字面の大きいUDの方がゲーム向きでしょう。これと別に「UD丸ゴ_ラージ」もあるんですが、こっちは枠一杯にツメた見出し向けの書体。本文として長く読むにはUD丸ゴ_スモールがいいでしょう。ゆるめのノベル、アドベンチャーゲームのテキスト、システムメッセージなどでもよさそうです。

セザンヌ-M

昔ながらのゴシック体枠。システムメッセージなどに加え、取扱説明書などの印刷物にもよさげ。これだけ1ウェイトのみ提供ですが、ボールドが欲しかったらニューロダンを選んでもいいんじゃないかな。見出しニューロダンB、本文セザンヌでも良い感じだと思います。

ハミング-M、スキップ-M

ちょっと小洒落た感じの文字・デザイン系枠。明朝とゴシックのあいのこみたいな感じ。確かFate/Grand Orderの各種メッセージがスキップ-Bあたりを使ってたんじゃないかなと。ファンタジーもののシステムフォントに合いそう。ハミング-Mはスキップの角を丸くした感じ。ゲームの雰囲気に合わせて選びたいところ。

ドットゴシック16-M、ドット明朝16-M

ドット絵・デザイン系枠。レトロっぽいゲーム感出したいときにはいいんじゃないかなあ……ドットバイドットにしづらい印象があって殆ど使ってません( 正直ドット文字は自家製フォント工房さんとこのKHドットフォントと自家製ドットフォントシリーズが強い(バリエーションもライセンスも)ので……

コミックレゲエ-B

インパクト・デザイン系枠。12書体で明らかに異彩を放つとんがったフォント。確かタイトーのLeft4Dead(アーケード)のメインのフォントがこれだったはず。テロップでドーンと使うのには一番向いてそう。


というわけで、ゲームのシステムメッセージやノベルの本文などに使える「明朝体」「今時のゴシック」「昔ながらのゴシック」「丸ゴシック」が一揃いあることに加え、装飾したりアクセントとして使える「デザイン系」もざっくりではありますが揃っているということで、通り一遍のゲーム制作ではまず困らないんじゃなかろうかと思います。本当に絶対必要なものだけに絞った感じが「選ばれし者」感というわけです。ドットフォントはやや微妙だけど

買うべきか?

ゲーム制作者なら買って損はしません。特にUnity、UE4など「フォントを組み込めるゲームエンジン」を使っているならフル活用できるでしょう。ローレゾ表現・ドット絵が主戦場の方は若干厳しいかもしれませんがまあまあ使いどころはあると思います。

LETS契約済みの方は「LETS拡張ライセンスのフォント限定版が95%OFF」と考えましょう。フォントは大きく限定されるものの、上記の通りこのライセンスが必要になるような局面には必要十分なフォントが揃っています。

というわけで画像も貼らず勢いだけで書いてしまいました。完全に早口で推しを語るヲタク状態ですが何かの参考になれば幸い。
そのうち画像とか増やすかも。

人生で初めてGameJamに参加した話

UnityRoomで催されている「Unity1週間ゲームジャム」に参加してみたのでその記録。

Unity1週間ゲームジャムのルール

  • 月曜日0時にお題発表
  • 同じ週の日曜20時が締め切り(UnityRoomに登録)
  • WebGLで出力して提出する

いままでの開催はだいたい仕事ヤバい期間と被ってて手も付けらずという感じだったのですが、今回はちょっと時間が取れそうな雰囲気だったので一念発起しました。

……まあ、月曜の夜までお題発表されてたこと忘れてたんですけどね……

ゲームの内容

できたゲームはこちら。 - 敵弾にカスると反撃のショットが撃てる(自動照準) - 自機の中心に当たるとゲームオーバー - 敵は4種+ボスをそれなりに難易度カーブを考えつつ出す - ボス倒したらランク上げてループ

「ぎりぎり」がお題ということで弾にカスって何かするSTG寄りのものを想定。 DNAさんはWebGLとの相性が致命的に悪く、これまでに作ったUnityアプリはWebGLだと起動すらしないという残念っぷりだったので、とにかくゲーム本体はなるたけ手早く(でもそれなりにウチっぽさは出す)、WebGLで確実に動かすためテストは着実に、という方針をとりました。

ブラウザゲーなので操作は少なく、でもただの避けゲーはありきたり、ということでカスリで撃ち返し弾を放つ以外の攻撃方法がないSTG、ということに決定。

連続カスリでたーのしーって感じを出すためコンボ、連続カスリでの攻撃力強化(弾数UP)などお勤めの行き帰りで方針を決定し、帰宅して実装のスタイルで進めました。

アセットとWebGL対策

とにかくWebGLエクスポートできなければ話になりませんが、かといってUnity標準のSkyboxまんまとかArialとかも使いたくない、ということで、ともかく作りたい画面を作ったら、新しいアセットを入れたら、その他なんか変更があったらまずBuildしてブラウザで走るか確認、という地道な手順で開発を進めました。時短に使えるものはなんでも、ということで買ったきりだったアセットもかなり投入しています。

  • 背景はAllSkyの宇宙テクスチャ
  • フォントはitch.ioで買ったフォントパックから
  • ゲームの状態遷移はお馴染みArbor 3
  • 敵出現順の管理に Advanced Spawn Manager なるアセットを投入(時間あるなら自作するんですけどね……)
  • 自機の攻撃力を示す(かつ、カスリ有効範囲を示す)円ゲージはAmplify Shader Editorで自作
  • あとAAとBloomが欲しかったのでPost Processing Stackも投入

幸いWebGLとの相性BADなアセットはなく、なんとかスムーズに作業は進みました。 唯一、Trail Rendererだけはなんでか不思議なレンダリングになってしまったので泣く泣く削除。自機ショットに尾が付いたらキレイダナーとか思ってたんですが無念。 スクリプト部分に関係はありませんがBGMもアセットストアで購入したものを使っています。

WebGLで動かないとかWebGLでしか起こらないトラブルなどはなかったのは幸いでした。最後、SpawnManagerに起因するコルーチン無限ループのフリーズが発生して頭を抱えましたが、Panic buttonを導入してとりあえず無限ループを止めつつ原因究明。原因は判ったんですがこのアセットはどうも不親切さが……やっぱりSpawnerは自作した方がいいんだろうなあ…… コルーチン使いの人はPanic Button導入は必須です。マジ。「応答なし」から復活できるとか神ですよ神。

実装のスケジュールについて

各日の作業内容を掘り返してみます。

  • 6/5 …… 方針決定、UIデザインだけがっつり
  • 6/6 …… 円形ゲージの実装、自機と敵モデル作成、インポート
  • 6/7 …… ザコ1種実装、(Spawner探しに難儀)
  • 6/8 …… ザコ2種実装、Advance Spawn Managerの使用開始、タイトル・ゲームオーバー画面
  • 6/9 …… ザコ1種+ボス実装、敵配置完成
  • 6/10 …… エフェクト・効果音載せ、画面写真等撮影、投稿

見事にゲームバランス調整の時間がとれてません。ちょっと他の仕様盛りすぎたかなーというのと、前述の通り新たに導入したSpawnerでトラブル発生したりで時間を食いすぎた気がします。けっこう改造したし。 エフェクトと効果音は前に作ってたSTGのエフェクトを丸ごと移植。こういうときは過去の資産が役に立ちます。

まとめ/感想

ここまで短距離走でゲーム作ったのは10年以上前のSuica32(Gates32というMADビデオみたいな縦STGのパロ)以来な気がします。 手癖でできるからとSTGの文脈のゲームにしましたが次はもうちょい普段やらないジャンルにしたいかも、でももっと手を抜かないと1週間で終わらなくなりそうな…… 今度はもうちょっと仕様をしっかり絞りたいなと思いました。 あとすぐアーケードゲームみたいにしようとするクセも直したい。

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

まとめ

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