VR Shared - ローカルリググラブ
基本的なVR Sharedのサンプルグラビングロジックとの相違点
このグラブシステムは、VR Sharedの技術サンプルで説明したものと非常によく似ていますが、状況によっては有用ないくつかの相違点があります。
- つかみやすいオブジェクトを検出するコライダーが、ネットワークリグではなく、ハードウェアリグにローカライズされている。
- こうすることで、ネットワークリグがまだ生成されていないときでも使用できるようになります(完全なオフライン状態のように)。
- また、意図的にネットワークリグをハードウェアリグと異なる位置に配置する場合(テレポート時にネットワークリグの位置が平滑化される場合など)にも使用できるようになります。
ダウンロード
このサンプルは、VR Shared page ダウンロードに含まれています。ローカルリググラビングのデモシーンは、Scenes/AlternativeHardwareBasedGrabbingDemo
フォルダにあります。
概要
グラブロジックは、2つのパートに分かれています。
- ハードウェアハンドがグラブ可能なオブジェクト(グラブする側とグラブ可能クラス)に対してグラブ/アングラブのアクションをトリガーしたときに、実際のグラブ/アングラブを検出するローカルパート(非ネットワーク化)。
- ネットワークに接続された部分には、すべてのプレイヤーがグラブの状態を認識できるようにし、グラブするハンドに従った実際の位置変更を管理するクラス(NetworkGrabberとNetworkGrabbable)があります。
- 注:このコードには、オフラインで使用する際に、ローカルパートが以下を自ら管理できるようにするための行がいくつか含まれています。例えば、同じコンポーネントをオフラインのロビーに使用するようなユースケースです。しかし、このドキュメントでは、実際にネットワークで使用することに焦点を当てます。
詳細
グラブ
各手にある HardwareHand
クラスは、更新のたびに isGrabbing
bool を更新します : この bool は、ユーザーがグリップボタンを押したときに true になります。
なお、updateGrabWithAction
ブールは、マウスとキーボードで操作できるリグのバージョンであるデスクトップリグをサポートするために使用されます(このブールは、デスクトップモードでは False
に、VRモードでは True
に設定する必要があります)。
C#
protected virtual void Update()
{
// update hand pose
// (...)
// update hand interaction
if(updateGrabWithAction) isGrabbing = grabAction.action.ReadValue<float>() > grabThreshold;
}
グラブ可能なオブジェクトとの衝突を検出するために、シンプルなボックスコライダーが各ハードウェアハンドに配置されており、このハンドに配置されたGrabber
コンポーネントによって使用されます:衝突が発生すると、メソッドOnTriggerStay(Collider other)
が呼ばれます。
まず、オブジェクトがすでに掴まれているかどうかをチェックする。このサンプルでは、簡略化のため、複数回のグラブを許可していません。
C#
// Exit if an object is already grabbed
if (grabbedObject != null)
{
// It is already the grabbed object or another, but we don't allow shared grabbing here
return;
}
次に、以下のことをチェックします。
- 衝突したオブジェクトが掴めるかどうか(
Grabbable
コンポーネントを持っているか)。 - ユーザーがグリップボタンを押す
これらの条件が満たされた場合、Grabbable
Grab
メソッドにより、掴まれたオブジェクトは手についてくるよう求められます(上図の(1))。
C#
Grabbable grabbable;
if (lastCheckedCollider == other)
{
grabbable = lastCheckColliderGrabbable;
}
else
{
grabbable = other.GetComponentInParent<Grabbable>();
}
// To limit the number of GetComponent calls, we cache the latest checked collider grabbable result
lastCheckedCollider = other;
lastCheckColliderGrabbable = grabbable;
if (grabbable != null)
{
if (hand.isGrabbing) Grab(grabbable);
}
グラブ同期
Grabbable
の Grab()
メソッドは、グラブ位置のオフセットを保存します。
また、NetworkGrabbable.LocalGrab()
の呼び出しにより、ローカルユーザーによるグラブが発生したこと(上図の(2))を NetworkGrabbable
関連コンポーネントに通知します。
C#
public virtual void Grab(Grabber newGrabber, Transform grabPointTransform = null)
{
if (onWillGrab != null) onWillGrab.Invoke(newGrabber);
// Find grabbable position/rotation in grabber referential
localPositionOffset = newGrabber.transform.InverseTransformPoint(transform.position);
localRotationOffset = Quaternion.Inverse(newGrabber.transform.rotation) * transform.rotation;
currentGrabber = newGrabber;
if (networkGrabbable)
{
networkGrabbable.LocalGrab();
}
else
{
// We handle the following if we are not online (online, the DidGrab will be called by the NetworkGrabbable DidGrab, itself called on all clients by HandleGrabberChange when the grabber networked var has changed)
DidGrab();
}
}
NetworkGrabbable
では、LocalGrab()
を呼び出します。
- まず、そのオブジェクトの状態権限を要求します。sそうることで、[Networked] 属性値を格納することができます。
- 状態権限を受信すると、それらの[Networked]属性にグラブ(グラボとオフセット)を説明する詳細を格納する。
c#
public async virtual void LocalGrab()
{
// Ask and wait to receive the stateAuthority to move the object
isTakingAuthority = true;
await Object.WaitForStateAuthority();
isTakingAuthority = false;
// We waited to have the state authority before setting Networked vars
LocalPositionOffset = grabbable.localPositionOffset;
LocalRotationOffset = grabbable.localRotationOffset;
// Update the CurrentGrabber in order to start following position in the FixedUpdateNetwork
CurrentGrabber = grabbable.currentGrabber.networkGrabber;
}
注: WaitForStateAuthority
はヘルパー拡張メソッドです。
c#
public static async Task<bool> WaitForStateAuthority(this NetworkObject o, float maxWaitTime = 8)
{
float waitStartTime = Time.time;
o.RequestStateAuthority();
while (!o.HasStateAuthority && (Time.time - waitStartTime) < maxWaitTime)
{
await System.Threading.Tasks.Task.Delay(1);
}
return o.HasStateAuthority;
}
Networked]varの変更により、すべてのクライアントでOnGrabberChanged
が発生します(上図の(3))。
c#
[Networked(OnChanged = nameof(OnGrabberChanged))]
public NetworkGrabber CurrentGrabber { get; set; }
この中で、LoadOld()
とLoadNew()
の呼び出しによって、CurrentGrabber
の値を以前のものと比較することができます。
c#
// Callback that will be called on all clients on grabber change (grabbing/ungrabbing)
public static void OnGrabberChanged(Changed<NetworkGrabbable> changed)
{
// We load the previous state to find what was the grabber before
changed.LoadOld();
NetworkGrabber previousGrabber = null;
if (changed.Behaviour.CurrentGrabber != null)
{
previousGrabber = changed.Behaviour.CurrentGrabber;
}
// We reload the current state to see the current grabber
changed.LoadNew();
changed.Behaviour.HandleGrabberChange(previousGrabber);
}
こうすることで、すべてのクライアントが関連するときに DidGrab()
と DidUngrab()
を呼び出し、両方のメソッドが Grabbable
DidGrab()
と DidUngrab()
の呼び出しに転送されます(上の図の *(4) *)。
c#
protected virtual void HandleGrabberChange(NetworkGrabber previousGrabber)
{
if (previousGrabber)
{
DidUngrab();
}
if (CurrentGrabber)
{
DidGrab();
}
}
protected virtual void DidGrab()
{
grabbable.DidGrab();
if (onDidGrab != null) onDidGrab.Invoke(CurrentGrabber);
}
protected virtual void DidUngrab()
{
grabbable.DidUngrab();
if (onDidUngrab != null) onDidUngrab.Invoke();
}
こうすることで、Grabbable
コンポーネントがisKinematic値(掴んだときにオブジェクトの物理演算が無効になる)を適切に設定し、リリース速度を適用することができます。
c#
public virtual void DidGrab()
{
// While grabbed, we disable physics forces on the object, to force a position based tracking
if (rb) rb.isKinematic = true;
}
public virtual void DidUngrab()
{
// We restore the default isKinematic state if needed
if (rb) rb.isKinematic = expectedIsKinematic;
// We apply release velocity if needed
if (rb && rb.isKinematic == false && applyVelocityOnRelease)
{
rb.velocity = Velocity;
rb.angularVelocity = AngularVelocity;
}
ResetVelocityTracking();
}
追跡
掴まれている間はオブジェクトの物理が無効になるため、現在のグラバーを追うことは単にその実際の位置へのテレポートになります。
c#
public virtual void Follow(Transform followingtransform, Transform followedTransform, Vector3 localPositionOffsetToFollowed, Quaternion localRotationOffsetTofollowed)
{
followingtransform.position = followedTransform.TransformPoint(localPositionOffsetToFollowed);
followingtransform.rotation = followedTransform.rotation * localRotationOffsetTofollowed;
}
オンライン時は、FixedUpdateNetworkの呼び出し時に以下のコードが呼び出されます(上図の*(5)*)。
c#
public override void FixedUpdateNetwork()
{
// We only update the object position if we have the state authority
if (!Object.HasStateAuthority) return;
if (!IsGrabbed) return;
// Follow grabber, adding position/rotation offsets
grabbable.Follow(followingtransform: transform, followedTransform: CurrentGrabber.transform, LocalPositionOffset, LocalRotationOffset);
}
位置の変更は、グラブユーザー(状態権限者)のクライアントでのみ行われ、その後、NetworkTransformにより、すべてのプレーヤーが位置の更新を受け取るようにします。
レンダリング
Render()
の中で行われる外挿(上図の(6))(NetworkGrabbable
クラスは、必要に応じてNetworkTransform
の外挿をオーバーライドするOrderAfter
指令を持ちます)に関して、2つのケースを処理する必要があります。
- オブジェクトが掴まれている間、すべてのクライアントに対して外挿を行います:オブジェクトの予想位置がわかっているので(手の位置にあるはずです)、グラブ可能なビジュアル(つまり
NetworkTransform
の補間ターゲット)は手のビジュアルの位置にある必要があります。 - オブジェクトの権限が要求されている間、グラビングクライアントのための外挿:数フレームの間、
CurrentGrabber
はまだ設定されておらず、権限要求がまだ保留されています([Networked] varは状態の権限を持っている間のみ設定可能です)。そのため、IsGrabbed
はまだtrueを返さず、追跡する実際のNetworkGrabber
はローカルのGrabbable
とGrabber
コンポーネントを通して見つける必要があります。
c#
public override void Render()
{
if (isTakingAuthority && extrapolateWhileTakingAuthority)
{
// If we are currently taking the authority on the object due to a grab, the network info are still not set
// but we will extrapolate anyway (if the option extrapolateWhileTakingAuthority is true) to avoid having the grabbed object staying still until we receive the authority
ExtrapolateWhileTakingAuthority();
return;
}
// No need to extrapolate if the object is not grabbed
if (!IsGrabbed) return;
// Extrapolation: Make visual representation follow grabber, adding position/rotation offsets
// We extrapolate for all users: we know that the grabbed object should follow accuratly the grabber, even if the network position might be a bit out of sync
grabbable.Follow(followingtransform: networkTransform.InterpolationTarget.transform, followedTransform: CurrentGrabber.hand.networkTransform.InterpolationTarget.transform, LocalPositionOffset, LocalRotationOffset);
}
protected virtual void ExtrapolateWhileTakingAuthority()
{
// No need to extrapolate if the object is not really grabbed
if (grabbable.currentGrabber == null) return;
NetworkGrabber networkGrabber = grabbable.currentGrabber.networkGrabber;
// Extrapolation: Make visual representation follow grabber, adding position/rotation offsets
// We use grabberWhileTakingAuthority instead of CurrentGrabber as we are currently waiting for the authority transfer: the network vars are not already set, so we use the temporary versions
grabbable.Follow(followingtransform: networkTransform.InterpolationTarget.transform, followedTransform: networkGrabber.hand.networkTransform.InterpolationTarget.transform, grabbable.localPositionOffset, grabbable.localRotationOffset);
}
Back to top