各システムは特定の役割を果たすように設計されており、他のシステムに関する知識は必要最小限に留められています。
概要
- PlayerDataManagerは、ワールドに入場した各プレイヤーに対してPlayerDataオブジェクトを割り当てます。
- PlayerDataオブジェクトがスタートゲートのCheckpointに入ると、そのプレイヤーが参加したCourseがタイム計測を開始し、次のCheckpointを有効化します。
- PlayerDataオブジェクトが最後のCheckpointを通過すると、計測タイムがスコアボードに追加されます。
- PlayerDataオブジェクトがPowerUpトリガーに入ると、PlayerModsManagerが一時的にプレイヤーの移動速度やジャンプ力を変更し、一定時間経過後にデフォルト値へリセットします。
- PlayerDataオブジェクトがRespawnトリガーに入ると、Courseはプレイヤーを最後に通過したCheckpointの位置にリスポーンさせます。
以下のセクションでは、これら全体のエクスペリエンスを構成するプログラムとスクリプトについて説明します。
プレイヤー
ワールドに参加する各プレイヤーには、状態やコースの進行状況を管理するための「PlayerData」オブジェクトが割り当てられます。PlayerDataManagerはPlayerDataオブジェクトを割り当て、OnPlayerDataEnterプログラムをトリガーできます。
PlayerDataManager
このプログラムは、シーン内の「Udon」オブジェクトの下にある「PlayerDataManager」GameObjectで見つけることができます。ここには2つの重要なパブリック変数があります。 dataPool: このマネージャーと同じオブジェクト上にあるVRC Object Poolコンポーネントへの参照です。プレイヤーがワールドに参加すると、このマネージャーがそのプレイヤーのためにPlayerDataオブジェクトのスポーン(TryToSpawn)を試み、その所有権を付与します。 followCam: プレイヤーがコースを走る際にプレイヤーの上を追従するカメラへの参照です。PlayerDataオブジェクトが更新されるたびに、PlayerDataManagerが各オブジェクトにこの参照を割り当てられるように、ここで設定します。
Toolkit Windowの「Number of Players」オプションを変更すると、シーン内の既存のPlayerDataオブジェクトはすべて削除され、新しく作成されたコピーがPlayerDataManagerの子要素として追加されます。それぞれのパブリック変数は適切に設定され、Object Poolは新しいPlayerDataオブジェクトすべてを保持するように更新されます。
PlayerObject
PlayerObjectのプレハブには、PowerUpやHazardなどをトリガーするために必要なRigidbodyおよびCapsule Colliderコンポーネントが含まれています。このプレハブはカスタムレイヤーであるCoursePlayerに設定されており、HazardやPowerUpとインタラクションを行うためにCourseTriggerとのみ衝突判定が行われます。また、重要なプログラムが組み込まれたUdonBehaviourも備えています:
PlayerData
このプログラムは、コースを走行しているプレイヤーと他のすべてのシステムを繋ぐメインのコネクタです。変数は以下の通りです。 timeElapsed: Courseプログラムがフィニッシュゲートを通過した際に更新するSynced Floatです。この値が変更されると、PlayerDataオブジェクトのオーナーは自身のスコアボードにこのタイムを表示し、ローカルで最新の記録を確認できるようになります。ScoreManagerオブジェクトのオーナーはこの変更を検知し、新しいタイムとプレイヤーのdisplayNameをスコアボードに追加します。
isRacing: プレイヤーがスタートゲートに入った際にCourseによってtrueに設定されるBooleanです。プレイヤーがフィニッシュゲートに入った時、メニューから手動でリスポーンした時、またはCourseでResetが呼び出された時にfalseに設定されます。Courseによって使用されます。詳細については該当プログラムを参照してください。
rigidbody: プログラムのStart時にキャッシュされるため、インスペクターで設定する必要はありません。毎回のUpdateのたびに、プレイヤーの位置と回転に合わせて移動します。
player: ローカルプレイヤーの実際の VRCPlayerApi オブジェクトへの参照です。このプログラム上で同期された playerId が変更された際にキャッシュされます。プレイヤーの displayName を取得するために使用します。
timeDisplay: ローカルプレイヤーに対して最新の時間を表示する UdonBehaviour への参照です。
scoreManager: ScoreManager UdonBehaviour への参照です。そのオブジェクトのオーナーが、コースを完走したばかりの PlayerData オブジェクトから timeElapsed の変更を受け取った際、ScoreManager オブジェクト上のパブリック変数 scoreToProcess に、displayName と elapsedTime を連結した文字列をセットして処理させます。
scoreManagerObject: ScoreManager プログラムを持つ UdonBehaviour を保持している GameObject への参照です。スコア処理のロジックが ScoreManager オブジェクトのオーナー上でのみ実行されるようにするために必要です。UdonBehaviour の参照からこの GameObject を取得することはできないため、ここに含めています。
followCam: プレイヤーがコースを移動する際に追従する CinemachineVirtualCamera への参照です。プログラムは自身の Transform をカメラの follow および lookAt ターゲットとして設定し、isRacing が変化した際にこのカメラの優先度(priority)を変更します。
OnPlayerDataEnter
このプログラムは、PlayerData オブジェクトがトリガーコライダー内に入ったことを検知すべきオブジェクトに使用します。カスタムレイヤーである CoursePlayer と CourseTrigger を使用することで、特定のオブジェクトのみがこのコライダーをトリガーするように制限しています。トリガーが発生すると、内部イベントの OnPlayerDataEnter が実行され、様々な処理が行われます。このプログラムには以下の変数があります:
fxPrefab: Trigger 発生時にスポーンさせる GameObject です。プレイヤーに何かが起きたことを知らせるために、音を再生したりパーティクルを表示したりする用途を想定しています。
program: Trigger 発生時に実行したいイベントを持つ、対象の UdonBehaviour です。この program には、Checkpoint、PowerUp、Hazard など個別のロジックが含まれます。
eventName: ターゲットのprogramで実行するイベント名。
deactivateOnTrigger: 1回の Trigger 後に、このオブジェクト自身を非アクティブにするかどうかを指定します。これは Checkpoints など、1回の実行につき一度だけ有効にする必要があるアイテムで役立ちます。
lastCollider: Trigger ロジックを開始したコライダーです。Trigger が呼び出される前に一時的にキャッシュされ、必要に応じて PlayerData UdonBehaviour を検索するために使用されます。
fxSpawn: スポーンさせるFXの位置を設定するために使用するTransformです。設定されていない場合は、コライダーを持つオブジェクトのTransformがデフォルトとして使用されます。Finish Gateのように、コライダーを通過した際に別の場所で花火を発生させたい場合などに便利です。
sendPlayerData: ロジックのトリガーとなった PlayerData プログラムを渡すかどうかを決定するブール値です。Start Gateに入る際に使用されますが、他の用途にも活用できます。
PlayerData のコライダーへの侵入が検知されると、このプログラムは以下の処理を行います:
- eventName変数が設定されている場合、sendPlayerDataがtrueかどうかを確認します。trueであれば、衝突したUdonBehaviourのplayerData変数に、現在のUdonBehaviourを代入しようと試みます。
- 次に、ターゲットプログラム上でeventNameイベントを実行します。
- このプログラム上のfxPrefab GameObjectが設定されている(デフォルトの「self」のままではない)場合、そのプレハブのコピーをInstantiateし、fxSpawn変数の値に基づいて位置と回転を設定します。
- deactivateOnTriggerがtrueの場合、this GameObjectを非アクティブに設定します。
コースとチェックポイント
これは本プロジェクトの中核となる部分であり、タイムトライアルを完了するために通過する必要があるゲートやチェックポイントです。
コース
このプログラムはCourseManagerオブジェクトに配置され、ローカルプレイヤーのタイムトライアルの状態を管理します。同期される変数は一切なく、コースを走行しているローカルプレイヤーの情報のみを保持します。
Start時にResetを呼び出し、適切に初期設定を行います。 プレイヤーが自身でリスポーンすると、コースはResetされます。
Resetが呼び出されたら、Start Gate以外のすべてのCheckpointトリガーをオフにし、Start Gateのみをオンにします。これを行うには、checkpoints配列内の各GameObjectをループ処理してすべてのTrigger Colliderを検索し、インデックス0のGameObjectに対してSetActiveをtrueに、それ以外のGameObjectに対してはfalseに設定します。 また、nextIndexを-1に設定し、isRacingをfalseに設定します。
StartRaceが呼び出されたら、以下の処理を行います:
- startTimeに現在の時刻を代入する
- isRacingをtrueに設定する
- nextIndexを1に設定する(Checkpoint 0を通過することでレースが開始されるため)
Checkpointがトリガーされると、CourseのnextIndexを自身のインデックス+1に設定します。これによりCourseプログラムのnextIndexChangeイベントがトリガーされ、次のCheckpointのGameObjectがアクティブになります。
Update中、プレイヤーがisRacing状態かどうかを確認します。もしそうであれば、走行開始からの経過時間を取得し、timeDisplayのTextオブジェクトに設定します。
FinishRaceが呼び出されたら、以下の処理を行います:
- isRacingをfalseに設定する
- 対象の PlayerData プログラム上の timeElapsed を、現在の時刻から startTime を引いた値に設定します。
- コースを走行中のプレイヤーがいなくなったため、playerData を null に設定します。
- resetDelay 秒間待機してから、コースを Reset します。
Respawn 時には、プレイヤーが isRacing かどうかを確認します。もしそうであれば、最後に通過したチェックポイントの座標へ戻します。そうでなければ、ワールド側で元のスポーン地点のいずれかにリスポーンされるよう、十分に低い位置へテレポートさせます。
ObstacleCourseData
このカスタムスクリプトは、使用するプレハブ、プレイヤー数、デフォルト速度など、コースに関するすべての情報が格納された ObstacleCourseAsset への参照を保持するものです。これは Utility Window によって読み込まれるため、シーン内に1つ配置しておく必要があります。このパッケージを新しいバージョンに更新した際に上書きされないよう、独自のファイルを必ず作成してください。既存のアセットを複製することで、デフォルト値を正しく設定できます。
Checkpoint
Checkpointオブジェクトにはそれぞれ、タイムトライアルでの順序を示すindex(インデックス)が設定されています。このインデックスは、Utility Windowを使用してCheckpointを配置したり、順序を変更したりする際に自動的に設定されます。各オブジェクトにはTrigger ColliderとOnPlayerDataEnterプログラムが備わっており、これがサンプルプレハブの「UdonProgram」というオブジェクトにあるCheckpointプログラムを呼び出します。プログラムの仕組みは単純で、以下の3つのイベントが発生します。
StartRaceは、Courseプログラム上のplayerData変数を、このチェックポイントに入ったばかりのUdonBehaviourに設定します。これが発生すると、Courseがレースを開始します。
Triggerは、Courseプログラム上のnextIndex変数を、現在のindex + 1に設定します。
FinishRaceは、単にCourseプログラム上のFinishRaceを呼び出します。
スコア
タイムトライアルといえば、親しい仲間との競争が欠かせません。Scoreシステムは、最新の走行記録と名前、そしてそのインスタンス内でのこれまでのベストタイムを同期します。
ScoreManager
このプログラムは、「Udon」GameObjectの下にある「ScoreManager」という名前のオブジェクトに配置されています。このプログラムはキューシステムを使用して、受信したスコアを処理し同期させます。プログラム自体はsynced variables(同期変数)を一切持たず、代わりにScoreFieldsを使用して値を同期します。「Number of Scores to Show」を変更すると、これらのフィールドはUtility Windowによって自動的に入力されます。
Start時、このプログラムは自身のRenderイベントを一度呼び出します。
Render時、プログラムはscoreCamのRenderを呼び出します。これにより、現在のスコアを表示するためにコース全体で使用されるRenderTextureへ、現在の視点がレンダリングされます。
このオブジェクトのオーナー側でscoretoProcessが変更されると、MakeRoomを呼び出した後にProcessNextScoreを呼び出します。誰かがランを終えてtimeElapsedが更新されるとインスタンス内の全プレイヤーにその情報が届き、もしそのプレイヤーがオーナーであれば、このオブジェクト上のscoreToProcessが更新されるという仕組みです。
MakeRoomでは、すべてのscoreFieldsがいっぱいかどうかを確認します。もし埋まっている場合は、値を順番にコピーして一番上に空きを作ります。
ProcessNextScoreにて:
- displayNameとtimeを再度分割して見栄えを整え、対応するScoreFieldのtargetVarName値に設定します。このターゲット変数は同期されるため、このように設定することで全員に対して更新されます。
- このスコアのtimeをハイスコアのtimeと比較し、必要であればHighScoreFieldを更新します。
- scoreToProcessの値を空の文字列に設定し、次にくるスコアを処理できるようにします。
- Renderイベントを全員に送信し、各ユーザーのスコアテクスチャを更新します。
ScoreField
このプログラムはシンプルかつ効果的なパターンを採用しています。logという名前のパブリックな同期変数があり、logが変更されると、フィールド内のテキストが新しい値に更新されます。このようにして、オブジェクトのオーナーが値を更新すると全員に対して同期・更新が行われます。これは他のプログラムからも簡単に行うことができ、本プログラムではScoreManagerからこの値を更新しています。
HighScoreField
このプログラムは上記の score フィールドと同じパターンを使用していますが、さらに同期された score float を持ち、スコアを比較して新しいスコアの方が高い場合にのみ更新するような使い方が可能です。また、変更内容の前に挿入される文字列である「prefix」も持っています。この場合、「High Score:」という文字列が入力された文字列の先頭に付加されます。
PowerUps
スコアを最大化したいプレイヤーにスピードやジャンプのブーストを提供するのは楽しいものです。また、スピードやジャンプのペナルティを障害物や危険要素として使用することで、プレイヤーに戦略の選択肢を与えることもできます。PowerUps を Utility Window で作成すると、すべて PlayerModsManager オブジェクトの子として配置されます。また、効果を適用できるように PlayerModsManager UdonBehaviour が自動的に設定されます。
これらは非常にシンプルなプログラムで構成されています。当然ながら OnPlayerDataEnter プログラムから呼び出され、単一の Trigger イベントを持っています。その変数は以下の通りです。
playerModsManager: Utility window を介して PowerUps を作成する際に自動的に設定されます。実際に効果を適用するために使用されます。
speedChange: トリガーされた際にプレイヤーの速度に適用する効果。0の場合はスキップされ、正の値で速度が増加し、負の値で減少します。
jumpChange: speedChangeと同様ですが、こちらはジャンプの勢い(Jump Impulse)に適用されます。
effectDuration: 効果が切れるまでの時間。
Triggerが発生すると、プログラムはPlayerModsManager上のspeedToProcessが0でない場合にそれを設定し、同様にjumpToProcessが0でない場合にも設定を行います。ロジックを簡略化するため、変化量と持続時間の値を一つのVector2にまとめ、xを変化量、yを持続時間として扱います。
PlayerModsManager
プレイヤーの能力変更を一元管理する場所があると便利です。例えば、持続時間2秒の「速度 +3」の効果中に、持続時間3秒の「速度 -1」の効果を受けた場合などを考えると分かりやすいでしょう。このプログラムでは、速度補正とジャンプ補正はそれぞれ打ち消し合うようになっています。そのため上記の例では、プレイヤーが「速度 -1」のパワーアップを取得した瞬間に、元の速度から1減少した状態にリセットされ、新たに3秒間のタイマーが開始されます。
このプログラムは、ScoreManagerと同様にキューを使用して動作します。speedToProcessが変更されると、使用する新しい速度を計算し、それをローカルプレイヤーのVRCPlayerApiに適用した上で、PowerUpのeffectDurationに基づいてカウントダウンを開始します。このプログラムはHUD上にモッド(効果)を表示し、そのタイミングに合わせてフェードアウトさせることで、プレイヤーが残り時間を直感的に把握できるようにします。タイマーが終了すると、VRCPlayerApi上のターゲットプロパティをデフォルト値にリセットします。これが「VRCWorldSettings」プログラムではなく、このプログラム内でそれらの値を保持・設定している理由です。
DestroyAfterXSeconds
このシンプルなプログラムは、OnPlayerDataEnterプログラムによって作成されるFXプレハブのような、ローカルでインスタンス化されるオブジェクトに役立ちます。このプログラムを使用することで、古いサウンドエフェクトやパーティクルシステムが何百個も残り続けるのを防ぎ、オブジェクトが確実に破棄されるようにします。
PlayClipFromArray
このプログラムは、FX Prefabなどで使用するサウンドにバリエーションを持たせたい場合に便利です。単一のAudioClipではなく、グループとして設定しておくことで、生成時にその中からランダムに1つが選ばれて再生されます。Footstepsプログラムなどにも活用できます。
ハザード(障害物)
プレイヤーに試練を与えたい場合は、さまざまなハザードを追加できます。いくつかサンプルプログラムを用意していますが、自由に独自のプログラムを作成してみてください!
Autorotate(自動回転)
このプログラムは、アタッチされたTransformを回転させるだけのものです。各軸の回転量を調整でき、Time.deltaTimeが乗算されるため滑らかに回転します。Animatorを使用した方がパフォーマンスは向上しますが、実験的な用途にはこれで十分です。
SpawnedHazard
このハザードは、接触したプレイヤーの速度を低下させます。speedChangeの設定方法はPowerUpと同様で、xはプレイヤーの速度に加算する量、yは効果の持続時間です。プレイヤーの速度を1秒間3低下させたい場合は、speedChangeを(-3,1)に設定します。生成時に名前を指定して「PlayerModsManager」GameObjectとUdonBehaviourを検索するため、パフォーマンスはあまり良くありませんが、機能はします。
HazardSpawner
このプログラムでは SendCustomEventDelayedSeconds を使用して、delay 秒ごとに障害物をスポーンさせます。サンプルプロジェクトでは、プレイヤーが避けるべき複雑な樽の丘を作るために、あえてわずかに異なる遅延時間を設定しています。
FallingBlock
このプログラムは、本サンプルに含まれるプログラムの中で唯一、プレイヤーとやり取りを行いながら OnPlayerDataEnter を使用しないものです。これは、プレイヤーの「入場」だけでなく「退出」も検知したいという目的がありますが、そのプログラムではそれに対応していないためです。プレイヤーが入場すると、SendCustomEventDelayedSeconds を使用して triggerTime 秒後に CheckForDrop を実行します。
CheckForDrop が呼び出された際、プレイヤーがまだコライダー内から退出していなければ、Rigidbody を非キネマティック(non-kinematic)に設定して落下させます(プレイヤーも一緒に落下します)。その後、resetTime 秒後に Reset を呼び出します。
Misc
Injection
このプロジェクトには、特定のコンポーネントへの参照をインジェクト(注入)するシステムがあります。これについてはこちらで解説しています。
最終更新: