Help us understand the problem. What is going on with this article?

【XRKaigi連動】1ヶ月でVRリズムゲームを作る【xRTech連動】

おことわり

このQiitaは12月3,4日に開催された国内最大級のVR/AR/MRカンファレンス XRKaigi
コミュニティオーガナイズドセッション xR Tech Tokyo ごんびぃー講演と連動した記事になります。
XRKaigiに参加した人は記憶を、
参加していない人はYoutubeで公開されている登壇動画と資料を見ながらこの記事を読んでいただければ
理解が深まるかと思われます。

はじめに

すべては10月30日のこのツイートから。

株式会社GONBEEE-projectはリズムゲームの製作を専門とするB2Bのツール屋です。
代表作はリズムゲーム制作支援ツールRefrain Coreですが、今回はあまり関係ありません。

この記事では題名どおり、1ヶ月程度でそこそこ動くVRリズムゲームの作り方を紹介します。
実は(VRに限らず)リズムゲームの作り方はあまり知らないという声をちらほら聞くので、
リズムゲームの教科書的な記事にしていけたらと思います。
想定する読者層は
脱Unity初心者レベルの人(ダニング・クルーガー)
ゴーグル買ったはいいが何を作ればいいか迷う人
リズムゲームを作りたいが何から始めたらいいか分からない人あたりです。

資料リンク

XRKaigiでの登壇スライド
GitHubリポジトリ Beat Knuckle
資料フォルダのGoogleドライブ
モデル製作依頼時の発注資料
オペレーター台本
オペレーター用サンプルボイス
ロゴ製作依頼時の発注資料

リズムゲームの設計思想【初級】
リズムゲームの設計思想【中級】

今回作ったもの

今回製作したゲームは
ボクシングをテーマにしたVRリズムフィットネス

Beat Knuckle ビートナックル

です。
beat.jpg

設計思想的には
Nintendo Switchでリリースされている Fit Boxing のオマージュで
空間奥から飛んでくるボクシングミットを打ち返そうというデザインのゲームです。

VRリズムゲームとしてはオーソドックスな表現技法である、
空間奥からノーツが飛んできて手で叩き返すタイプのゲームになります。

動画を見れば分かる通り、
全編通してオペレータにつきっきりで誘導をしてもらいつつ有酸素運動をしていこうという思想のゲームです。

今回の記事ではBeat Knuckleを
完全に0からツイートの動画を撮影するまでの全行程を紹介します。
この記事をなぞればVRリズムゲームを作れる状態になっている(かも)ことでしょう。

Step.1 "ジャスト"を固めよう

開幕異論ありかもしれませんが、
弊社ではリズムゲーム製作時に
グラフィックや最終的なビジュアルよりも先に
どのような"ジャスト"の表現にするか
を固めてから設計に入ります。

ジャストとは?

"ジャスト"とはリズムゲームにおいて
振ったり触れたりプレイヤーがアクションを起こすべきその瞬間のこと
と定義しています。
"ジャスト"にどんな表現があるかは別の記事にまとめてあるので、
そちらを参照してください。

何故真っ先に"ジャスト"から固めるかというと、(あくまでも弊社の思想ですが)
リズムゲームの体験の本質部分は"ジャスト"の表現ですべてが決まる(と考えている)ためです。
ビジュアルももちろん大事ですが、プレイヤーの行う操作は"ジャスト"の表現で8割方決まります。
要するに"グラを剥いだら中身一緒じゃない?"ということです。

例えばBeat Knuckleのような、空間奥から飛んできて手で叩き落とすゲームは
プレイヤーの動作は(完全ではありませんが)似てきます。
プレイヤーにどのような動き(操作)をさせたいかを考える設計順序は悪くないかなという認識です。

エンジニア向き

プレイヤーの操作が似るということはゲームの作り方、(ここではScriptなど具体的な組み方を指します)
が近くなることでしょう。
グラフィックは違うが本質的な動作は一緒、ということは
エンジニア目線では、経験があれば似たようなゲームをもう一度作るということになり
開発速度が圧倒的に加速することかと思われます。

今回Beat Knuckleがこの"ジャスト"表現を選択したのは
プレイしやすさもありますが、ごんびぃーに開発経験があったというのも大きな要因です。
実際Beat Knuckle最初期のモックアップは1時間やそこらで完成しました。
動きが見えるということは大きなアドバンテージなので、
モック製作部分を加速できるというのは十分考慮に値すると思います。

Step.2 適当に組んでみる

適当に組んでる当時はQiita書くことは想定してなかったので、動画が残っていません。
GitHubのコミットログからブランチを切り直してサルベージしてきたものになります。

このパートはStep.3に向けての準備です。
そのため見た目はボロクソでも大丈夫ですが、
どういう動きをするか等の最終的なビジョンはある程度ここで固めましょう。

先程書いた1時間やそこらで完成したモックアップになります。

見ての通りひどい出来でございます。
ノーツはCylinder、サンドバッグはCapsule、背景はPlaneにメートルグリッド、
そもそもOculusIntegrationすら投入しておらず、そこにあるのはMain Cameraのみ。

曲もなければエフェクトもない、唯一あるのは”ジャスト”だけ。
実装の考え方としては、
ノーツ生成時の初期化関数だけをとにかく爆速で書いて、
Spaceキーでノーツ生成関数を叩くということをやっています。
Step.1で記したとおり弊社作品はまず動きから作られ、
開発後期はグラフィックの向上に全戦力を注ぎ込みます。

Step.3 苦手分野は頼ろう

完全自給自足型のクリエイターにとっては嫌なパートと思いますのでスキップして構いません。
その気持ちはわかります。

弊社株ごんでは外注を是とします。
加えて弊社から発注する案件に関してはすべての納品物に対して
権利は受注側保持、実績公開フルオープン(タイミングは指定)、制作裏話など公開OKを徹底しています。
非常に不甲斐ないところではありますが、
少なくともごんびぃーは全部こなす万能フルスタックマンではありません。
私にできるのはリズムゲームのシステムを作ることだけです。
そのため苦手分野はどんどん他人を頼ります。
そうすることで自分は得意とするリズムゲーム部分に注力でき、
最終的な完成形のクオリティを底上げすることができます。
悔しいですが、制作物がもっとよくなるのであれば手段は選びません。悔しいですが。
また、自分の発注が実績となってまた別の案件が決まれば受注側にとってもメリットでしょう。
みんなで成長すればいいんです。

今回外注を投げたのは
・3Dモデル全て
・ボイスオペレーター
・ゲームロゴ
の3つです。
発注時の資料なども含めて順番に紹介していきます。
(受注側に発注資料公開OKの確認を得ています)

3Dモデル

Blender挫折歴3回のごんびぃーは完全にモデル自作を諦めています。
自分で作っても綺麗なものになる訳ないので、早々に外注を決定し、
Beat Knuckle制作開始からわずか2日(くらい)で発注しました。

今回3Dモデル製作を依頼したのは
XRコンテンツ・クリエイトチーム「Mark-on」です。
こちらのツイートから話が動き出しました。

Mark-onの代表であるJackMasaki氏から頂いたリプから案件が動き出し、
翌日には見積もりをお願いしました。
今回のQiitaを書くにあたって、Jack氏から許可が出ているので
実際に依頼時に送った発注書・要望書のPDFを公開いたします。

Jackくんモデル素材発注依頼書

(Jack氏の許可は出てるけど、PDF内に出てる参考資料とか掲載大丈夫なのか?
問題ありそうだったら即削除します)

このPDFを読めば分かる通り、
様々な手段で自分の理想?ビジョン?を伝えようとしています。
これのお陰でかなり意思疎通がスムーズに進み、
依頼→サンプル確認→修正要望追加→本制作と、すべて最初に予定した日程で実行できました。

3Dモデルの持つ力は非常に強力で、
見た目部分はゲーム全体の評価を左右するといっても過言ではありません。
この場を借りて改めてお礼を、ありがとうございました。

ボイスオペレーター

当然ながらごんびぃーは配信者でも実況者でもVtuberでもなく、
ボイス担当なんてやったことありません。
ましてやゲームに声を吹き込むとか、そもそも自分の声の録音聞いてモヤモヤしてるような人間ですから、、、

今回ゲーム内ボイスオペレーションを依頼したのは
ましろ星からやってきた宇宙天使さぶれーぬさんです。
さぶれーぬさんには昔から弊社の(当時は法人ではないですが)コンテンツStar Bullet Refrainに
チュートリアルボイス担当で出演して頂いたり、なにかとお仕事繋がりがありました。
さぶれーぬさんの声の良さや真剣に取り組む姿勢は当時から知っており、
今回もまた発注いたしました。

同一の人物に対して依頼することにはいくつか利点があると分析しています。
まず1つは発注のしやすさです。
一度案件を依頼してある場合「もっかいあれやって!」で何を依頼しようとしているかが通じます。
どんなお仕事内容なのか、どんな作業が発生するかを説明しなくて済むというのは
案件発注の心理的障壁を下げる上に、発注の最序盤のコストを削減できます。
ゲーム制作は最序盤こそ重要であり、エンジニアは実装に注力したいことだろうと思います。

2つ目はお互いの癖を理解しているかどうかです。
案件を依頼した経験があり、何度もやり取りを通してどのようなものを求めているかをお互いに理解していると
依頼内容がスムーズに伝達できます。
ただ、当然ながら
スムーズに伝達できるからと言って詳細を省いた資料を作るのは下の下です。
考えはすべて明文化しましょう。

すべて明文化した上での話ですが、
「この資料の書き方からするに、この部分はこう読むんだな」みたいな雰囲気が、多分伝わってると思います。

こちらも実際に使用した発注内容の台本などを公開します。

Beat Knuckle台本
参考用動画
ごんびぃー版サンプルボイス

このクソみたいなサンプルボイスからよくあそこまで仕上げてくれました。
今回のゲームは全編通して喋り続けで、ボイスの有無で遊び心地が変わるだろうと考えていました。
やはりボイスが吹き込まれて音が生まれるとゲームが突然生き生きとする感じがします。
音周りの部分はゲーム全体の評価を左右するといっても過言ではありません。
この場を借りて改めてお礼を、ありがとうございました。

天丼

ゲームロゴ

フォトショなんて持ってませんし、デザイン構築スキルが無いどころか
うまいデザインを考える力すらありません。

今回ロゴ制作を依頼したのは
PrismPlane代表 柚木勇魚さんです。
勇魚さんもさぶれーぬさんと同じく、昔から弊社のロゴ周りをお願いしていました。
今回が3回目4つ目のロゴ依頼になります。

前述の通り、同じ人物に依頼をするメリットを存分に活かしています。
またそれらに加え、弊社製品・作品のロゴは全て一つの世界観を共有しています。
複数人に依頼するでもいいとは思いますが、その場合世界観の統一は難しいのではないでしょうか。

弊社(当時は法人ではない)が初めて発表した作品は
今からもう2年前にもなる2017年、xRTechTokyo#8にて初お披露目となった
VRリズムシューティング Star Bullet Refrainでした。
弊社依頼の初めてのロゴはこちらです。
昔からごんびぃーを知る人間はよく見たことがあるでしょう。

2017年当時から今後発表する全ての作品を直線で繋いでやろうという構想がありました。
今の所のその構想通りに全て進んでおり、以降発注したロゴ全て
横に並べると一直線に繋がるように作られています。

弊社の代表製品、リズムゲーム制作支援ツールRefrain Coreから始まるレーザーが
SBR,Beat Knuckleを貫き、株式会社GONBEEE-projectのロゴにたどり着きます。
こういった統一デザインは恐らく複数人に依頼しては難しくなるでしょう。
勇魚さんに発注するときは「例によって貫きます。」と伝えています。

今作の発注書でも貫いてくださいとお願いしました。
こちらも同じく発注書掲載OK許可が出ていますので紹介いたします。

勇魚さん発注書

ロゴはゲームの顔です。
SteamやOculusなどのストアで購入者が真っ先に目にするものはスクリーンショットではなく、
製品一覧に並ぶゲームロゴです。
そのためロゴでインパクトを与えなければ購入者に見てもらうことすら叶わないでしょう。
Beat Knuckleロゴではその点強烈なインパクト(物理)を与えています。
お陰様でTwitterに投稿した動画もサムネイルがキリッと決まり、
何というタイトルのゲームなのか完璧に伝わったと思います。
ロゴは文字通りゲーム全体の、どころかゲームに辿り着けるかどうかの大事なパートであると言っても過言ではありません。
この場を借りて改めてお礼を、ありがとうございました。

天丼大盛り

Step.4 システムを組もう

一通り発注を終えたらシステム全体を構築していきます。
ポエムは終わりです、エンジニア!出番よ!

弊社ではまずざっくり"ジャスト"を作って確認すると前述しました。
このパートでは具体的に実装の順番と中身のコードを紹介していきます。
全コードはGitHubに置いてあるのでそちらも参照してください。
Beat Knuckle

1. ノーツ初期化関数

まず初めに実装するのはノーツ生成時に発火される初期化関数です。
ノーツがどのような動きをするかはここで作ってしまうので、
"ジャスト"の表現を確定させておく必要があります。
今回ノーツのアニメーションには我らがDOTweenを使用しています。

Punch_Item.cs
前略
    /// <summary>
    /// ノーツの初期化とアニメーション
    /// </summary>
    /// <param name="_offset">生成からジャストまでの時間</param>
    /// <param name="_lane">どのレーンに流すか</param>
    public void Initialize(float _offset, int _lane)
    {
        //Runnerから送られてきたデータを保持しておく
        Offset = _offset;
        LaneNum = _lane;

        /* アニメーション表現
         * 初期化からジャストまでの座標をOffsetジャストでDOPathする
         * その後消滅するまでの距離を計算してもう一度DOPathする 
         * その分の時間はマーカーとOffsetから算出
         */
        Vector3[] path = MarkerHolder.Instance.GetPath(LaneNum);
        Vector3[] afterPath = MarkerHolder.Instance.GetAfterPath();
        float afterOffset = MarkerHolder.Instance.GetAfterOffset(LaneNum);

        //ふわっと生成するためにPrefab状態でのスケールを保持しておく
        var seq = DOTween.Sequence();
        var scale = transform.localScale;

        //ノーツを生成場所に転送とスケールを0にして消しておく
        transform.position = MarkerHolder.Instance.GetStartPos(LaneNum);
        transform.localScale = Vector3.zero;

        //スケールを戻して出現
        seq.Join(
            transform.DOScale(
                scale,
                Offset * 0.1f)
                    .SetEase(StatusHolder.Instance.ScaleEase));

        //出現と同時に移動を開始
        seq.Join(
            transform.DOLocalPath(
                path,
                Offset,
                PathType.CubicBezier)
                    .SetLookAt(0.05f, Vector3.forward)
                    .SetEase(StatusHolder.Instance.MoveEase));

        //ジャストまで移動した後少しオーバーランさせる
        seq.Append(
            transform.DOLocalPath(
                afterPath,
                afterOffset,
                PathType.CubicBezier)
                    .SetLookAt(0.05f, Vector3.forward)
                    .SetEase(Ease.Linear));

        //オーバーランまで完了したのに生きてる場合はミス判定
        seq.OnComplete(() =>
        {
            OverRun();
        });

        //シークエンスを再生
        seq.Play();
    }

初期化関数に渡しているデータはアニメーションに掛ける秒数floatと、
どちらのレーン(今回は左右パンチどちらか) のintです。
これらの情報からゴニョゴニョいたしまして、
transform.DOPath()を発火しています。
DOPathに関する情報はここでは詳しく記しません。各自で調べてください。
VRリズムゲームではノーツが完全なる直線で動いてしまうのはあまりよろしくなく、
せっかくのVRなのに空間を活かしきれないデザインになりやすいです。
そのためDOPathでベジェ曲線にして立体感と疾走感を与えています。

2. テスト用擬似ノーツ生成

よくC#の開発でまずTestを書けと言われますが、ここでいうテストは少し違い
譜面データを作るのが大変なので、先にノーツ生成関数だけを叩くテストコードを書いて
ノーツがどのような動きをするかを確認するというのが主眼となります。

TestRunner.cs
public class TestRunner : MonoBehaviour
{
    public GameObject[] punch;

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            int r = Random.Range(0, 2);

            GameObject g = Instantiate(punch[r]);
            g.GetComponent<Punch_Item>().Initialize(TimeController.Instance.Offset, r);
        }
    }
}

本当にシンプルでRandomだしGetKeyDownだし、見ての通り本当に簡易テストコードです。
動きから作るという設計順序なので、どのような動きをするかを真っ先に確認する必要があります。
しかし譜面データを設計するには時間がかかるので、苦肉の策でノーツの動作テストをしています。
そのためTestRunnerとノーツ初期化は同時並行で進めます。

3. ゲームの手応え管理

こちらも動きに関するところなので大事ですが、遊び心地を操作するパートなので後回しでも構いません。
ここまででMarkerHolder.InstanceやTimeController.Instanceというコードがありましたがその説明をします。

まずMarkerHolderから

MarkerHolder.cs
/// <summary>
/// レーン別にマーカーを保持するクラス
/// </summary>
[System.Serializable]
public class LanePath
{
    public Transform Start, StartOut, EndIn, End;
}

public class MarkerHolder : MonoBehaviour
{
    private static MarkerHolder instance;
    public static MarkerHolder Instance
    {
        get
        {
            if (instance == null) instance = FindObjectOfType<MarkerHolder>();
            return instance;
        }
    }

    //ノーツが飛ぶレーン情報、DOPath用
    [SerializeField]
    private LanePath[] _lanes;

    //手元通過後消滅までのレーン情報
    [SerializeField]
    private Transform _endOut, _afterIn, _after;


こちらはDOPath用のマーカー情報を保持しておくためのクラスです。
レーン情報管理は十分1機能であるので他マネージャー機能からは分離して作っています。
Transformでマーカーを管理しているのでデザイナーが簡単にゲームバランスを調整でき、
Unityの想定する役割分担ができている状態かと思われます(が、全部ごんびぃー作なのであんま関係ない)。
長いので省略していますが、このコードの後にDOPath用に座標を配列に詰め直す関数も書いてあります。

次にStatusHolderです

StatusHolder.cs
    [Header("Tag")]
    [SerializeField]
    private string _noteTag;
    public string NoteTag { get { return _noteTag; } }

    [SerializeField]
    private string _handTag;
    public string HandTag { get { return _handTag; } }

    [Header("Ease")]
    [SerializeField]
    private Ease _moveEase;
    public Ease MoveEase { get { return _moveEase; } }

    [SerializeField]
    private Ease _scaleEase;
    public Ease ScaleEase { get { return _scaleEase; } }

    [Header("Clap")]
    [SerializeField]
    private float _clapSpeed;
    public float ClapSpeed { get { return _clapSpeed; } }

    [Header("Debug")]
    [SerializeField]
    private bool _isAuto;
    public bool IsAuto { get { return _isAuto; } }

前略ですがシングルトンです。
(正確にはシングルトンじゃないです、簡易シングルトンというか、アクセスできればいいじゃん論です)
ここでは
・当たり判定確認用のTag
・ノーツの移動用イージング
・後述する拍手システムの速度
・オートプレイの切り替え
を管理しています。
オートプレイは特に重要で、リズムゲーム開発には必須の機能だと考えます。
テストプレイ毎に全部プレイする訳には行きません。面倒臭すぎる。
これを実装したおかげで、譜面の難易度調整もスムーズにいけました。

最後にTimeControllerです。株ごんではこれが一番重要だと捉えています。

TimeController.cs
public class TimeController : MonoBehaviour
{
    private static TimeController instance;
    public static TimeController Instance
    {
        get
        {
            if (instance == null) instance = FindObjectOfType<TimeController>();
            return instance;
        }
    }

    /* ノーツの飛ぶ時間など
     * 音ゲーのプレイ感触に関する時間系を
     * 一箇所で管理する用のクラス
     */

    [SerializeField]
    private float _offset;
    public float Offset { get { return _offset; } }
}

スクリプトとしては一番短いですが、ここでは難易度に直結するノーツの飛来時間を管理しています。
ノーツの疾走感、ひいてはゲーム全体の爽快感を管理するクラスなので最も大切です(の割には雑実装ですね)。
Offsetをいじることでノーツ全体のアニメーション速度が変わり、
ゆっくり低難易度から高速高難易度までこの一つのパラメータで操作できます。
中身的にはStatusHolderあたりに合体してもよさそうですが、
もしゲームがスケールして他の種類のノーツが増えた場合、そのノーツ用の時間も管理しなければならないので
時間専用があっても悪くないと思います。

ゲームの根幹を管理するマネージャー系クラスはこんな感じでした。

Step.5 曲を決めよう

一通り動くところまで中身のシステムを組めたら次は楽曲部分を作り始めましょう。
今回Beat Knuckleで使用している音源はフリー素材音源を配布している煉獄庭園さん
(ドメインが死んでてサイトアクセスできないんだが、、、)
から使わせていただきました。
理想は手前で曲作りですが、まだその域には達していません。
モデルやロゴは諦めてますが、楽曲は自分で作りたいなぁ、、、将来がんばる、、、

初期設計当時はアコースティックギターがブンツク響くような楽曲を考えていましたが、
尺不足と世界観の不一致で途中で楽曲を切り替えました。
最終的にゲームをリリースする際は世界観とか関係なく大量に組み込むつもりですが、
今回は展示版ということで1曲で進めました。

ぶっちゃけ曲周りはあまり書けることはありません。
強いて言うなら権利周りが非常に厳しいエリアなので、
公開する際などは権利関係の裏をちゃんと取っておくことをおすすめします。

リズムゲーム制作の観点から言える選曲へのアドバイスとしては、
「譜面設計しやすい曲を探そう」と「BPM早めの曲を探そう」です。
譜面設計しやすいというのは
・BPMが途中で変動しない
・7分などの変調子でない
・低音がちゃんと鳴っている
辺りでしょうか。
BPMが変動しないというのは言わずもがな、
途中で変動するものはプレイも難しく、制作も難しくなります。
変調子でないというのも同上で、双方が難しくなるので
モックアップ用としてはあまり適さないです。
低音が鳴っているのはあまり重要ではありませんが、
リズムが取りやすい曲はプレイしやすい曲であることが多いです。
(近年の難易度インフレを考えるとそうとも言えないけど…)

BPM早めの曲というのは単純に、早い曲は楽しいからです。
よくあるJ-popとかって案外遅くてリズムゲームとしてはノリにくかったりしますね。

Step.6 譜面を作ろう(地獄)

XRKaigiでの登壇でも言いましたが、
譜面データ構築は本当に過酷です。
綺麗なタイミングの譜面データを構築するには専用の譜面エディタが必要で、
BPMベースでの自動タイミング計算がほぼ必須です。
弊社株ごんにはRefrain Coreという最強ツールキットがありますが、
今回の作品はオープンソースで公開することが事前に決まっており、
弊社製品はまだごんびぃーの、株ごんの飯の種ですのでオープンにはできません。
そのため今回はノーツデータも譜面データも1から設計し、
譜面はすべて音源の波形を見つつExcelに手打ちしました。
当然ですが弊社にはノーツデータも譜面データも構築経験があるので、
この部分にはあまり時間を取られませんでした。
もし知見0からだとこのステップで一年位足踏みしたことでしょう。
(余談ですが、Refrain Coreが正式に世に出たのは開発開始から1年程経ってからでした。)

1. ゲーム側ノーツデータ

譜面データをロードして何かしらの配列にノーツ情報を保存します。
その情報が下記のクラスです。

Note.cs
/// <summary>
/// 譜面データからロードしてきたノーツ情報用のクラス
/// 1インスタンスで1ノーツ
/// </summary>
public class Note
{
    public float Timing { get; set; }
    public int Lane { get; set; }
}

今回は表現幅が狭く、TimingとLaneさえあれば動きました。
このクラスを1ノーツのインスタンスとして扱い、
このクラスの配列をノーツリスト(配列)としてゲーム内で走らせています。

2. 譜面側ノーツデータ

ゲーム側のノーツ情報に合うように譜面データの配置順を決めます。
今回はオープンソースで公開予定だったので、
独自の譜面形式は避け、単純なCSVでの構築を選択しました。
(Refrain Coreでは.rcbファイルという専用形式を使います。中身的にはほぼxmlです。)

nemuri.csv
8.57143,0
12,0
12.4286,0
12.8571,0
13.2857,0
20.5714,1
27.4286,1
41.1429,0
42,0
42.8571,1

これが今回実際に使用した譜面csvの切り抜きです。
1行を1ノーツとして、列によってデータを分けています。
第一要素がfloatでTiming情報、
第二要素がintでレーン番号(今回は左右の打ち分け)です。
豊かなノーツ表現(ホールドとか連打とか)を実装したい場合はこの情報量では足りませんが、
今回のBeat Knuckleはとにかくシンプルに済ませたので、これで十分でした。

たとえば

連打ノーツなどを実装する場合、ごんびぃーなら
第三要素に連打制限時間のfloat、
第四要素に目標連打数のintを書いたりすると思います。

3. 波形

実際に譜面データを作る際は波形とExcelを並べて配置して、
タイミングを目視してExcelに書き込んでいきます。
あまりにも地獄過ぎるので参考資料とか出しませんが、
波形を見てノーツを配置するときのコツとしては
周波数スペクトログラムを対数表示にして、低音ビートが強く鳴った瞬間をジャスト
として見ると結構キレイな譜面が作れます。

SnapCrab_2019-12-07_01-03-47_No-0000.png

白くなっているところが音の鳴っているところです。
低音ほど下に、高音ほど上に描画されます。
高音は基本的にリズムを刻む音にはなりにくいので、下さえ見てればなんとかなります。
ぶっちゃけ慣れです。
ここに再生カーソルを合わせると再生時間がわかるので、
それをExcelに記入していきましょう。
かなりマンパワー解決の暴力的実装ですが、人力なら手間をかければかけるほどいい譜面になります。
諦めずに進めましょう。

Step.7 エフェクトを盛ろう

Step.2で動画を載せましたが、開発最初期はとにかく酷い見た目でエフェクトも市販品のカスタムだったりしました。
ですが今回はオープンソースということで権利裏が取れているもののみを公開というルールを課して作りました。
もちろん無料公開のAssetsを組み込んでもよかったんですが、なんか嫌でした(Step.3参照)。
エフェクトに関してはあまり知見はありませんが、テクスチャ自作してShurikenのパラメータも自分で組みました。
リポジトリにテクスチャを作ったときのプロジェクトデータが入っているので詳しくはそちらを参照してください。

作成したエフェクトは
・左右ノーツが飛来する際のトレイル(オレンジ、青)
・ジャブに成功したときのパンチトレイル(オレンジ)
・ストレートに成功したときのビーム(青、緑?)
・サンドバッグをくるくるしているトレイル(オレンジ、青)
の4つです(が、だいたいコピーしてパラメータいじっただけなので実質2個とかです)。

ここで得た知見としては
ParticleSystemのNoiseオプションは偉大だということでしょうか。
Particleは作っていてわかったんですがかなり単調になりやすく、
軽くNoiseを盛るだけでかなりいい感じになりました。
おすすめです。

Step.8 動きを完成させよう

ある程度エフェクトを盛って、最終的にどのくらいの見せ方にするかを確定させたら
実際に使う譜面を組み込んで動きの最終確認を行います。
ここでリズムゲームの最も重要な根幹、譜面の実行機構を紹介します。
弊社ではこの実行機構を譜面を走らせるものとしてRunner.csという名前で統一しています。

Runner.cs
    /// <summary>
    /// ノーツの探索ループ
    /// </summary>
    private void SearchNote()
    {
        //ノーツを最後まで生成したらゲーム終了
        if (_noteID < _notes.Count)
        {
            /* ノーツ生成ロジック
             * 保持してるタイミング情報は楽曲再生時間でのジャストなので
             * アニメーションする用のオフセット分を引いて早めに生成する
             * 規模が大きくなった時のために時間は他の場所で管理するのがいい
             */
            if (_notes[_noteID].Timing - TimeController.Instance.Offset < _source.time)
            {
                GenerateNote(_notes[_noteID]);
                _noteID++;
            }
        }
        else
        {
            GameEnd();
            _isPlaying = false;
        }
    }

    /// <summary>
    /// ノーツを生成する
    /// </summary>
    /// <param name="note">該当ノーツクラスを転送してくる</param>
    private void GenerateNote(Note note)
    {
        //生成と同時に初期化
        var g = Instantiate(_items[note.Lane]);
        g.GetComponent<Punch_Item>().Initialize(TimeController.Instance.Offset, note.Lane);
    }

UpdateでSearchNoteメソッドを叩き、ノーツ生成タイミングをコントロールしています。
ただ、リズムゲームのノーツ生成メソッドとして60FPSは遅く、せめて120FPSは確保しておきたいところです。
今回はさほどコストをかけられないのでこのような実装になっています。
(ちなみにRefrain Coreにはその制限を突破できる小ネタが入ってたり入ってなかったりします。)

Step.9 ビジュアルを磨こう

譜面作りに悶絶していると、外注した素材がそろそろ揃ってきているはずです。
ここからは素材をすべて組み込んで、爆速で見た目を作り上げていきます。
(こいつ常時爆速で組んでね?)

1. モデル配置

今回はMark-onから受け取ったモデルを何もいじらずそのまま配置しました。
というのもそもそもポリゴン削減のためすべてのメッシュが結合されていました。
モデルを配置したらどの方向を向いてプレイするのか、カメラ配置と原点を確認します。
このあたりはリズムゲームというか、Unityの話なので割愛します。

2. ライティング

ここはRiftS版とQuest版で大きく異なります。

RiftS

PCビルドではかなり性能に余裕があるので、ポイントライトを贅沢に使います。
しかもベイクせずにリアルタイムで描画します。まぁ贅沢。
SnapCrab_2019-12-07_01-21-40_No-0000.png

部屋に組み込まれている電球が21個(だっけ?)存在し、そのすべてにポイントライトを埋め込む暴挙。
お陰様でフローリングの床反射もでらキレイに写っています。
ここは正直おすすめはしませんが、性能が許すのでやってしまいました。

Quest

ご存知の通りそもそもAndroidのライティング性能には限界がある上に、
Questがいくらいいチップ積んでるとはいえポイントライト21個もおいたらそりゃフレームレート2ですわ。

SnapCrab_2019-12-07_01-33-26_No-0000.png

今回Quest版では全ポイントライトを削除し、スポットライト2つを向き合わせて照らすという手法で
ノーツも部屋も照らしています。
こっちも暴挙感が否めませんが、まぁ動くしいいんじゃない。
リズムゲームとしてはノーツがちゃんと目視できれば問題ないです。
(リリースするときは別の話)

3. ポストプロセス

プレイヤーが見る用のポストプロセスを組みます。
軽率にBloomを載せましょう。
ただ今回は少しBloomと相性が悪く全体的に明るい部屋なので、
油断するとすべてが発光して白飛びします。
今回はめんどくさかったのでPPSv1です。
プロファイルはRiftSQuest共通でこんな感じのパラメータになりました。
SnapCrab_2019-12-07_01-50-19_No-0000.png

Thresholdは少し高めに、Intensityを限界まで下げて、
本当に光ってるところだけを光らせています。
具体的には天井の電球ですね。
もう少し落とすと電球が光らなくなり、もう少し強くすると床やグローブの反射が光り始めるという
難しい場所での調整でした。かなりいいパラメータになったと思います。

Step.10 動画を撮ろう

ビジュアルが完成したら徹夜録画をしてみんなに見てもらいましょう。
個人的にはVRリズムゲームは主観視点がいいんじゃないかなーと思います。
というのも実際にプレイヤーが見るのは主観視点であり、譜面確認にもなるためです。
ただVRリズムゲームのプレイヤー層とゲーセンリズムゲーム層は根本から違うみたいなので
話半分に聞いてください。
ごんびぃーが見たいので主観動画もください(ヨクバリス)。

大人は嘘をつく

実はBeat Knuckle初出の動画にはトリックがあり、嘘はついていませんが真実を言っていません。
皆さんはVRゲームのプロなので分かる方もいらっしゃるでしょうが、一応答え合わせです。
この動画は見栄え用に別のポストプロセスプロファイルを使っていました。
パット見主観視点の動画ですが、OVRCameraRigをそのまま表示しているのではなく、
OVRCameraRigの子に別のカメラを設定して更に視野角を少し広げています。
そのカメラにVR用とは違う設定のポストプロセスプロファイルを適用し、
VR空間内で見るであろう光景とはかなり違う見栄えに仕上がっています。

SnapCrab_2019-12-07_02-41-11_No-0000.png

VRゲームとしてはご法度のモーションブラーを少し強めに掛けています。
更にBloomもかなり強化して、画面全体がびかびかするようにしています。
Radiusが低いので全体が白飛びするようなことはありませんが、
光っているところは更に綺麗に光るようになっています。

まぁ、そりゃ綺麗に見えるよという種明かしでした。
ただもちろんこれは嘘ではなく、実際にゲームをビルドした際、外部画面に出力されるカメラとなっています。
Spaceキーを押すことで部屋を様々な角度から定点カメラで見られたり、
プレイヤーの頭部追従のカメラに切り替えられたりします。
そのため嘘ではありません。真実を言わなかっただけです。

余談ですが、XRKaigi2日目に有名な彼とお話するチャンスがありましたが、
最終的な結論としては「やっぱりビジュアル第一だね」という身も蓋もない結論にたどり着きました。
悲しいですが真実ですね。
分岐しなかった方の未来なのでわかりませんが、もし外部カメラではなくOVRCameraを単純に録画した場合、
ここまでふぁぼRTが伸びることはなかったんじゃないかなと分析中です。

「ごんびぃー君・・・大人は嘘をつくもんだよ・・・」
この思想を継承してしまった。

Step.x 以降の展開を考えよう

ここまでで完全0スタートからBeat Knuckleが出来上がるまでの
ざっくりとした流れを解説できました。
Beat Knuckleの作り方どころか株ごんの思想まで出てしまいました。
今後自己紹介にこの記事を出そうかな。

このあとBeat Knuckleがスケールアップしていく上で考えられる選択肢はいくつかあります。
今の所ごんびぃーの脳内にあるビジネスモデルを少しまとめてみます。

1. ゲームシステム見直し

要するに根本を作り直すという話です。
正直なところ飛来式の表現をするVRリズムゲームは飽和状態です。
慣れ親しみやすさは確かにありますが、それ以上にすでに陳腐化している可能性のほうが高いと考えています。
そのためStep.1からやり直し、”ジャスト”の表現から再構築します。
もちろんStep.2以降は無駄にはせず、エフェクトや3Dモデルなどはそのまま活かせるでしょう。

アイデアは当然ながら出しません。飯の種です。

2. リファクタリング

これはゲームの本質的にはあまり変わりませんが、エンジニアの満足度が上がります。
今回は1ヶ月での制作で常時爆速だったため、ごんびぃー産リズムゲームにしてはかなり雑な作りになっています。
SnapCrab_2019-12-07_02-59-35_No-0000.png
ほんとに1ヶ月なのよ。

リファクタリングというより、ごんびぃーが作り直す場合は
全てをRefrain Coreに差し替えます。
このまま開発継続とか到底無理です。譜面制作で死にます。

3. 操作法改良

現在のBeat Knuckleは完全に物理判定で、グローブとミットが衝突したかどうかだけを検知しています。
これはリズムゲームとしては非常に雑で、本来ならば手を動かしているかを検知すべきです。
その操作法周りを改良します。

これにはいくつかルートがあり、
・パンチ力を判定として、一定パワー以上で殴ると判定→SEIYAルート
・ジャストタイミングと手を振ったタイミングを検知して近ければ判定→Airtoneルート
などいくつか考え方があります。

4. プレイヤー層変更

そもそもBeat Knuckleの想定プレイヤー層ってどこだよっていう話ですが、
一応当初の想定では運動したい人向けと考えていました。
これもいくつか改良ルートが存在し、
・Vtuberなどとコラボしてキャラクターゲームとして仕上げる→ポリフるルート
・ノーツ低速化やシステム単純化をして、ご老体想定とする→Rikutyさんルート
などの考え方があります。

要するに

株ごんはBeat Knuckleでは満足しておらず、とりあえず登壇のためのネタ作りのために出しました
という認識です。
どちらかというと今回の主題は”1ヶ月でVRリズムゲームを作ること”にあり、
ボクシングはたまたまテーマとして狙われた哀れな子です。
Beat Knuckleというタイトルを手放すつもりはないので、
殴ることを操作方法とするゲームを今後も継続して開発していくつもりではあります。
Refrain Coreを投入できる環境ならば今回レベルのモックアップ程度なら連発できるので、
いろんな表現方法を軽率に試して知見を稼いでいこうと思います。
(ついでに周囲を牽制していゲッホゲホ)
目指せ国産VRリズムゲーム制圧。

まとめ

ということで、1ヶ月でVRリズムゲームを作ってみました。
割と現実的に可能ではないでしょうか?
Beat Knuckle初期型のリポジトリはオープンソースですので、
ご自分で改築してぜひごんびぃーに見せてください。

ちなみに弊社は今の所コンサルは無料なので、
リズムゲームわからん!!!という方はご相談ください。
といってもBeat Knuckleリポジトリにある以上のことはあまり言えません、、というくらい圧縮しました。
実装は雑でしたがコメントをかなり丁寧に残したつもりなので、ぜひ読んでください。

おわび?

この記事すらかなり爆速で書いているのでおそらく抜けがあります。
しばらくは編集を繰り返すと思いますので、ちらほら見に来てくださると大変嬉しいです。

では、この記事はここで締めさせていただきます。
ありがとうございました!

株式会社GONBEEE-project 代表取締役
ごんびぃー

GONBEEE_project
リズムゲームを作っています。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away