スクリーンショット_2020-03-14_2.29.40

VRCSDK2で制作したアクションゲームワールドのギミックを解説する

記事の概要

筆者はSteamVR/OculusQuest上で動くアプリケーション「VRChat」にてアクションゲームワールドを制作しました。
まずは、普段のUnityゲーム開発とVRCSDK2を使ったゲームワールド作りにおいての相違点を紹介した上で、ワールドに用いたギミックを具体的に解説します。
(自分はワールド制作においてまだまだ知らない機能などが存在します。特にシェーダー周りについては言及しません。今回の記事は、数ある制作の手法の中の1つのケースとして捉えてください)

制作したワールドは以下のものです。

以下の記事が企画とリソース制作編だとすれば、今回は技術編となります。そのため、多少想定読者の層が異なります。

グラフィックリソースに困っている方へ(宣伝)

今回、アクションゲームワールドの制作について解説しますが、その前にグラフィックリソース(3Dモデル)を持たないので制作に取りかかれないという読者がいることが予想されます。

そんな時のために!!今回私がゲームワールドを作る上でモデリングした3Dモデルを販売しています。

今回の記事で解説する「武器」「敵」についてのモデルが全て揃っている上に、床や武器を配置する台座などもついてきます。
自分のワールド制作でも使えるような利用規約に設定しています。
なので、今回の記事と販売モデルをベースにワールドを作り、VRChatで公開などされても大丈夫です。

モデルの準備に困りそうな方は、よければご購入ください。
もちろん別のアセットを利用した場合でも、解説するギミックは動くと思います。

記事の想定読者

・VRChatのゲームワールドをVRCSDK2で作りたい方
・Unityゲーム開発についてある程度の経験がある方

※ 以下のいずれかに当てはまれば、記事は読み切れると思われます。
 ・「はじめてのUnity」チュートリアルの範囲を理解している
 ・Unity, VRCSDK2で自作のゲームを1, 2つ作ったことがある
 ・C#の基本的な使い方を理解している(書籍「スラスラわかるC#」相当)
 ・上3つには該当しないが、プログラミングができる

普段のUnityでの開発と、VRCSDK2を用いた開発との相違点

この節はUnityゲーム開発とVRCSDK2の違いを明確にするためのものです。私と明らかに違う境遇の方(もうVRCSDK2の特性を捉えている、Unityゲーム開発を全く知らないなど)は、この節は読み飛ばして、後の具体的なギミック解説の部分に進まれても大丈夫だと思います。

私はUnityでのゲーム開発(スタンドアロンPC向け・ネットワークを使わない)を趣味で少しだけ行ったあとにVRChatを始めました。
最初は今までのUnityゲーム開発の知識でワールドも作れるのではないかと考えていましたが、それは大きな間違いでした。
自分で書いたC#が使えないというのです。

最初に、普段のUnityでの開発(= スタンドアロンPC向け)と、VRCSDK2を用いた開発の相違点をまとめます。

・開発で使える技術的なリソース
・アニメーションとVRC_Triggerコンポーネントで値を扱う
・VRC_Triggerコンポーネントの使い方
・アニメーションでコンポーネントを活用する
・他のスクリプトの機能を使う
・同期の考え方と選択肢
・変数の出力方法

以下の話はVRC_TriggerのAdvanced Mode前提です。

・開発で使える技術的なリソース

先ほど自分で書いたC#が使えないと書きましたが、代わりにどのような方法でゲーム制作を進めていけばいいのでしょうか。

使えるコンポーネントが公式のドキュメントに記載されています。

Quest向け開発を視野に入れている方は、さらに使えるコンポーネントが制限されます。こちらもご確認ください。

使えるコンポーネント自体はたくさんあるな...という感じですが、PC/Quest共通してここで重要なのは以下のことです。

・Unityコンポーネントの多くが使える
・Animation, Animatorが使える
StandardAssetsのC#スクリプトが使える
・VRC_(なんとか)というコンポーネントが大量に使える

これらのコンポーネントを中心にゲーム開発を進めることになります。

・AnimatorとVRC_Triggerコンポーネントで値を扱う

私はVRChatのワールドを作り始めるまで、UnityのAnimation, Animatorに苦手意識を持っていました。苦手意識は特定の操作をするとアニメーションがリセットされ、初期座標に戻るなどの仕様に起因します。

ただ、VRCSDKにおいてAnimation, Animatorは強力な味方になります。数少ない変数の保持手段となるからです。
自分でC#を書けないことにより、値の宣言や保存が封じられるように思われます。しかしAnimator Controllerはint, float, bool, triggerの変数を保持できます。

今回の開発では、Animator Controllerでの値の宣言と保持を活用し、ゲーム制作に役立てました。

値の宣言と保持にはAnimator Controllerを使いますが、書き込みや演算はどうしたらいいでしょうか。ここでVRC_Triggerコンポーネントが登場します。

VRC_Triggerコンポーネントで、Animator Controllerが保持している変数に対する書き込み(int, float, bool, trigger)と四則演算(int型限定)ができます。

ただし、ひとつ注意点があります。

・代入対象の変数 = x
・代入対象の変数 [+-*/]= y

基本的に、上記の例ではx, yが定数の形しかとれないことにご注意ください。変数と変数の足し算は特殊な条件下でないと難しいと思われます。

今回のゲームでは、アニメーションやパーティクルシステム、SimpleCounterというアセットを併用し、擬似的に変数同士の足し算を実現しましたが、かなり条件を整える必要があります。

・VRC_Triggerコンポーネントの使い方

先ほどVRC_Triggerコンポーネントを用いた値の書き込みと四則演算について説明しましたが、それ以外にもまだたくさんの機能があります。

機能について書く前に、コンポーネントの使い方を紹介します。
VRC_Triggerコンポーネントで指定できる項目は、大まかに以下のものが挙げられます。解釈は私独自のものです。
詳しく知るためには公式の説明をご覧いただくのもいいと思います。https://docs.vrchat.com/docs/trigger-summary

・トリガー発火条件(Trigger)
・同期タイプ(Broadcast Type)
・アクション(Action)
 ・遅延秒数(Delay in Seconds)
 ・ランダマイズ(Randomize)

トリガー発火条件では、どのような条件下で後述のアクションが実行されるかを指定できます。
例として、OnEnterTriggerという、アタッチしているTriggerに設定されたCollider系コンポーネントの範囲内にオブジェクトが入ると発火するトリガーが挙げられます。Unity C#使いには馴染み深そうですね。
カスタムトリガーと呼ばれる、専用の命令によって発火されるトリガー(void型関数, サブルーチンに似たもの)も作成できます。

同期タイプでは、だれが実行したらトリガーが発動するのか、他のプレイヤーの環境でもトリガーを発動されるのかといった設定をできます。
VRChatはネットワークを使って他のプレイヤーと同時に遊ぶゲームなので、このような項目が必要とされるようです。
詳しくは「同期の考え方と選択肢」で解説します。

アクションでは、発火条件を満たしたときにどのような効果を発生させるかを指定できます。
例として、SetGameObjectActive(bool)という、指定したゲームオブジェクトがアクティブかどうかを書き換えるアクションが挙げられます。
ActivateCustomTriggerという、先ほど説明したカスタムトリガーを実行するアクションもあります。

先ほど説明した値の書き込み、四則演算もアクションのひとつです。
Animator Controllerの保持するint型の値に定数を書き込む場合、AnimationIntアクションを使います。また、定数を減算する場合にはAnimationIntSubstractアクションを使います。

遅延秒数では、トリガー発火条件を満たしたあと、どのくらい遅れてアクションを実行するかを秒数で指定できます。
同じ条件のトリガーを2つ並べて、遅延秒数と同期タイプで実行順、影響を及ぼすプレイヤーをコントロールするテクニックなどが存在するようです。

ランダマイズでは、トリガー発火条件を満たしたあと、どのくらいの確率でアクションを実行するかを指定できます。ランダム性のあるゲームを作るときに使えそうですね。

ギミックの発想の幅を広げるためには、トリガー発火条件とアクションを幅広く知っておくことが重要そうです。
ただ、最初からすべて暗記しようとすると苦行になるので、一通りVRCSDK2のサンプルなどで遊びながら確認するくらいがいいかなと思います。

以下の記事などもトリガーの機能を詳しく知る上で有用です。

アニメーションでコンポーネントを活用する

アニメーションでは、普段のUnityゲーム開発と同様に様々なことができます。ゲームオブジェクトのTransformの変更、MeshRendererコンポーネントからマテリアルの変数の変更など、いろいろな用途に使えそうです。

VRCSDK2で拡張されたアニメーションの機能のひとつとして、Triggerを呼び出せるアニメーションイベントの命令が挙げられます。

同一のゲームオブジェクトにVRC_TriggerとAnimatorをアタッチしていた場合、アニメーションイベント経由でトリガーを発火させられます。
ExecuteTriggerType(TriggerType)で指定したトリガータイプのイベントを、ExecuteCustomTrigger(String)で指定した名称のカスタムトリガーを実行できます。

アニメーションの機能は、先述したVRC_Triggerの遅延秒数などの機能よりも使い勝手がいい場合が多いです。アニメーション経由でトリガーを発火させたいときは有用な機能となります。

他のスクリプトの機能を使う

VRCSDK2を用いた開発では、StandardAssetやVRC_(なんとか)コンポーネントの機能を使い(関数を呼び)たくなるときがあります。
その場合、以下の方法で呼ぶことができます。他にもあるかもしれませんが...。

1. VRC_TriggerコンポーネントのSendRPCアクションを使う
2. Buttonコンポーネントから関数名を指定して呼び出す

2. については以下のサイトが詳しいです。

同期の考え方と選択肢

同期の基本事項をおさえる上で、以下のプレゼンテーションが非常に有用です。

このプレゼンテーションを踏まえた上で、少しアクションゲーム向けの話をします。

アクションゲームでは、しばしば反応速度が爽快感に直結します。敵に攻撃を当てて数秒後にダメージエフェクトと効果音が発生してもあまり気持ちよくありません。

基本的に、ゲームシステムを組む上ではMasterのもとで多くの処理を同期させていけば問題はない気もしますが、それでは反応速度が遅くなってしまいます。
そのため、スライド44ページ目のOverSyncを回避しながらAlwaysを使って反応速度を高めるなどの処理を書くことがあります。

変数の出力方法

例えば、アクションゲームを開発しているとして、今どのくらいのスコアを取っているかをUnity C#で書くことは簡単です。具体的には以下のような手順をとります。

1. スコアをint型で保持
2. スコアの値を取得
3. スコアの値をString型に変換し、スコアの文字列にする
4. スコアの文字列をtextなどに出力する

VRCSDK2では2以降の難易度が上昇します。それは以下の理由によります。
・出力機能を持つものが別のAnimatorControllerの場合、変数の値は取得できない
・スコアの値はString型に変換できない

この問題を解決するために、まずString型の利用を諦めました。代替的に、文字(数字など)を書いたテクスチャを用意し、パーティクル、アニメーション、カスタムシェーダーなどの方法で文字の内容を動的に切り替えることにしました。

まとめと余談

このように、VRChatでワールドを作るには、C#を使わないことにより代替的な手段を考えながら作業を進める必要があります。

余談ですが、Unityの機能やVRChatの機能を既存のプログラミング的手法に置き換えて見てみると面白いかもしれません。

以下のツイートにつながる一連の会話も、既存のプログラミング的手法をVRCSDK2の上で再現する上でとても参考になりそうな気がします。

アクションゲームのギミック解説

それではいよいよアクションゲームのギミックを解説します。ゲーム全体のロジックから、他のゲームにも再利用できそうなギミックを抜粋して図にしました。解説するのは以下の3要素です。

・武器のシステム
 ・剣のシステム
 ・銃のシステム
 ・杖のシステム
・敵のシステム
・スコアシステム

武器のシステム

武器の最終目標は、敵の当たり判定に接触できる(意図しない状況下で勝手に消滅しない)パーティクルを発射することです。

このパーティクルをAttackParticleと定めます。AttackParticleの決まりは以下の通りです。
・AttackParticleは、ParticleSystemコンポーネントを備える
・ParticleSystem>Collisionが有効化されている
・CollisionでDamageColliderレイヤーと衝突するように設定されている
・AttackParticleレイヤーに属する

以上のAttackParticleの決まりにしたがっていれば、武器の見た目は剣の軌跡でも、銃弾でも、魔法でも、レーザー兵器でもいいわけです。

この最終目標を念頭に、プレイヤーが「その武器を使っている」と実感できるようなシステムで各武器種の設計を行います。

剣のシステム

画像1

剣はコントローラのトリガーを押した直後に1.5秒間アクティブ(刃が大きくなる+AttackParticle=DamageParticleが放出される)状態になり、その状態でのみ敵にダメージを与えられるという仕様にしました。

アクティブ状態になったら、大きくなった刃の部分であるBladeObjectを有効化、同時にAttackParticleの放出を開始します。

アクティブ状態が終わったら、BladeObjectの有効化とAttackParticleの放出を止めます。

これだけではコントローラのトリガーを押しっぱなしでずっとアクティブ状態が続くことになってしまうので、AnimatorControllerのtriggerで一度指を話したかの確認までを行います。

銃のシステム

画像2

銃はコントローラのトリガーを押した瞬間に、AttackParticleの条件を満たした弾のパーティクルがまっすぐ発射されるという仕様です。

また、3発撃つごとに1.5秒のリロードが入ります。

