各システムは特定の責任範囲を持つように設計されており、他のシステムについての知識を必要最小限に抑えるようになっています。
概要
- 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 の子として追加されます。それぞれの PlayerData オブジェクトに対して公開変数が適切に設定され、Object Pool はすべての新しい PlayerData オブジェクトを保持するように更新されます。
PlayerObject
PlayerObject プレハブには、PowerUp や Hazard などをトリガーするために必要な Rigidbody コンポーネントと Capsule Collider コンポーネントが含まれています。このプレハブはカスタムレイヤーである CoursePlayer 上に配置されており、Hazard や PowerUp とインタラクトするために CourseTrigger とのみ衝突するように設定されています。また、このプレハブには重要なプログラムを含む UdonBehaviour がアタッチされています。
PlayerData
このプログラムは、コースを走行中のプレイヤーと他のすべてのシステムを繋ぐメインのコネクタです。変数は以下の通りです。
timeElapsed: Finish Gateを通過した際に Course プログラムによって更新される同期された 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: 単一の Trigger の後に、このオブジェクト自体を非アクティブ化するかどうかを指定します。これは、実行ごとに一度だけアクティブにする必要がある Checkpoints やその他のアイテムに役立ちます。
lastCollider: トリガーロジックを開始したコライダーです。Trigger が呼び出される前に一時的にキャッシュされ、必要に応じて PlayerData UdonBehaviour を見つけるために使用されます。
fxSpawn: スポーンさせるFXの位置を設定するために使用する Transform です。設定されていない場合は、コライダーを持つオブジェクトの Transform がデフォルトとして使用されます。フィニッシュゲートのように、コライダーを通過した際に別の場所で花火を打ち上げたい場合に便利です。
sendPlayerData: ロジックをトリガーした PlayerData プログラムを渡すかどうかを決定するブール値です。スタートゲートに入る際などに使用され、他の用途にも役立つ可能性があります。
PlayerData コライダーのトリガーエントリーが検知されると、このプログラムは以下の処理を行います。
- eventName 変数が設定されている場合、sendPlayerData が true かどうかを確認します。true の場合、ターゲットの UdonBehaviour プログラムにある playerData 変数に対し、衝突した対象の UdonBehaviour を設定しようとします。
- 次に、ターゲットプログラムで eventName イベントを実行します。
- このプログラムの fxPrefab GameObject が設定されている場合(デフォルトの 'self' のままではない場合)、プレハブのコピーを Instantiate し、その位置と回転を fxSpawn 変数から設定します。
- deactivateOnTrigger が true の場合、this GameObject を非アクティブに設定します。
コースとチェックポイント
ここはプロジェクトの核心部分であり、タイムトライアルを完了するために通過する必要があるゲートとチェックポイントです。
Course
このプログラムは CourseManager オブジェクト上に配置され、ローカルプレイヤーのタイムトライアルの状態を管理します。同期変数は持たず、現在コースを走行しているローカルプレイヤーの情報のみを扱います。
Start 時、Reset を呼び出して自身の初期設定を行います。 プレイヤーが自身をリスポーンさせた場合、コースは Reset されます。
Reset 時には、スタートゲート以外のすべての Checkpoint トリガーをオフにします。これは _checkpoints 配列内の各 GameObject をループ処理し、すべての Trigger Collider を探し出し、インデックス 0 の GameObject に対して SetActive を true に、それ以外は false に設定することで実現しています。
また、_nextIndex を -1 に、_isRacing を false に設定します。
StartRace 時には、以下の処理を行います。
_startTimeに現在の時間を設定する_isRacingを true に設定する_nextIndexを 1 に設定する(Checkpoint 0 を通過することでレースが開始されるため)
Checkpoint がトリガーされると、そのコースの _nextIndex が自身のインデックス + 1 に設定されます。これによりコースプログラム上で 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オブジェクトには、タイムトライアルでの順序を示すインデックスがそれぞれ割り当てられています。これは、Utility Windowを通じてCheckpointを配置したり、その順序を変更したりする際に自動的に設定されます。
各オブジェクトにはTrigger Colliderがあり、OnPlayerDataEnterプログラムによって、サンプルプレハブ内の「UdonProgram」というオブジェクトにあるCheckpointプログラムが呼び出されます。このプログラムはシンプルで、以下の3つのイベントをトリガーできます。
StartRaceは、Courseプログラムの_playerData_変数を、このチェックポイントに入ったUdonBehaviourに設定します。これが発生すると、Courseはレースを開始します。
Triggerは、Courseプログラムの_nextIndex_変数を_index_ + 1に設定します。
FinishRaceは、単純にCourseプログラムのFinishRaceを呼び出します。
スコア
タイムトライアルにおいて、フレンドリーな競争なしでは物足りません。スコアシステムは、最新の走行結果のプレイヤー名とタイム、そしてインスタンス内でこれまでのベストタイムを同期します。
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_にその値を設定します。このターゲット変数は同期されるため、このように設定することで全員の値を更新します。
- このスコアのタイムと現在のハイスコア(High Score)のタイムを比較し、必要に応じてHighScoreFieldを更新します。
- _scoreToProcess_の値を空の文字列に設定し、次に受信するスコアを処理できるようにします。
- 全員にRenderイベントを送信し、それぞれのスコアテクスチャを更新します。
ScoreField
このプログラムは、シンプルかつ効果的なパターンを使用しています。_log という名前のパブリックな同期変数を持ち、_log が変更されるとフィールドのテキストを新しい値に更新します。この仕組みにより、オブジェクトのオーナーが値を更新すると、全員の環境で値が同期・更新されます。他のプログラムから簡単に操作できるため、今回のケースでは ScoreManager からこの値を更新しています。
HighScoreField
このプログラムは上記のスコアフィールドと同じパターンを使用しますが、さらに同期された score float を持ちます。これを使用することでスコアを比較し、新しいスコアが高い場合のみ更新を行うことができます。また、変更の前に挿入される「prefix」も持っています。この場合、「High Score:」という文字列が入力された文字列の先頭に追加されます。
パワーアップ
スコアを最大化したいプレイヤーにスピードやジャンプのブーストを提供するのは楽しいものです。また、障害物や危険の一部としてスピードやジャンプのペナルティを配置し、プレイヤーに戦略的な選択肢を与えることもできます。Utility Windowを使用して作成されたパワーアップは、すべて "PlayerModsManager" オブジェクトの子として配置されます。また、それらには PlayerModsManager UdonBehaviour が自動的に設定されるため、効果を適用することができます。
そのプログラムは非常にシンプルです。もちろん OnPlayerDataEnter プログラムから呼び出され、単一の Trigger イベントを持っています。変数は以下の通りです。
playerModsManager: Utility Windowからパワーアップを作成する際に自動的に設定されます。実際に効果を適用するために使用されます。
speedChange: トリガーされた際にプレイヤーのスピードに適用する効果。0を指定するとスキップされ、正の値を指定するとスピードが増加し、負の値を指定すると減少します。
jumpChange: speedChangeと同様ですが、こちらはジャンプの勢い(Jump Impulse)に作用します。
effectDuration: 効果が切れるまでの時間。
Trigger が実行されると、プログラムは0以外の値であれば PlayerModsManager の speedToProcess を設定し、同じく0以外の値であれば jumpToProcess を設定します。ロジックを簡略化するため、amount(量)と duration(持続時間)の値を1つの Vector2 にまとめており、x が量、y が持続時間を表します。
PlayerModsManager
プレイヤーの能力値に対する変更を一元管理する場所を用意しておくと便利です。例えば、「Speed + 3」(効果時間2秒)を適用した直後に「Speed - 1」(効果時間3秒)が適用されるような状況を考えてみてください。このプログラムでは、速度変更(speed mods)同士、およびジャンプ変更(jump mods)同士は互いに上書きされる仕様になっています。そのため、上記の例では「Speed - 1」のパワーアップがトリガーされた瞬間に、プレイヤーの速度は「デフォルト速度 - 1」にリセットされ、そこから新たに3秒間のタイマーが開始されます。
このプログラムは ScoreManager と同様にキュー(行列)の仕組みで動作します。speedToProcess が変更されると、プログラムは適用すべき新しい速度を計算し、それをローカルプレイヤーの VRCPlayerApi に適用して、そのパワーアップの effectDuration に基づくカウントダウンを開始します。このプログラムは適用中の修正値をユーザーのHUDに表示し、時間経過とともにフェードアウトさせるため、プレイヤーは残り時間を直感的に把握することができます。タイマーが終了すると、VRCPlayerApi 上の対象プロパティがデフォルト値にリセットされます。これが、対象の値を「VRCWorldSettings」プログラムではなく、ここで保持・設定している理由です。
DestroyAfterXSeconds
このシンプルなプログラムは、OnPlayerDataEnter プログラムによって作成される FX Prefab のような、ローカルでインスタンス化されるオブジェクトに役立ちます。このプログラムを使うとオブジェクトが確実に自己消滅するため、古い効果音やパーティクルシステムが何百個も残り続ける事態を防ぐことができます。
PlayClipFromArray
このプログラムは、FX Prefabなどで使用するサウンドにバリエーションを持たせるのに役立ちます。単一の AudioClip を設定する代わりに、このプログラムに複数の AudioClip のグループを設定することで、生成時にその中からランダムに1つを選択して再生します。「Use Auto Footstep」プログラムなどにも活用できます。
危険物
プレイヤーに試練を与えたい場合は、さまざまな危険物を追加することができます。いくつかのプログラム例を用意しましたが、自由に独自の危険物を作成してください!
自動回転 (Autorotate)
このプログラムは、自身がアタッチされている Transform を回転させます。各軸の回転量を調整でき、その値に Time.deltaTime が乗算されるため、滑らかに回転します。Animator を使用する方がパフォーマンスは向上しますが、実験的な試行を行う場合にはこの方法が有効です。
SpawnedHazard
このハザードは、接触したプレイヤーの速度を低下させます。speedChange は PowerUp と同様の方法で設定でき、x はプレイヤーの速度に加算する値、y は効果の継続時間を表します。プレイヤーの速度を1秒間3下げる場合は、speedChange を (-3, 1) に設定してください。生成時に名前で "PlayerModsManager" GameObject と UdonBehaviour を検索するため、パフォーマンスはそれほど高くありませんが、機能としては動作します。
HazardSpawner
このプログラムは SendCustomEventDelayedSeconds を使用して、delay 秒ごとにハザードをスポーンさせます。サンプルプロジェクトでは、プレイヤーが回避すべき厄介な樽の丘を作るために、それぞれわずかに異なる遅延時間を設定しています。
FallingBlock
このプログラムは、プレイヤーとインタラクションを行うものの中で、OnPlayerDataEnter を使用しない唯一の例です。その理由は、プレイヤーがエリアに入った時だけでなく「出た時」も検知する必要があり、OnPlayerDataEnter ではそれが考慮されないためです。プレイヤーが入った際には、SendCustomEventDelayedSeconds を使用して、triggerTime 秒後に CheckForDrop を実行します。
CheckForDrop が実行された際、プレイヤーがまだコライダーから出ていない場合は、自身の Rigidbody を非キネマティック(non-kinematic)に設定することで落下させます(その際、プレイヤーも一緒に落下します)。その後、resetTime 秒後に Reset を呼び出します。
その他
インジェクション
このプロジェクトには、特定のコンポーネントへの参照をインジェクション(注入)するシステムがあります。詳細はこちらで解説されています。
最終更新: