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

最近は麻雀のバージョンアップを続けておりましたが、この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だけ無効にした)のにこの数字。ウーン効果てきめん……あとでビルドしてアップデートします。