#無人化すべきである ~ゲームを「全自動」で展示するためのまとめ

インディ・同人ゲーム界隈も、最近はCOVID-19のせいですっかり展示会イベントが減ってしまいました。
2020年は秋のデジゲー博だけが唯一の実会場イベントという状態でしたが、2021年になってからは行政やイベント会場からイベント開催のガイドラインが出されるようになり、その範囲内でイベントをやる、という方向に進むようになってきました。(それもデルタ株の流行でまた吹き飛びそうなわけですが……)

で、久しぶりにイベントがあるとこんな事案が発生するのです。

実は2018年のBitsummitの時期にPlatineDispositifの紫雨さんがこんな記事をあげておられました。
murasame.hatenablog.jp

……歴史は繰り返すんだな、って。

ということでここ数日で再燃した「展示あるある」とその対策をツイートなどからまとめてみます。

現地の様子

今年のBitsummitはみやこめっせに会場こそありましたが、一般客はすべてストリーミングイベントの視聴やSteamでの体験版プレイなどで楽しんで貰う形として入場は不可、実会場はメディアとビジネス関係者限定ということになりました。さらに開催前に緊急事態宣言が発令されたことで、さらなる人数制限がかかったようで、ビジネスチケットの発売が早期にクローズされていました。

で、展示者もまた実会場とリモート参加の二択となっており、リモート参加の場合、事前にソフトを運営に送って現地ボランティアがセットアップし、当日もボランティアがアテンドを行っていたようです。ただ、つきっきりという感じではなく割と放置状態のマシンも多かったようです。

ということで放置対策をしていないと大変辛い展示になっていたわけですが……。

……対策をしてみてもそれを上回る放置をされてしまうのでありました。

放置の傾向と対策

もっとも困るのは「ゲーム途中での放置」です。ほったらかしにされると次のプレイヤーはチュートリアルも何も無い状態で続きをするしかなくなり、ゲームへの印象が猛烈に弱まります。そのため前のプレイヤーが諦めた場合はさっさとタイトルに戻っていてほしいのですが……

展示を前提にせず作業してると意外とソフトリセット機能とか実装しなかったり、後回しにしてたりするんですよね……。
どのみちゲーム中断とかポーズとかはあった方が良い機能なのと、中断もポーズも後にすればするほど書くべき後始末コードが増える傾向があるので、早い内に仕込んでおくのが良いと思います。
で、展示特有の機能として紫雨さんが挙げている「無操作検知でソフトリセット」も展示無人化にあっては非常に大事です。
単に30秒くらい入力なしの時間を検知したらソフトリセットしてタイトルに戻せばいいだけ、ではありますが、「イベントシーンの文字送りでの放置が検知できてなかった」「ステージクリア後の放置が略」的な事案がよくあるので、デモ系も放置して大丈夫なように仕込みましょう。無入力でもイベントがちゃんと進んで終わるのが理想。

  • ゲーム途中での放置への対策:パッド操作を常に監視しておき、一切の入力がない状態が30秒ぐらい続いたらソフトリセットをかけてタイトルに戻す

常にお客さんが並ぶほど盛況ならいいんですが、今回みたいに人がまばらだとタイトルで放置されるパターンは日常茶飯事となります。
タイトルで放置されてると動いてる状態の絵を見せらないわけで、はじめから遊ぶ気満々の人はともかく、ちょっとぶらぶらと流し見しているようなお客さんには見逃されてしまうかもしれません。
そこで入力を記録してリプレイ……なんてのは地獄しかないのでここは動画・PVを仕込むのがよいでしょう。タイトルで放置したときにPV再生するぐらいならローコストに実装できるかと思いますが、全画面でPV流してるだけだとPVだけの展示と勘違いされて遊ばれない罠もありうるので、ちゃんとゲームプレイ可能な旨の表示をするのが良いと思います。その点双腕のソルダートはタイトル画面の一部にPVを表示しっぱなしにするようになっていて良い作りでした。

  • タイトルで放置されているときの対策:PVを用意して流せるようにする。ただし、プレイアブル台であることも主張しないと誤解される。

その他、無人化のために必要なこと

後は正直紫雨さんのまとめがシッカリしてるのでみんな読んでって感じなのですがこっちでも書くと、

  • 口頭説明をせずに済むようにチュートリアルをしっかり作る。ゲーム内ヒントも多めに。アテンドしてると間違いなく喉が1日でお亡くなりになります。このご時世だと飛沫飛ばす量も減らしたいですね。
  • プレイ時間を短くする・ゲーム側で管理する。イベント開催時間5時間で、1プレイ15分だと20人しか遊べません。やはり1プレイ5~10分で。できればゲーム内通しての時間制限をつけて、時間が来たら即THANK YOU FOR PLAYINGにするぐらいの仕込みがあってもいいかも。アテンドが常駐するならキッチンタイマーがあればいいんですが。
  • (多言語対応の場合)言語選択をデモの冒頭に仕込む 外国の参加者さんに対応するのが楽になります。

是非デベロッパー諸氏にはこの辺を覚えていただいて、イベントで喉枯らしたり脚ガクガクになったりせず、健康にイベントを終えられるようにして欲しいと思います。

コナミ「カードコネクト」が実はトレカのネットプリントだったという話をダシにアーケードでのオンデマンド印刷の歴史を整理しようと試みる(きっと不完全)

先週末、突如タイムラインにオンデマンドなトレカを刷ったというツイートが流れてきました。


見た瞬間、コナミの「カードコネクト」というカードベンダーに任意の絵柄をアップロードして印刷する機能があったことを思い出しまして、おめがさんにレスしたところみんなでいろいろ調べる流れとなり

  • カードコネクトのWebサイトで任意の絵柄(表・裏・ホログラムのマスク、ラミネートのマスク)をアップロード可能(KONAMI IDログイン不要)
  • 出てきたQRコードをゲームセンターにある「カードコネクト」に読ませると印刷可能
  • QRコードは所有者制限などはなく、画像としてシェアすれば誰でも「カードコネクト」で印刷できる
  • 唯一有効期限が14日と設定されているほか、当然だが他人の権利物は登録禁止
  • 印刷代はホロなし100円、ホロありで300円(現金 or PASELI払い)

とまあ、結構至れり尽くせりの仕様で、いわゆる「ネットプリント」的な使い方ができると盛り上がりました。

で、その中でこういう「ゲームから独立したカードベンダー」の歴史的話題となりまして

なんかの縁だろうしということでちょっと「トレカのオンデマンド印刷」の歴史を調べてみることにしました。
一応タイムラインには雑に書いていましたが、改めて。

前史:印刷済みトレカを使うアーケードゲーム

リアルなトレーディングカードを使うゲーム自体はかなり前から存在しています。
ジャンルの先駆けはセガの「WORLD CLUB Champion Football」(2002年)かと思います。11枚の選手カードをフィールドに直接置いてフォーメーションを指示するなど直感的UIは革新的なもので、このシリーズは2020年のいまでも「WCCF FOOTISTA」として継続しています。
その後もセガは継続的にトレーディングカードを併用するアーケードゲームをリリースします。キッズ向け初のトレーディングカードゲームにして、長期ヒットを生んだ「甲虫王者ムシキング」(2003年1月)や、ファンタジー寄りの世界観と乱入対戦をより全面に押し出すような作りにした「アヴァロンの鍵」(2003年2月ロケテ、7月稼働)がそれぞれヒットします。ただ「アヴァロンの鍵」の方はゲーム性が大概セガセガしく(強制的に対人戦になるうえダメージを受けると直接プレイ時間が縮む。初心者狩りもあった)、当初はウケたものの長持ちはしなかったと思います。
その後、ダンジョンRPGQuest of D」(2004年)ではアイテムを「Dフォースカード」としてトレカ化する試みが行われましたが、デッキのプレイヤー間使い回し、レアカードの払い出し法則が知られたことによる一部ユーザーの張り付き連コ、及び運営の対応のまずさや一部カードの価値が極端に高まる難易度設定のセガセガしさでプレイヤー離れを引き起こしたとされています。

2005年にはWCCFのカード配置を発展させ、カードを直接動かして操作する「三国志大戦」がデビューします。(2005年3月稼働)これも長期ヒットを飛ばし、途中「戦国大戦」(2010年11月稼働、2017年2月サービス終了)として新シリーズに展開したりしながら継続しています。また、バンプレスト機動戦士ガンダム0079カードビルダー」(2004年12月)、スクウェア・エニックスの「ロード・オブ・ヴァーミリオン」(2008年6月~2019年8月サービス終了)など、似たUIを持ついくつかのフォロワーも登場しました。

いずれのゲームでもカードを巡ってのトラブルは少なからず存在しており、「レア抜き」が疑われる事例や特定カードを目当てとした「掘り」などもあったといいます。

オンデマンド印刷へ

ゲームセンターでのオンデマンド印刷といえばアトラスの「プリント倶楽部」(1995年)で既に行われていたわけですが、印画紙印刷をトレーディングカードに応用するアイデアが実際に世に出たのは2012年コナミ「モンスター烈伝 オレカバトル」が最初のようです(2012年3月)。QRコードを印刷し、それを筐体で読むことでカード上のキャラをゲームに登場させられるという仕組みでした。この時点で既にカードとプレイヤーの紐付けがある程度行われており、他人が印刷したカードは「かりモン(借り物+モンスター)」と呼ばれ、使用制限が発生するようになっていました。カード自体の印刷品質は既に十分高かったようですが、紙質はちょっと薄めで、また元がロール紙なので払い出しの時点でちょっと反っていました。後、2015年にはSOUND VOLTEX IIIに「SOUND VOLTEX GENERATOR -REAL MODEL-」というカードプリンタが追加され(2015年3月11日)、ゲーム内の楽曲ジャケットやキャラカードが印刷できるようになりました。中には「PUR」というレアリティ付きのものがあり、これを引き当てるとゲーム内のナビゲーター「ネメシスクルー」を変更できるという趣向がありました。なお、SDVXのカード用紙は当初オレカバトルと共有しており、裏面の柄がオレカバトルのままのカードが出ていたそうです。(2016年頭からKONAMIロゴのみの用紙が出回り始めた模様)

一方セガも同じ2015年6月、既に稼働中だった「初音ミクProject DIVA Arcade Future Tone」に「フォトスタジオ」として印刷機能を追加。こちらは台紙が「シール」になっており、レイアウトを変えることで複数サイズのシールを一気に印刷できるようになっているそうです。PV鑑賞中に複数枚スクリーンショットを取ることができたので、好きな写真を複数並べてまとめてシール化できるようになっています。また、ゲーム内のアイコンやロゴを用いたデコレーションも可能とのこと。このあたりには「プリント倶楽部」の息吹が感じられます。

セガのオンデマンドトレカ印刷としては2016年の「艦これアーケード」(2016年4月稼働)がその発端でしょうか。印刷のベースとなるカード自体がICカードになっており、反りの無い高品質な出力、さらにホロの印刷にはじめて対応しました。また他人のカードであっても使用制限が緩いようで、カードショップでの中古市場ができあがり、いわゆるSSR的立ち位置である「中破ホロ」(キャラ絵が中破絵のカード)はそれはもう高値での取引がされているとかなんとか。

同じ2016年12月には「三国志大戦」の第2期が稼働開始。こちらのオンデマンドは両面印刷に対応しており、これまでの三国志大戦同様に片面にイラスト、もう片面で各種情報の記載がなされるようになっています。こちらの使用制限は比較的厳格で、筐体でのトレード機能の範囲でしかトレードもできず不評を買っていたようです。後に改善され、今はトレードの健全さと自由度がある程度両立されているようです。また、ICチップでは無く特殊インクによるコード印刷が使われているらしく、どうしてもカードを擦る事の多い三国志大戦においてはカードの耐久性が問題視されています。対処としてはスリーブを装着するしかないのですが、万が一に備えカードの再印刷が機能として備え付けられています。

独立ベンダー化

セガのカードメイカ

2018年7月に「maimai」「CHUNITHM」に続くセガ音ゲー「オンゲキ」が稼働開始。同時に「カードメイカー」という独立したカード販売機が稼働を開始します。
カードメイカーの当時の機能としては、

  • オンゲキで入手したデジタルカードを実際に印刷して入手する「カードプリント」
  • オンゲキで使用できるカードをガチャって印刷する「ガチャプリント」

の2つで、あくまでもオンゲキの補助という立ち位置のものでした。
オンゲキはゲーム中に入手したカードでデッキを構築し、そのデッキの「攻撃力」とプレイ精度で音ゲーのスコアが決まるシステムです。(デッキに関係ない精度のみで決まるスコアも別途記録されます)
で、ゲーム内で入手したカードをカードメイカーで「印刷」するとレベル上限が解放されデッキ強化が促進されるというシステムになっています。(オンゲキPLUS以後はゲーム内アイテムでもレベル解放可能)
ところが、初代オンゲキは本当にカードを印刷する以外のカード強化の手段がなく、カードメイカーに100円を払わない限りデッキ強化がすぐ頭打ちして解禁が止まるため、補助的な立ち位置に見えて実際は使用必須同然の作りになっていました。
「ガチャプリント」も初代オンゲキでは曲者で、正直ガチャ引かないと後半の解禁、というかTitaniaで勝つ戦力作るのが大変という事がありまして……(実際はゲーム中で入手できるカードやイベントの報酬を集めればある程度戦力はまとまるが、イベントを走るコストやカード入手までのプレイ曲数考えたらガチャに走った方がマシという可能性があった)初代オンゲキはまあ大概セガセガしいゲームでした。「オンゲキPLUS」「~SUMMER」と急激に改善されて今では相当楽になってます。

後、2018年10月からは「CHUNITHM AMAZON」がカードメイカーの対応機種に追加されました。こちらはガチャがメインとなっており、

  • ゲーム本編で入手不可になったキャラをガチャで引く
  • あるいはガチャ限定のレアキャラを引き当てる

という趣向になっています。カードを引いた時点でaimeアカウントで関連付くCHUNITHMのゲームデータに保存され、QRの読み取り等もなくゲームで使用可能になります。
もう一つ特徴的なのは印刷されたカード自体を使って遊ぶ「チュウニズム大戦」という遊び方の提案がなされているところにあります。

2019年7月には「maimai でらっくす」が稼働しカードメイカーと連動を開始。こちらでは解禁促進や上位難易度のプレイ制限撤廃などの効果がある「でらっくすパス」を購入する形になっており、カードの印刷=パスの購入となります。特にガチャ要素はありません。

コナミのカードコネクト

で、コナミの独立型カード販売機である「カードコネクト」ですが、2019年12月23日に稼働開始します。
カードメイカーの後追いという形での稼働ではありましたが、両面をオンデマンド印刷可能とスペックもカードメイカーより上、ついでに対応機種数も圧倒的で

と、あらかたの現行コナミコンテンツは大概対応しています。
ただ、対応具合はまちまち、というかそもそもゲームと連携していないヤツも多く、ポップンDDRボンバーガール、エルドラクラウンやキャラコンテンツの場合は事実上ただのカードダスです。
SOUND VOLTEXのガチャについてはゲーム機でのオンデマンド同様「PUR」を引けばネメシスクルーが解禁されます。それ以外の場合はアピールカードが解禁されます。

珍しいのは「プロフィールリント」と「メモリアルプリント」で、ゲーム機のプレイデータを元に名刺のようにプロフィールを印刷したり、なんらかのメモリアルを達成した記念のカードを印刷することができます。
プロフィールの方は名刺サイズなこともあるので交流会での名刺交換に使うという手もありそうです。(量産するには高いけど)
メモリアルについてはまだ「beatmaniaIIDXの段位突破記念」と「麻雀格闘倶楽部のプロ本人マッチング記念(またはプロ勝利記念)」しかありませんが、一種のリアル記念品になるので面白いと思います。

そんなわけで稼働当初は新機軸はあれど、セガのカードメイカーの後追いという印象は拭えず微妙なところだったのですが、2020年3月25日になって突然「オリジナルプリント」がリリースされ、「トレカのネットプリント」という新たな役割が立ち上がったのでした。

まとめ

2002年ぐらいから始まったリアルトレカを用いるアーケードデジタルゲームの流れはロングランシリーズを複数生む大ヒットになりましたが、リアルカードであるがゆえにゲームセンターでのトラブルが絶えず、カードのオンデマンド化は強力なトラブル解決策となりました。
2012年に業界初のオンデマンド印刷採用TCGが登場。最初はフォトプリンタをベースとしていたようですが、後にトレカサイズの専用用紙や、ICチップを埋め込んだカードが作れるようになり、急激にクォリティも向上、オンデマンド印刷のカードゲームも一般化しました。
音ゲーとリアルトレーディングカードの融合を試みた「オンゲキ」&「カードメイカー」はカード生成が必須すぎる仕様が不評で軌道修正。そこに後追いとなったコナミはゲームのプロフィールやメモリアル要素を前面に出した「カードコネクト」を展開、さらにユーザーによる完全データ入稿さえ受け入れる「オリジナルプリント」を開始して大きく差別化しています。BEMANIコナミ製品のあるゲーセンなら高確率でカードコネクトがあるという、意外と高い普及度も見逃せない可能性があります。
今後はどうなるんでしょうか、ベンダーが独立して存在することで、ゲーム筐体への追加装置を入れずともカード絡みの追加コンテンツを用意出来るわけですが、まあ、最後にはガチャなんですかねやっぱし……ただメモリアルプリントなどガチャだけでない使い方が模索されてるのはいいことだと思います。

『エア搬入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」としておけばとりあえず何事もない

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