日本時間では8/19から8/31 午前4時の12日間開催されたVRChat向けのWorldJamです。
お題は「Obstacle Course」でした。
itch.io
本エントリーではどのように開発を進めていったか、それで得た知見についてを書きます。
Obstacle Jamについて
今回は WorldJam2 となり、1回目は5末頃に開催されました。
その時の記事は以下になります。
hyoromo.hatenablog.com
そもそもGameJamは
ゲームジャムはゲームとジャムセッションの合成語である。ジャムセッションとは、ミュージシャンが新たな素材や演奏を開発するために、ほとんど又はまったく事前準備なしに音楽をつくりだす行為を指している。同様にゲームジャムとはゲームクリエイターが実験的なアイデアをもとにプレイ可能なゲームへとプロトタイプをつくりあげるイベントである。
出典元:ゲームジャム - Wikipedia
なので、テーマを見て閃いたアイディアを元に期間内で作ればOKなイベントです。
前回は「後からJoinしたプレイヤーにも同期されるペン」、そして今回は「障害物コース製作ツール」です。
コースから外れた時にチェックポイントからやり直せたり、プレイ可能な人数分レース用のPrefabを生成管理してくれたり、スコア管理してくれたり、パワーアップアイテムによるPlayer状態変化を管理してくれたりと、いたれりつくせり。
なお私はToolを使わずにPlayerManager/ScoreManagerをカスタマイズして使わせて貰い、それ以外は未利用です。
このToolkitはGithubで公開されているのでJam未エントリー者でも利用できますよ。公式ドキュメントもあるのでまず読んでみると良いのかもしれません。
github.com
作ったゲームワールド
www.youtube.com
https://vrchat.com/home/world/wrld_10988403-18be-499b-a276-2990961f49df
手に自機を持ち、トリガーを引くことで射撃&前進をし続けて障害物をかわしながらゴールを目指すタイムアタック シューティングゲームを作りました。
初回プレイでも5-10分くらいでクリア可能なのでVRChat出来る環境があれば是非プレイしてスコアをTwitterで#VoxelShooting付き投稿してください!!!!
今回試したかったチャレンジは以下の通り。
- VRCObjectPoolを使った射撃システム
- Pickup状態でVRCStationに座らせての移動システム
- 大量の敵(障害物)
開発工程
VRC WorldJam向けにゲーム企画とざっくりタスク洗い出しまで終了。今回は用意された土台があるからモデリングも頑張ってみよう pic.twitter.com/Mk3S9WR1VU
— ひょろも (@hyoromo) 2021年8月19日
12日間いったい何していたかは上記Tweetのツリーに書き続けていました。
12日間しかないのにプロトタイプ開発に1週間掛かってしまっていまいました...というのも新しい試みに加えてVoxelモデル製作も自分で行っていたので手一杯でした。
今回得た知見まとめ
雑に今回得た知見についてのまとめです。パフォーマンスチューニングはイベント期間後に実施した内容でしたがまとめて書いておきました。
プレイヤーに理不尽さを感じさせない作り
結構ラフ通りに敵モデル作れた
— ひょろも (@hyoromo) 2021年8月22日
真上からのカメラでミニマップ表示されるんで、直上から見た時に敵種が判別できるようにしました pic.twitter.com/U2c9fPRdXr
障害物である敵がやってきそうな行動をデザインに落とし込みました。
- 矢印っぽい敵はその方向へ移動
- 口が尖っている敵はそこからチャージして弾を発射
- 頭に大きな電球みたいな物を付けている敵はその部分がバチバチする
遠くから見てプレイヤーに危機感を与えるデザインや、行動予測をある程度して貰えるようなデザインになるよう心がけていました。
プレイヤーへの自動割当
インスタンスにJoinしたプレイヤー個々に自動割り当てたいものってあると思います。例えば専用ペン、銃、個別管理データとか。
ToolkitのPlayerManagerがそれをVRC_ObjectPoolで行っていたため勉強になりました。
VRChat-Obstacle-JamプロジェクトのStarterSceneシーンにある Udon/PlayerDataManager がそれにあたります。
子のPlayerDataが親であるPlayerDataManagerにVRCObjectPoolで管理されています。どのように割り当てているかはPlayerDataManagerのコードを読むと分かります。
誰かがインスタンスへJoinしてきたら、PlayerDataManagerのオーナーがPlayerDataをSpawnさせ、そのPlayerDataのオーナーをJoinしてきたプレイヤーにします。
VRCStationによる空中移動
Pickupした自機のトリガーを引くことで、見えない座席に座らせて前進させています。
これは単にVRCStationコンポーネントを付けているGameObjectにRigidbodyも付け、Rigidbody#velocityで速度を与えているだけです。
これによる問題は加速させすぎるとPickupの持つ位置がズレてしまい自機がガクガク移動してしまうことです。
Interact後にStationに座らせ、Interactした手のBone座標に自機を乗せる対応の方が良かったかもなーと後から思いました。
Pickup&Sit
初めは自機をPickup直後にVRCStationに座らせる実装をしていました。ですが、これをやると何故か前回Dropした時の慣性が1秒未満の間だけStationに掛かる謎現象が発生、一通り止める手立てを試してみましたが効果なくて諦めました。
なので今回はPickUp後にトリガーを引いて貰うことで回避しました。
弾の管理
VRC_ObjectPoolで行っています。ObjectPool自体そんなに難しいものではなく、予め決めておいたObjectを再利用しているだけです。
上図のようにHierarchyにObjectPoolコンポーネントを追加するGameObject(ZikiBullets)、その子にObjectPoolで再利用されるGameObject(ZikiBullet)を配置。
上図のようにObjectPoolのPoolに再利用予定のGameObject(ZikiBullet)を追加*1。
コードも以下のようにシンプルに扱えます。前者が弾を発射する自機側で、後者が弾側です。
VRCObjectPool _bullets // 弾を発射 public void OnShot() { var bulletObj= _bullets.TryToSpawn(); if (bulletObj) { Networking.SetOwner(Networking.LocalPlayer, bulletObj); bulletObj.GetComponent<ZikiBullet>().OnShot(); } else { // 弾切れ } }
VRCObjectPool _bullets // 発射 public void OnShot() { // TODO:弾を撃った時の処理 } // 削除 public void OnDelete() { // TODO:弾を消す処理 _bullets.Return(gameObject); }
エリア外に出た時のエリア内に戻す処理
宇宙空間には目に見える壁が設けにくいため、エリア外にColliderを設置して自機が外に出ようとしたらエリア内に戻すようにしています。
移動は Rigidbody#velocity で行っていたため、エリア外になったら数秒間ベクトルを反転させてエリア内に戻すようにしました。
弾の自動Respawnに対処
シューティングゲームなので当然自機は弾を発射します。
発射する位置によっては弾がVRC_SceneDescriptorのRespawnHeightYで指定した座標を超えてしまい、Respawnされるケースがあります。
Respawnされると初期座標へ戻されてしまい、都合が悪い場合があります。
そのため「エリア外に出た時のエリア内に戻す処理」のCollider内に入ったら手動で弾を消すようにしました。
声の減衰距離
本ワールドは移動スピードが速く、すぐフレンドと離れてしまってデフォルト設定では会話がままなりませんでした。
なので、Playerの声が遠くに離れても届くようワールド側で設定しています。スタート地点からゴール地点まで声が届くよう調整しました。
APIとしては VRCPlayerApi.SetVoiceXXXX で設定する事になりますが、UdonSharpがWorldAudioSettingsクラスを用意しており。こちらを使うことでInspectorから手軽に調整出来ます。
非Humanoidアバターが椅子に座れない問題
VRCStationはHumanoidアバターしか座れないようで、非Humanoidアバターが座ろうとすると Interact は呼ばれるけど OnStationEntered は呼ばれません。
これは今回の知見というよりMaze In Cubeリリース後に知ったことです。非Humanoidアバターはpublicだと AFK_Avatar_1 とかですね。
同時SE再生による音割れ問題
音を同時に鳴らすと音割れする問題が発生します。この問題はVRChatで音声を鳴らす時に付ける VRC_SpatialAudioSource を有効にする事で解決しました。
Use AudioSource Volume Curve を false にする事が重要で、更にGain/Farを設定して音が届く範囲の調整を行います。
Occlusion culling
初期状態だと1フレームで描画されるものは見えている範囲だけじゃなく、遮蔽物に隠れているものも描画されます。このイメージは一度UnityのFrame Debuggerを利用すると分かりやすいです。
遮蔽物(Occluder)と遮蔽される物(Occludee)の設定を予めしてBake、リアルタイムでカメラから見て遮蔽物に隠れる物を描画しないでくれます。
Occlusion cullingはVRChat独自で別途何か必要な事は無かったので、分かりやすい使い方の記事を参照ください。
light11.hatenadiary.com
LOD
大量にモデルを表示する時、カメラからの距離に関わらず全て同じ品質のモデルを描画するとなると頂点数が多くなり処理負荷が上がってしまいます。
これに対応する手法の一つとしてOcclusion cullingとは別に、カメラから近いか遠いかで表示するモデルを切り替えるLOD(Level of Detail)を使いました。
1. ObjectPoolと同じように管理するRootGameObjectを用意して LOD Group コンポーネントを追加
2. カメラ距離に応じて何段階モデルを切り替えたいかに応じ、子にモデルを追加してLOD0/1/2ごとにどのモデルを表示するか設定*2
3. それぞれカメラ距離がどの程度で切り替わるかのパーセンテージを調整
4. Culledはモデル自体がCullingされる距離設定なため、されて問題無いなら設定
こちらもUnity標準機能そのまま使えるため、分かりやすい本エントリーでは詳しく言及しません。詳しい解説記事を参照ください。
nn-hokuson.hatenablog.com
Animator
Animatorはたとえidle状態でもチリツモで負荷に繋がっていました。
大量に表示するものにAnimatorを付けている場合は必要な時に有効にする、とした方が負荷が下がります。
なお、今回はカメラに大量のAnimator付き敵が映ってしまうため上記対応を行いましたが、カメラに映らない場合はAnimatorの Culling Mode をCull Completely(描画されない場合はAnimatorを無効にする)にしておけば同じ事だと思います。
VRChatのJam向けAssetの注意点
前回も今回も開催期間中に何度かアップデートが入りました。
そのため配布Assetの改造は後半にするか、最初から実施する場合は覚悟しておくことです。
itchサイトでのJamエントリー手順
WorldJam1・2回目ともにitchサイトにワールド情報を登録し、Jamへのエントリーを求められました。3回目以降も同じと思われるため登録方法についても軽く触れます。
手順は以下の通り。既にアカウントを持っていたら(2)から。
1. itchサイトへアクセスしてユーザーアカウント作成
https://itch.io
2. ダッシュボードへアクセス
https://itch.io/dashboard
3. Create new projectボタンを押下
4. 登録プロジェクトに何を設定するかはWorldJamの提出方法を確認して入稿します。
ただ1・2まではサムネイル画像の登録し忘れ以外は特に気にする必要なかったです。
5. WorldJam開催ページを開く*3
6. ページのどこかにエントリー方法が記載されているので読んで実施します
1・2回目はJoin Jamボタンを押した後、submitボタンを押して先程登録したプロジェクトを選択。詳細にWorldURLを設定する....でした。
あくまで1・2回目のエントリー手順なので、3回目以降同じかは分かりません。必ず公式の手順を確認し、本内容は同じだった箇所だけ参考にしてください。
まとめ
公式から提供されるAssetは勉強になるし、新しいことへのチャレンジ出来るのはやる気も沸いてくる。
参加メンバーには独創性に溢れていてイベント完走後にメチャクチャ刺激される。同じテーマで走るからこそ味わえる刺激がありました。
テーマ次第ですが次回もあれば参加してみたいです。