VR Escape Room
概要
Escape Room サンプルは、プレイヤーと環境との間の物理的な相互作用を開発する方法についてのアプローチを示しています。
コア は基本的なインタラクションとロコモーションを提供し、オプションモジュール は追加機能やインタラクションタイプを実装例として提供します。
すべてのモジュールにはフォルダが用意されており、必要なければ削除することができます。なお、本サンプルで提供するメインシーン(/Scenes/EscapeRoom Full.scene)は、すべてのモジュールとコンポーネントを使用しているため、モジュールを削除すると壊れてしまう可能性があります。
モジュールについては、本ドキュメントの最後に別途解説しています。
モジュール
- Object Pull
- Instant Camera
- Whiteboard
- Observer
- Ui Module
- Slots Module
- Shooting Module
技術情報
- このサンプルは、Server/Hosted Mode のトポロジーを使用しています。
- このプロジェクトは、Unity 2020.3.37f1で開発されました。
はじめる前に
サンプルを実行するには :
PhotonEngine Dashboard で Fusion AppId を作成し、Real Time Settings (Fusion メニュー) の
App Id Fusionフィールドに貼り付けます。PhotonEngine Dashboard で Voice AppId を作成し、Real Time Settings の
App Id Voice欄に貼り付けます。次に、"Start "シーンを読み込んで、"Play "ボタンを押します。その後、シーン全体を起動するか、特定のモジュールだけをテストすることができます。
[email protected] にメールでお問い合わせください。ダウンロード
| バージョン | リリース日 | ダウンロード | ||
|---|---|---|---|---|
| 1.1.3 | Oct 20, 2022 | Fusion VR Escape Room 1.1.3 Build 5 | ||
入力の処理
メタクエスト
- テレポート:AボタンまたはXボタンを押すとポインタが表示されます。ポインターを離すと、任意のターゲットにテレポートします。
- 触る : 手や指をボタンの上に置くだけで、ボタンが切り替わります。
- 掴む : 最初にオブジェクトに手をかざし、コントローラのグラブボタンで掴みます。
- 使う: セレクトボタンを押すと、掴んだオブジェクトを使用することができます。
HTC Vive ControllerとValve Index Controllerに対応していることをご確認ください。
デスクトップ
キーボード
- 歩く : WASD
- 上/下:スペース&左シフト
マウス
- 回転 : マウスの右ボタンを押したまま、マウスを動かすと視点が回転します。
- UI : 左ボタンで UI ボタンを押す。
- 掴んで使う (3D pens) : マウスをオブジェクトの上に置き、マウスの左ボタンで掴みます。その後、キーボードのスペースキーで使用することができます。
コア
サンプルのコアは、物理ベースのインタラクションを Rigidbodies と Articulation bodies を使って提供します。
リジッドボディは投げられるオブジェクトに、Articulation bodiesはレバーやダイヤル、ドアや引き出しのような機械的なシステムに使用されます。
カメラは物理的なものではなく、ユーザーの入力に基づくものです。テレポートロコモーションも含まれています (TeleportHandler.cs)
インタラクションフローの概要
- ポーリングと入力の送信 :
- (
XRInput.cs,LocalController.cs,PCInput.cs)
- (
- 入力に基づき、剛体と力を使って頭と手の位置を更新する :
- (
PlayerSystem.cs,XRObject.cs)
- (
- 手は入力を読み取り、HandTool と任意の
IControllerInputReceiverに渡す。- (
Hand.cs,IControllerInputReceiver)
- (
- HandTool と他のコンポーネントは入力に反応する :
- (
HandTool.cs,TeleportHandler.cs,InstantCameraInteractable.cs)
- (
- ハンドツールはホットスポットが範囲内にあるかどうかをチェックし、入力に基づいて取得/解放を行う。
- (
HotspotCollector.cs,Hotspot.cs,HighlightBase.cs)
- (
- ホットスポットは、インタラクションの開始/停止時に InteractableBase とすべての IInteractable に通知。
- (
InteractableBase.cs,GrabbableBase.cs)
- (
- GrabbableBase は力を使って手の位置を追跡する。
- (
GrabbableBase.cs,GrabbableRigidbody.cs,GrabbableArticulation.cs)
- (
入力
入力の送信
### LocalInputBase.cs
Fusion からは独立した DontDestroyOnLoad オブジェクトとして存在し、セッション間で持続的に動作します。入力を収集して Runner に送信する役割を担います。
XRInputがその主な実装です。PCInputはデバッグ用の入力メソッドで、より素早く反復処理を行うことができますが、機能は限定されています。
入力を受け取る
PlayerSystem.cs
プレイヤーオブジェクト(頭と手)の基本的な位置決めを XRObject.cs を介して処理します。
XRObject は入力に追従して剛体力を更新します。
Hand.cs
コントローラの入力を読み取り、IControllerInputReceiverを介して入力を受け取るために登録されたものに渡します。
基本的なインタラクションのために、明示的に HandTool.cs に入力を渡します。
- 入力を受け取りたいシステムは
IControllerInputReceiverを実装して、ハンドに登録することができます (例 :TeleportHandler.cs) - 掴んだオブジェクトは一時的に登録することができます ( 例
InstantCameraInteractable.cs) -> Hand.AddInputBehaviour() / Hand.RemoveInputBehaviour())
インタラクション
HandTool:
- Grab / Dropのための入力を読み取ります。
HotspotCollectorを使用して、ホットスポットを見つけ、ハイライトします。Hotspotの取得と削除
HotspotCollector:
- レイヤーとタグを元に、半径内の
ホットスポットを検索する。 - フィルタの順番:
レイヤー, タグ, HighlightPriority, 距離。 - ホットスポットがホバーされるとハイライトされる (
HotspotCollector -> Hotspot -> HighlightBase)
Hotspot:
HandTools がオブジェクトと相互作用することができる相互作用点を表します。
- オブジェクトをより多くのハンドでつかむ必要がある場合は、ホットスポットを追加します。必要であれば、これらのホットスポットは同一であっても構いません。
- Start / Stop Interactionコールは同じGameObjectの
IInteractableとInteractableBaseを親として渡します。
InteractableBase:
InteractableBase: インタラクティブ・オブジェクトの基底クラス。
** Start / Stop Interaction 呼び出しを取得します
グラブ可能なオブジェクトの階層構造。
Root:InteractableBase, Rigidbody, NetworkRigidbody, Highlight, NetworkObject, BodyProperties.VisualsHighlight Visuals
CollisionHotspot:Hotspot, GrabbableRigidbody / AttachmentRigidbody, Collider (Trigger)
BodyProperties
BodyPropertyCollection (スクリプト可能なオブジェクト)を割り当てて、掴んだときのオブジェクトの動作を制御します。
BodyPropertyCollection
BodyPropertyDataの配列が格納されており、何人の手がオブジェクトを掴んだかに応じて適用される(0:掴んでいない、1:一人の手で掴まれた、n:n人の手で掴まれた)。
この方法によって、オブジェクトは片手で扱うには扱いにくいか重すぎるが、2人以上なら簡単に扱うことができるようになる。
BodyPropertyData:
- `Mass
DragAngularDragJointFrictionArticulation(Articulation bodiesに対してのみ適用可能)UseGravityOverrideInertia(設定すると、慣性がInertiaWhenGrabbedScale * Vector3.oneに設定されます。)InertiaWhenGrabbedScale(オブジェクトが回転の変化に対してどの程度抵抗するかを制御します)VelocityExtrapolation(オブジェクトに新しい力を加えるとき、現在の速度をどれだけ打ち消すか。力自体の倍率としても使用される)TorqueExtrapolation(オブジェクトに新しい力を加えるとき、現在の角速度をどれだけ打ち消すか)
GrabbableBase (Hotspot上)。
掴まれたときにオブジェクトを手の位置まで追跡する処理を行います。
ボディの種類によって適切な実装を選択する。
GrabbableRigidbodyGrabbableArticulation
AttachmentRigidbody (on Hotspot GameObject):
Rigidbodyオブジェクトを
HandToolにアタッチします。これは非常にキビキビした動作が必要なライトオブジェクトに使用されます。(例:ホワイトボードマーカー、InstantCamera Pictures、Pistol)
これが動作するためには、オブジェクトは特定の構造を必要とします。RootVisualsCollision/LogicCollidersHotspot:AttachmentRigidbody, Trigger Collider,
ハンドを掴むと、
CollisionとVisuals GameObjectsはHandに直接アタッチされ、あたかもハンドの一部であるかのように動作します。ハイライト
インタラクト可能なオブジェクトがハンドの範囲内にあり、インタラクト可能であることを視覚的に確認することができます。
Value Provider / Reader
value providerは、あるシステムから別のシステムへ情報を転送し、特別なコンポーネントを使用せずに単純なロジックチェーンを作るための汎用的なコンポーネントです。例:
- articulation body (
ArticulationBodyValueReader.cs) から値を読み取る。 - 閾値と比較する (
ValueLogicIntCompare.cs) - その値を使って、他のものを動かす (
ArticulationBodyDriverSetLimits.cs)
StreamTextureManager
モジュールに必要です。OnReliableDataコールバックを使ってネットワーク上にテクスチャーデータを送るシステム。Runnerと同じGameObject上にある必要があります。
IgnoreCollision
Physics.IgnoreCollisionAPIを使って特定のオブジェクト間のコリジョンを無視する必要がある場合があります。
IgnoreCollision.cs: 静的な無視。ゲームプレイ中に変更されることはありません。スクリプトのあるGameObjectのコライダーはリストにあるすべてのコライダーを無視します。NetworkColliderCollection.cs: 無視することができるコライダーのグループ。NetworkIgnoreCollision.cs:AddIgnore()とRemoveIgnore()を使用して、割り当てられたすべてのNetworkColliderCollectionsに対してローカルコライダーを無視することができます。
アーティキュレーションボディ
このサンプルでは、アーティキュレーションボディを多用しています。これは Unity で接続されたボディをシミュレートする安定した方法で、ここではレバー、ボタン、ダイヤル、ドロワーなどに使用されています。
このサンプルでは、NetworkArticulationBodyの実装は完全な機能ではありません。
参考文献としてhttps://docs.unity3d.com/2020.3/Documentation/ScriptReference/ArticulationBody.html
コアコンポーネント
NetworkArticulationBodyRootはRoot上に配置されます。NetworkArticulationBodyは、すべての子オブジェクトArticulation bodiesに配置されます。NetworkedArticulationDriveArticulation bodyの Drive プロパティをネットワーク化します。ArticulationBodyValueReaderSingleAxisArticulation bodyの値を読み込んで、Value Provider システムで使用することができます。ArticulationBodySingleAxisSoftSnap駆動プロパティを特定のポイントにスナップするように動的に設定します。例:ダイヤルのノッチ、引き出しのソフトクローズ、レバーの固定有効位置などです。
ローカル補間
NetworkArticulationBodyはNetworkTransformを継承し、安定したローカル補間を提供します。この機能を実現するためには、補間対象を設定し、咬合体の階層を反映させる必要があります。構造
Root:NetworkObject, ArticulationBody, NetworkArticulationBodyRoot, InteractableBase.Visuals:Articulation systemと同じ階層に従います。
Child:ArticulationBody, NetworkArticulationBody, オプション:BodyProperties* ArticulationBodySoftSnap, ArticulationBodyValueReaderSingleAxis) *Child: (オプション)ArticulationBody, NetworkArticulationBody, ArticulationBodyValueReaderSingleAxis)Child: (任意。上記と同じコンポーネント)
*Hotspot:ホットスポット, トリガーコライダー, GrabbableArticulation.
BodyPropertiesコンポーネントはHotspotの下に少なくとも一度は必要です。各Hotspotは親の中の最初のものを探します。
アーティキュレーションボディをリジッドボディに接続する
もし、
Articulationをrigidbodyに接続する必要がある場合は、Joint(例:ConfigurableJoint) を使って行う必要があります。既知の問題
Compute Parent AnchorをArticulation bodiesに使用しないでください。初期状態でない場合、クライアントによって異なることがあります。Articulation bodiesのバグにより、articulation hierarchy のインデックスが異なります。正しく同期させるためには、クライアント間で同じインデックスが必要です。一時的な修正はNetworkArticulationBodyRoot.Spawned()に実装されています。これは正しい順番に並べ替えます。- この「ハックフィックス」は、
rigidbodyとarticulation bodiesの間の Configurable Joints を切断してしまいます(例: Shooting モジュールの Pistol)。そのため、このオブジェクトでは順番を変更することはできません。
モジュール
オブジェクトの引き寄せ
グラブボタンを押しながら、オブジェクトを指差し、手を上にフリックすることで、オブジェクトを自分の方に引き寄せることができます。
要件
HandToolにあるObjectPullCollectorというプレハブ。HotspotにあるObjectPullBodyコンポーネント、またGrabbableBaseコンポーネント (GrabbableRigidbody) が必要です。
Instant Camera
インスタントカメラでは、より複雑なインタラクタブルオブジェクト(追加入力)と、ネットワーク経由で大きなデータを送信するための OnReliableData コールバック関数が紹介されています。
InstantCameraInteractable: カメラのレンダーテクスチャーをキャプチャして写真を撮ります。そのテクスチャのプリントアウトを生成します。InstantCameraPrintout: シェーダーを使用してフェードインするまで、テクスチャーの到着を待ちます。さらに、写真が最初に撮影された後に参加したクライアントには再送信されます。InstantCameraPicture.shader: 画像受信後にフェードインするためのシンプルなサーフェイスシェーダ
ホワイトボード
描画可能なホワイトボードです。
WhiteboardSurface: 描画する表面。現在の状態をRendertextureとして保存します。WhiteboardMarker: インタラクタブル。サーフェス(境界)を通して見て、近づいたらストロークを描くように指示します。
シェーダー
WhiteboardFill: ホワイトボードの塗りつぶし。ホワイトボードを与えられたテクスチャで初期化します。WhiteboardStroke: ホワイトボードを与えられたテクスチャで初期化します。レンダリングテクスチャに直線を描きます。WhiteboardStrokeEraser: レンダーテクスチャに直線を描きます。レンダリングテクスチャを直線で消去します。
Observer
VRプレイヤーを観察し、世界と対話するためのマウス/キーボードコントローラです。
要件は以下の通りです。
- Observers は異なる Input rig と Player prefab を使用します (
StartModulebutton.csを参照してください)。 - 操作したいオブジェクトに
IObserverableコンポーネントを追加します。- リジッドボディをドラッグするための
ObserverableGrabRigidbodyコンポーネント。 ObservableArticulationBodyButtonは一定の力を加えます (トグル可能)ObservableArticulationBodyDrawerは一定時間力を加え、bool の状態を維持します (もう一度押すと力が反転します)。ObservableArticulationBodyLeverは保持している間、力を加えます。ObservableValueProviderOverrideは float の値を直接設定します (オーバーライドのトグルを使用します)。効果を得るにはロジックチェーンにインジェクトする必要があります。
- リジッドボディをドラッグするための
フローティング UI ウィンドウは CreateObserverUi() を使用して、すべての IObserverable コンポーネントに自動的に作成されます。カスタムUIはここで実装することができます。デフォルトのコンポーネントは UI_ObserverItem プレハブで定義されています。
Observer Input
Observer オブジェクトの操作は Input として直接送信されます。(ObserverInput / ObserverInputHandler.cs を参照してください)
入力は以下の要素で構成されます。
- 修正されるコンポーネントを識別するための
NetworkBehaviourId。 Value(現在は float, bool, Vector3 のみ)
UIモジュール
ネットワークに接続された基本的なCursorとUIインタラクションです。Unity の 'Tracked Device Raycaster' と 'InputSystem UI Input Module' を使い、Canvas インタラクションをローカルに登録し、Input として送ります。
PC Rig で使用する場合は 'Graphics Raycaster' も必要です。
LocalController.OnInputUi()ポインタが指すキャンバスを
InputDataController.CanvasBehaviour(NetworkBehaviourId) に保存します。- ポインタがCanvasに衝突した際のワールドの位置を
InputDataController.CursorPositionに保存します。 - ポインタが
NetworkedUiButtonコンポーネントを持つ GameObject に当たった場合は、InputDataController.UiInteractionBehaviour(NetworkBehaviourId) に保存されます。
- ポインタがCanvasに衝突した際のワールドの位置を
UiPointerHandlerが入力を読み取り、ポインタを送信します。- 入力を読み取り、ポインタの位置とインタラクションを対応する
NetworkedCanvasとNetworkedUiButtonに送ります。
- 入力を読み取り、ポインタの位置とインタラクションを対応する
スロットモジュール
スロットシステムは、物体を所定の位置や方向にスムーズに誘導するために使用されます。例えば プラグ、キー、パズルのピース、マガジンなどです。
これを実現するために、GrabbableBase.csは、オブジェクトが掴まれたときに到達したい目標位置をオーバーライドするための様々な elegate とコールバックを持っています。
RotationDelegate GetRotationMethodGrabAndTargetPositionDelegate GetGrabAndTargetPositionMethodUnityAction PreTrackCallbackUnityAction PostTrackCallback
Slottable
Slottableコンポーネントはこれらのコールバックをオーバーライドし、Slotに対する現在のターゲットに応じて、望ましい位置と回転を変更します。
PreTrack: スロットに十分近いかどうかを確認し、後で使用するために2つの要素を計算します。_slotFactorHand: 手からスロットまでの距離。これは、オブジェクトが想定するターゲット位置と回転を決定するために使用されます。_slotFactorObject: スロットからオブジェクトまでの距離。オブジェクトからスロットまでの距離。これはオブジェクトがまだ回転できる場合(>RotationLockThreshold)に、オブジェクトがスロットされ、解放されるべきかどうかを判断するために使用されます。
PostTrack: オブジェクトをドロップすべきかどうかをチェックします。
オブジェクトファクターはオブジェクトの実際の位置で、ハンドファクターは力が加わる前の手の入力位置から決定されます。
Slottable は _slotFactorHand と _slotFactorObject に応じて Slot から目的の位置/回転を取得します。
その他のコンポーネント
GrabbableRidgidbody- 有効なスロットを見つけるための
HotspotCollector。 - ( 任意 )
NetworkColliderCollection:Slotに近づくと無視されるcollidersを指定します (設定されている場合は、スロットとホットスポットと相互作用する手に設定されているコライダーは無視されます)。
Slot
Slotコンポーネントはオブジェクトが範囲内にあるときにどのように動作するかのデータを含んでいます。
Curvesはオブジェクトの位置から希望のスロットの位置まで、位置や回転がどれくらい急激に変化するかを決定します。
Slottables HotspotCollectorに拾われるためには正しいLayerでTriggerとしてマークされたコライダーが最低1つ必要です。
HotspotsToIgnoreHandCollisionWhenGrabbed は、ホットスポットを掴む手を無視するために使用することができます。
これは、スペースが限られていて、Slotを掴んでいる手を無視したい場合に便利です。
例: 片方の手がピストルを持っていて、マガジンとそのスロットが手のすぐ近くにあり、通常であればマガジンが手に衝突してしまいます。
ここにハンドルのホットスポットを追加して、手の衝突も無視するようにします。
UseLineForRange を設定すると、点ではなく線分をスロットのターゲットとして使用することができます。
DropType はスロットに成功したときにオブジェクトがどのようにハンドリングされるかを決定します。
Kinematic: オブジェクトを静的レベルのジオメトリにスロットするときに使用します。オブジェクトはキネマティックになり、再び掴まれない限り移動することができません。Destroy: オブジェクトがデスポーンされます。- (
TeleportOffsite: まだよくテストされていませんが、リジッドボディにスロットされたオブジェクトに使うことができ、再びつかんだときに瞬時に再配置できます。スロットされたオブジェクトを所定の位置にレンダリングするには、受信側に何らかの工夫が必要です)
スロットは、様々なパラメータをチェックすることで、Slottableがスロット可能かどうかを判断します。
SlotTypeはスロットにマッチします。- 他の
Slottableが既に存在しない (またはAllowMultipleObjectsが設定されている)。 - (
Slot.CanSlot()) でさらに制限を加えることができます。
コンポーネント:
NetworkIgnoreCollision(オプション): 割り当てられたコライダをスロットされたオブジェクトに対して無視することができます。これはSlottableが壁を通り抜けられるようにする一方で、手自身やスロットに合わない他のオブジェクトは通り抜けられないようにするために使用できます。
Shootingモジュール
銃のようなインタラクションを実現するためのサンプル実装です。2つの例が提供されています。
ショットガン
- 一度に複数の弾丸を発射
GrabbableRigidbodyを使った重たい物理ボディ- 単発の弾丸をリロードする
ピストル
- アーティキュレーションボディによる手動コッキング
- フルマガジンのリロード
AttachmentRigidbodyを用いたグラブ動作
知っておくと便利なこと
シーンセットアップ
すべてのシーンには Connector.cs と LocalRigSpawner.cs が存在します。もしローカルリグが生成されていなかったり、シーンにあらかじめ定義されていない場合は、リグを生成し (xr または pc)、ランナーを生成して接続します。
Start.scene では、ランナーは SinglePlayer モードで起動します。この方法では、余分な作業をすることなく、ゲームコードに完全にアクセスすることができます。
手とオブジェクトの視覚的補間 :
異なる状況下では、NetworkRigidobdyの通常の補間が Render() で上書きされます。
- 手が何かを掴んだり衝突したりしていない場合は、ローカルコントローラの位置が設定されます。これにより、フォース(
XRObject.cs, UpdateRender())を使用してハンドオブジェクトを移動する際に発生する遅延を回避することができます。 - もし、手がオブジェクトをつかんでいる場合、そのオブジェクトが手の位置を決定するためのデファクトスタンダードとなります。手がオブジェクトを掴んでいる場合、オブジェクトが手の位置を決定するデファクトスタンダードとなります。
- オブジェクトが離された場合、ビジュアルハンドは実際の位置にレンダリングされます (
HandTool.cs Render())。
よくある問題
つかめるオブジェクトがあちこち飛んでいる / 回転がおかしい : 次の設定をチェックして、力が蓄積したり振動したりしていないことを確認します。 プレイ中にエディターで調整できます。
BodyProperties: 各グラブルオブジェクトにはBodyPropertiesコンポーネントがあり、物理プロパティが設定され、同期されます。プロパティ自体はスクリプト可能なオブジェクトで、似たようなオブジェクトで再利用することができます。オブジェクトに持たせたい重量や感触を得るために、値を微調整してください。オブジェクトが制御不能になったとき、位置や回転成分が振動している可能性があります。GrabbableBaseコンポーネントの中で、位置や回転をトラッキングするかどうかをトグルして、問題の原因を突き止め、それに応じて微調整します。- 位置: Mass, Drag, UseGravityk, Velocity Extrapolation.
- 回転: 質量、角度のあるドラッグ、インターリア、トルクの外挿
GrabbableRidgidbody, "Set Center Of Mass To Grab Point" :Grabbable Rigidbodyコンポーネントでは、掴んだときに重心を掴み点に設定するかどうかをトグルします。これはオブジェクトの回転特性に大きな影響を与えるので、異なるBodyPropertiesが必要になるかもしれません。もし、同じBodyPropertiesを持つ同じようなオブジェクトが異なる振る舞いをする場合、これが原因かもしれません。