[Weapon-Gun]のアニメーターについては弾数ごとにアニメーションクリップを用意しないでもよさそうです。
しかし、今回はアニメーションクリップを使って今回は残弾数を銃の上に表示する方法をとったのであえて分けてあります。
残弾数の表示に関しては図に記していませんが、単純なゲームオブジェクトのActive状態のオンオフで実現しました。

発射とリロード(と後述のスコア)システムについて、以下のワールドのギミックを参考にさせていただきました。

杖のシステム

画像3

杖は、チャージ秒数に応じて異なる魔法が発射される仕様です。
魔法の弾がAttackParticleの条件を満たしています。

チャージ秒数はコントローラのトリガーを引いている間溜まります。
(実はチャージはOnTimerで常時されていて、特定の条件でのみチャージしているように見える...ということが図を見るとわかります)

[Weapon-Wand]のVRC_TriggerにOnDrop : OwnerUnbが定義されていない場合は武器のトリガーを握って地面に下ろすと、そのままチャージが続行してしまいます。
地面に落ちているのにひとりでに杖がチャージを続ける...といった挙動は好ましくないので、Dropされた場合にもトリガーから手を離された場合と同じ挙動をとらせます。

敵のシステム

画像5

敵の仕様は以下の通りです。

・EnemyDamageColliderでAttackParticleとの衝突を検知する
・AttackParticleを検知したら、以下の行動を順番にとる
 1. ダメージエフェクトを発生
 2. 何点スコアが加算されたかを、今までいた場所に表示
 3. スコアをスコアシステムに加算
 4. 消滅
・保持スコアは最初は3点。生成後3秒ごとに1減少。最後は0に。
・生成地点からクリスタル(防衛対象オブジェクト)に向けて移動する
 ・NavMeshを用いる
・EnemyDeleteZoneに触れたら即消滅
 ・ゲームオーバーとゲームクリア時のリセットに対応するため

ここからは、どうやって敵の保持するスコアをスコアシステムに加算させたかの解説をします。

敵はPrefabから生成されます(オブジェクトプーリングなどは用いてないので、VRC_TriggerのアクションによってDestroyされます)
そのため、スコアシステムへの直接の参照を持つことはできません。

スコアシステムに参照させることを諦めたので、物理的に参照(?)させることにしました。

スクリーンショット 2020-03-14 17.40.49

敵はAttackParticleを検知したあとの3. のタイミングで、画像の右下の部分に移動します。
ちょうど丸を書いているあたりにパーティクルが衝突するとScoreSystemに1加算される命令を持ったゲームオブジェクト(ScoreReceiver)を置いておきます。
敵に丸印めがけてパーティクルを指定数発射させることで、スコアシステムに数字が加算させます。

敵には、パーティクルの発射に5秒間の猶予を与えます。5秒が経過した後に、最上位のオブジェクトを消滅させることでSceneから消します(Actionを使っているだけなので、本当にDestroyされているのかは不明ですが...)

ScoreReceiverはゲーム開始時からSceneに存在するので、ScoreSystemへの参照を持っておけることを利用したギミックです。

スコアシステム

画像5

先述したScoreReceiverは、パーティクルを検知するとScoreCounter(SimpleCounterをそのまま利用したアセット)に1点加算します。

まとめ

VRCSDK2を使ってアクションゲームワールドを作ったので、そのギミックを解説しました。

話は変わりますが、今回のワールドを作るにあたって、VRChatで出会った方にとても助けられました。

具体的には、VRChatを始めた日にチュートリアルワールドで出会った方に「我々は並行して決して交わることのない空間で他者の幻影を見ているに過ぎない」を意識してプレイした方がいいことを教えていただいたり、
ワールド制作初心者に制作手法を教えるイベントで出会った方に、多くのギミックの作り方を教えていただいたり...ということがありました。
今回のワールド制作は、周りの方々の助けがなければ形にならなかったでしょう。本当に頭が上がりません。

Unityで通常のゲーム制作をする上で必要な情報は、かなりの確率でQiitaに上がっていたりプログラミングスクールが記事にしていたりで、検索には困りませんでした。
しかし、VRChatのワールド制作は(一昔前よりはまだマシらしいですが)謎が多い側面があります。情報もTwitterに転がっていたり、口伝になっていたりと悪い意味で面白い散らばり方をしています。

これを読んでいる方もワールドが完成した暁には何らかの形で情報共有などしていただけると、コミュニティ全体の力になると思います。
私もまた機会があれば、ワールド制作記事など書ければと考えています。

ここまで長文をお読みいただきありがとうございました。