This document is about: FUSION 1
SWITCH TO

VR 共有

Level 4

概要

Fusion VR 共有 では、VR を使ったマルチプレイヤーゲームやアプリケーションを素早く簡単に開始する方法を紹介します。

共有トポロジーかホスト/サーバートポロジーのどちらを選択するかは、ゲームの特殊性によって決定すべきです。このサンプルでは、Sharedモード を使用しています。

このサンプルの目的は、VRリグの処理方法を明確にし、基本的なテレポートとグラブの例を提供することです。

fusion vr shared

はじめに

  • このプロジェクトは、Unity 2021.3.7f1 と Fusion 1.1.2f Build 579 で開発されています。
  • サンプルを実行するには、まず、PhotonEngine Dashboard で Fusion AppId を作成し、リアルタイム設定 (Fusion メニュー) の App Id Fusion 欄にペーストしてください。次に、Launchシーンを読み込んで、Playを押してください。

ダウンロード

バージョン リリース日 ダウンロード
1.1.8 Sep 21, 2023 Fusion VR Shared 1.1.8 Build 276

入力処理

メタクエスト

  • テレポート : A、B、X、Y、またはスティックを押してポインターを表示します。ポインタを離すと、任意のターゲットにテレポートします。
  • 掴む : まず対象物に手をかざし、コントローラーのグラブボタンで掴みます。

マウス

基本的なデスクトップリグは、プロジェクトに含まれています。これは、マウスを使った基本的なインタラクションがあることを意味します。

  • 移動 : マウスを左クリックするとポインタが表示されます。ポインタを離すと、任意のターゲットにテレポートします。
  • 回転 : マウスの右ボタンを押したまま、マウスを動かすと視点が回転します。
  • 掴む : オブジェクト上でマウスを左クリックすると、オブジェクトを掴みます。

接続マネージャー

NetworkRunnerConnection Manager ゲームオブジェクトにインストールされます。Connection Manager は、ゲームの設定と接続の開始を担当しています。

C#

private async void Start()
{
    // Launch the connection at start
    if (connectOnStart) await Connect();
}

 public async Task Connect()
 {
    // Create the scene manager if it does not exist
    if (sceneManager == null) sceneManager = gameObject.AddComponent<NetworkSceneManagerDefault>();

    if (onWillConnect != null) onWillConnect.Invoke();

    // Start or join (depends on gamemode) a session with a specific name
    var args = new StartGameArgs()
    {
        GameMode = mode,
        SessionName = roomName,
        Scene = SceneManager.GetActiveScene().buildIndex,
        SceneManager = sceneManager
    };
    await runner.StartGame(args);
}

INetworkRunnerCallbacks を実装すると、Fusion NetworkRunnerConnection Manager クラスとインタラクションできるようになります。このサンプルでは、OnPlayerJoinedコールバックを使って、プレイヤーがセッションに参加したときにローカルユーザープレファブを生成しています。

C#

public void OnPlayerJoined(NetworkRunner runner, PlayerRef player)
{
    if (player == runner.LocalPlayer)
    {
        // Spawn the user prefab for the local user
        NetworkObject networkPlayerObject = runner.Spawn(userPrefab, position: transform.position, rotation: transform.rotation, player, (runner, obj) => {
        });
    }
}

作成したプレイヤーオブジェクトを切断時に確実に破棄するには、プレハブのNetworkObjectで「Shared Mode Settings > Destroy when state authority leaves」にチェックが入っていることを確認してください。

リグ

概要

没入型アプリケーションでは、リグはユーザーを表現するために必要なすべての可動部、通常は両手、頭、プレイエリア(例えばユーザーがテレポートしたときに移動できるパーソナルスペースです)を記述します。

ネットワークセッション中、すべてのユーザーはネットワーク化されたリグによって表現され、その様々なパーツの位置はネットワーク上で同期されます。

fusion vr shared rigs logic

リグパーツがどのように編成され、同期されるかについては、いくつかのアーキテクチャが可能であり、有効です。ここでは、ユーザーを一つの NetworkObject で表現し、リグパーツごとにいくつかの NetworkTransforms をネストしています。

ローカルユーザーを表すネットワークリグの場合、このリグはハードウェア入力によって駆動される必要があります。このプロセスを単純化するために、ネットワークに接続されていない、別のリグを作成しました。ハードウェアの入力を収集するために、クラシックなUnityコンポーネントを使用します(TrackedPoseDriverなど)。

詳細

リグ

リグを動かす全てのパラメータ(空間での位置や手のポーズ)は RigInput 構造体に含まれています。

C#

public struct RigInput : INetworkInput
{
    public Vector3 playAreaPosition;
    public Quaternion playAreaRotation;
    public Vector3 leftHandPosition;
    public Quaternion leftHandRotation;
    public Vector3 rightHandPosition;
    public Quaternion rightHandRotation;
    public Vector3 headsetPosition;
    public Quaternion headsetRotation;
    public HandCommand leftHandCommand;
    public HandCommand rightHandCommand;
}

HardwareRig クラスは、Fusion NetworkRunner がユーザの入力をポーリングするときに、その構造を更新します。そのために、様々なハードウェアリグパーツから入力パラメータを収集します。

C#


public virtual void OnInput(NetworkRunner runner, NetworkInput input)
{
    RigInput rigInput = PrepareRigInput();
    input.Set(rigInput);
}

protected virtual RigInput PrepareRigInput()
{
    RigInput rigInput = new RigInput();
    rigInput.playAreaPosition = transform.position;
    rigInput.playAreaRotation = transform.rotation;
    rigInput.leftHandPosition = leftHand.transform.position;
    rigInput.leftHandRotation = leftHand.transform.rotation;
    rigInput.rightHandPosition = rightHand.transform.position;
    rigInput.rightHandRotation = rightHand.transform.rotation;
    rigInput.headsetPosition = headset.transform.position;
    rigInput.headsetRotation = headset.transform.rotation;
    rigInput.leftHandCommand = leftHand.handCommand;
    rigInput.rightHandCommand = rightHand.handCommand;
    return rigInput;
}

共有モードでは、ネットワーク Input インターフェースを使用することは必須では ありません。しかし、こうすることで、後でホストモードやサーバーモードへの移行が必要になったときに、コードのリファクタリングを簡単にすることができます。

そして、ローカルユーザーに関連付けられたネットワークリグがこの入力を受け取り、一致するハードウェアリグパーツから来る入力パラメータに単純に従うように、すべてのネットワークリグパーツを設定します。

ユーザープレファブにある NetworkRig コンポーネントは、ネストされたすべてのリグパーツに対してこのトラッキングを管理します。

共有モードでは、この NetworkRig への入力の転送は、ローカルユーザーである状態権限者のみで行われます。プロキシ(他のプレイヤーのアプリケーションにあるこのプレイヤーオブジェクトのインスタンス)に変更が反映されるようにするために、他のことを行う必要があります。

  • リグパーツの位置と回転については、リグパーツには NetworkTransform コンポーネントがあり、 Transform の位置や回転が更新されたときにこの同期をすでに処理します。
  • 手のポーズのようなアプリケーション固有のデータには、ネットワーク化された変数([Networked]タグの付いた属性)が設定され、その値の変更時にコールバックがトリガーされて、新しい値が処理されます。

C#

// As we are in shared topology, having the StateAuthority means we are the local user
public bool IsLocalNetworkRig => Object.HasStateAuthority;
public override void Spawned()
{
    base.Spawned();
    if (IsLocalNetworkRig)
    {
        hardwareRig = FindObjectOfType<HardwareRig>();
        if (hardwareRig == null) Debug.LogError("Missing HardwareRig in the scene");
    }
}

public override void FixedUpdateNetwork()
        {
            base.FixedUpdateNetwork();
            // update the rig at each network tick
            if (GetInput<RigInput>(out var input))
            {
                ApplyInputToRigParts(input);
                ApplyInputToHandPoses(input);
            }
        }

protected virtual void ApplyInputToRigParts(RigInput input)
        {
            transform.position = input.playAreaPosition;
            transform.rotation = input.playAreaRotation;
            leftHand.transform.position = input.leftHandPosition;
            leftHand.transform.rotation = input.leftHandRotation;
            rightHand.transform.position = input.rightHandPosition;
            rightHand.transform.rotation = input.rightHandRotation;
            headset.transform.position = input.headsetPosition;
            headset.transform.rotation = input.headsetRotation;
        }

protected virtual void ApplyInputToHandPoses(RigInput input)
        {
            // we update the hand pose info. It will trigger on network hands OnHandCommandChange on all clients, and update the hand representation accordingly
            leftHand.HandCommand = input.leftHandCommand;
            rightHand.HandCommand = input.rightHandCommand;
        }

NetworkRig コンポーネントは、FixedUpdateNetwork() の間にネットワークリグパーツの位置を移動する以外に、ローカルの外挿も処理します。Render() 中に、このオブジェクトに対する権限を持つローカルユーザに対して、様々なリグパーツの NetworkTransforms のグラフィック表示を処理する内挿ターゲットが、最新のローカルハードウェア リグパーツデータを用いて移動されます。

画面のリフレッシュレートがネットワークのティックレートよりも高い場合でも、ローカルユーザーが自分の手の位置を常に最新に保つことを保証します(潜在的な不安を避けるため)。

クラスの前の [OrderAfter] タグは、NetworkRigRender()NetworkTransform のメソッドの後に呼ばれることを保証するものです。これにより、NetworkRigNetworkTransform 自身の補間ターゲットの処理をオーバーライドすることができます。

C#

public override void Render()
{
    base.Render();
    if (IsLocalNetworkRig)
    {
        // Extrapolate for local user :
        // we want to have the visual at the good position as soon as possible, so we force the visuals to follow the most fresh hardware positions
        // To update the visual object, and not the actual networked position, we move the interpolation targets
        networkTransform.InterpolationTarget.position = hardwareRig.transform.position;
        networkTransform.InterpolationTarget.rotation = hardwareRig.transform.rotation;
        leftHand.networkTransform.InterpolationTarget.position = hardwareRig.leftHand.transform.position;
        leftHand.networkTransform.InterpolationTarget.rotation = hardwareRig.leftHand.transform.rotation;
        rightHand.networkTransform.InterpolationTarget.position = hardwareRig.rightHand.transform.position;
        rightHand.networkTransform.InterpolationTarget.rotation = hardwareRig.rightHand.transform.rotation;
        headset.networkTransform.InterpolationTarget.position = hardwareRig.headset.transform.position;
        headset.networkTransform.InterpolationTarget.rotation = hardwareRig.headset.transform.rotation;
    }

ヘッドセット

NetworkHeadset クラスは非常にシンプルで、NetworkRig クラスのヘッドセット NetworkTransform にアクセスするためのものです。

C#

        public class NetworkHeadset : NetworkBehaviour
        {
            [HideInInspector]
            public NetworkTransform networkTransform;
            private void Awake()
            {
                if (networkTransform == null) networkTransform = GetComponent<NetworkTransform>();
            }
        }

NetworkHeadset クラスと同様に、NetworkHand クラスは NetworkRig クラスの手の Network Transform にアクセスするためのクラスです。

手のポーズを同期させるために、HardwareHand クラスに HandCommand というネットワーク構造体を作成しました。

C#

// Structure representing the inputs driving a hand pose
[System.Serializable]
public struct HandCommand : INetworkStruct
{
    public float thumbTouchedCommand;
    public float indexTouchedCommand;
    public float gripCommand;
    public float triggerCommand;
    // Optionnal commands
    public int poseCommand;
    public float pinchCommand;// Can be computed from triggerCommand by default
}

この HandCommand 構造体は IHandRepresentation インターフェースで使用され、手のポーズを含む様々な手のプロパティを設定します。NetworkHand は子オブジェクトとして IHandRepresentation を持つことができ、その子オブジェクトに手のポーズデータを転送することができる。

C#

    public interface IHandRepresentation
    {
        public void SetHandCommand(HandCommand command);
        public GameObject gameObject { get; }
        public void SetHandColor(Color color);
        public void SetHandMaterial(Material material);
        public void DisplayMesh(bool shouldDisplay);
        public bool IsMeshDisplayed { get; }
    }

各手にある OSFHandRepresentation クラスは、提供されたハンドアニメーター (ApplyCommand(HandCommand command) function) によって指の位置を変更するために、このインターフェースを実装しています。

fusion vr shared hand representation
fusion vr shared hand animator

では、どのように同期しているのかを見てみましょう。

HandCommand 構造体は、指の位置を HardwareHandUpdate() に取り込んで更新されます。

C#

protected virtual void Update()
{
    // update hand pose
    handCommand.thumbTouchedCommand = thumbAction.action.ReadValue<float>();
    handCommand.indexTouchedCommand = indexAction.action.ReadValue<float>();
    handCommand.gripCommand = gripAction.action.ReadValue<float>();
    handCommand.triggerCommand = triggerAction.action.ReadValue<float>();
    handCommand.poseCommand = handPose;
    handCommand.pinchCommand = 0;
    // update hand interaction
    isGrabbing = grabAction.action.ReadValue<float>() > grabThreshold;
}

NetworkRig FixedUpdateNetwork() において、ローカルユーザーのハンドポーズデータが、他のリグ入力とともに更新されます。

C#

public override void FixedUpdateNetwork()
{
    base.FixedUpdateNetwork();

    // update the rig at each network tick
    if (GetInput<RigInput>(out var input))
    {
        ApplyInputToRigParts(input);
        ApplyInputToHandPoses(input);
    }
}


protected virtual void ApplyInputToHandPoses(RigInput input)
{
    // we update the hand pose info. It will trigger on network hands OnHandCommandChange on all clients, and update the hand representation accordingly
    leftHand.HandCommand = input.leftHandCommand;
    rightHand.HandCommand = input.rightHandCommand;
}

ユーザープレファブの各手にある NetworkHand コンポーネントは、手の表現の更新を管理します。

これを行うために、このクラスは HandCommand というネットワーク構造体を含んでいます。

C#

[Networked(OnChanged = nameof(OnHandCommandChange))]
public HandCommand HandCommand { get; set; }

HandCommand はネットワーク上の変数なので、ネットワーク上の構造が変わるたびに、すべてのクライアントで OnHandCommandChange() がコールバックされ、それに応じて手の表現が更新されます。

C#

public static void OnHandCommandChange(Changed<NetworkHand> changed)
{
    // Will be called on all clients when the local user change the hand pose structure
    // We trigger here the actual animation update
    changed.Behaviour.UpdateHandRepresentationWithNetworkState();
}

C#

void UpdateHandRepresentationWithNetworkState()
{
    if (handRepresentation != null) handRepresentation.SetHandCommand(HandCommand);
}

NetworkRig がリグパーツの位置に対して行うのと同様に、Render() の間、NetworkHand はローカルのハードウェアハンドを使ってハンドポーズの外挿と更新も処理します。

C#

public override void Render()
{
    base.Render();
    if (IsLocalNetworkRig)
    {
        // Extrapolate for local user : we want to have the visual at the good position as soon as possible, so we force the visuals to follow the most fresh hand pose
        UpdateRepresentationWithLocalHardwareState();
    }
}

C#

void UpdateRepresentationWithLocalHardwareState()
{
    if (handRepresentation != null) handRepresentation.SetHandCommand(LocalHardwareHand.handCommand);
}

テレポートとロコモーション

fusion vr shared teleport

各ハードウェアリグハンドにある RayBeamer クラスは、ユーザーがボタンを押したときにレイを表示する役割を担っています。ユーザーがボタンを離したとき、レイのターゲットが有効であれば、イベントが発生します。

C#

   if (onRelease != null) onRelease.Invoke(lastHitCollider, lastHit);

このイベントは、ハードウェアリグにある Rig Locomotion クラスによってリッスンされます。

C#

       beamer.onRelease.AddListener(OnBeamRelease);

次に、リグのテレポートコルーチンを呼び出します。

C#

protected virtual void OnBeamRelease(Collider lastHitCollider, Vector3 position)
{
[...]
    if (ValidLocomotionSurface(lastHitCollider))
    {
        StartCoroutine(rig.FadedTeleport(position));
    }
}

ハードウェアリグの位置を更新し、ハードウェアヘッドセットで利用できる Fader コンポーネントに、テレポート中に視界をフェードイン、フェードアウトするよう依頼します (サイバーシックを回避するため)。

C#

public virtual IEnumerator FadedTeleport(Vector3 position)
{
    if (headset.fader) yield return headset.fader.FadeIn();
    Teleport(position);
    if (headset.fader) yield return headset.fader.WaitBlinkDuration();
    if (headset.fader) yield return headset.fader.FadeOut();
}


public virtual void Teleport(Vector3 position)
{
    Vector3 headsetOffet = headset.transform.position - transform.position;
    headsetOffet.y = 0;
    Vector3 previousPosition = transform.position;
    transform.position = position - headsetOffet;
    if (onTeleport != null) onTeleport.Invoke(previousPosition, transform.position);
}

前述したように、OnInputコールバックにより、ハードウェアのリグ位置の変更はネットワーク上で同期されます。

同じ戦略がリグの回転にも適用され、CheckSnapTurn()がリグの修正をトリガーします。

C#

IEnumerator Rotate(float angle)
{
    timeStarted = Time.time;
    rotating = true;
    yield return rig.FadedRotate(angle);
    rotating = false;
}

public virtual IEnumerator FadedRotate(float angle)
{
    if (headset.fader) yield return headset.fader.FadeIn();
    Rotate(angle);
    if (headset.fader) yield return headset.fader.WaitBlinkDuration();
    if (headset.fader) yield return headset.fader.FadeOut();
}

public virtual void Rotate(float angle)
{
    transform.RotateAround(headset.transform.position, transform.up, angle);
}

掴む

fusion vr shared grab

概要

この掴み方のロジックは、2つのネットワークコンポーネント NetworkHandColliderGrabberNetworkHandColliderGrabbable をベースにしています。

  • NetworkHandColliderGrabber は、ハードウェアハンドが掴めるオブジェクトに対して掴みアクションを起こしたときに、掴んだり離したりするトリガーとなります。
  • NetworkHandColliderGrabbable はネットワーク変数でグラブ情報をネットワークに同期させ、プレイヤーのアプリケーションで掴めるオブジェクトが掴むプレイヤーに追従するようにします。

注:リグのパーツの位置や手のポーズの処理は、ホストやサーバーのトポロジーで行われるものと非常に似ていますが、ここでの掴みの処理方法は、できるだけ読みやすくするために、共有トポロジーに非常に特化されています

このページでは、ネットワークリグに強く結び付けられた、非常にシンプルで実装が容易なグラブ(掴み)システムについて説明します。 ハードウェア リグに依存する代替実装は、次の場所にあります: VR 共有 - ローカル リグ グラビング

詳細

fusion vr shared remote grab grabbing logic

掴む

各手にある HardwareHand クラスは、更新のたびに isGrabbing bool を更新します:この bool は、ユーザーがグリップボタンを押したときにtrueになります。 updateGrabWithAction ブールは、マウスとキーボードで操作できるデスクトップリグをサポートするために使用されることに注意してください(このブールはデスクトップモードでは False に、VR モードでは True に設定しなければなりません)。

C#

 protected virtual void Update()
 {
    // update hand pose
    handCommand.thumbTouchedCommand = thumbAction.action.ReadValue<float>();
    handCommand.indexTouchedCommand = indexAction.action.ReadValue<float>();
    handCommand.gripCommand = gripAction.action.ReadValue<float>();
    handCommand.triggerCommand = triggerAction.action.ReadValue<float>();
    handCommand.poseCommand = handPose;
    handCommand.pinchCommand = 0;
    // update hand interaction
    if(updateGrabWithAction) isGrabbing = grabAction.action.ReadValue<float>() > grabThreshold;
}

グラブ可能なオブジェクトとの衝突を検出するために、単純なボックスコライダーが各ネットワークハンドに配置されます。

ネットワーク上でつかむ動作を同期させるために、NetworkHandColliderGrabber クラスが各ネットワークハンドに追加されます。衝突が起きると、メソッド OnTriggerStay(Collider other) が呼び出されます。

まず、コライダーは各ネットワークハンドにあるので、他のプレイヤーではなく、ローカルハンドに関係する衝突に制限する必要があります。

C#

// We only trigger grabbing for our local hands
if (!hand.IsLocalNetworkRig || !hand.LocalHardwareHand) return;

次に、オブジェクトがすでに掴まれているかどうかをチェックします。本サンプルでは、簡略化のため、複数個の把持は許可していません。

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;
}

そして、以下のことを確認します。

  • 衝突したオブジェクトは掴むことができる (それは NetworkHandColliderGrabbable コンポーネントを持っている)
  • ユーザがグリップボタンを押した

これらの条件が満たされた場合、NetworkHandColliderGrabbable メソッドにより、掴まれたオブジェクトは手に従うように求められます (上図 の (1) の部分)

C#

NetworkHandColliderGrabbable grabbable;
if (lastCheckedCollider == other)
{
    grabbable = lastCheckColliderGrabbable;
}
else
{
    grabbable = other.GetComponentInParent<NetworkHandColliderGrabbable>();
}

// To limit the number of GetComponent calls, we cache the latest checked collider grabbable result
lastCheckedCollider = other;
lastCheckColliderGrabbable = grabbable;
if (grabbable != null)
{
    if (hand.LocalHardwareHand.isGrabbing) Grab(grabbable);
}

掴む動作の同期

本サンプルでは共有モードを使用しているため,すべてのプレイヤーがオブジェクトの状態権限を要求し,掴み状態を記述したネットワークバーを変更することが可能です。そのため、プレイヤーが掴んだオブジェクトを掴もうとしたときに、そのオブジェクトの権限を持っていないことがあり得ます。そこで、NetworkHandColliderGrabbable Grab メソッドは、現在のグラブ(とグラブポイントのオフセット)を保存する前に、まず状態権限を要求します。オブジェクトの位置を追うのは IsGrabbed が true のとき、つまり CurrentGrabber が設定されたときにアクティブになります。

C#

public async void Grab(NetworkHandColliderGrabber newGrabber)
{
    if (onWillGrab != null) onWillGrab.Invoke(newGrabber);

    // Find grabbable position/rotation in grabber referential
    localPositionOffsetWhileTakingAuthority = newGrabber.transform.InverseTransformPoint(transform.position);
    localRotationOffsetWhileTakingAuthority = Quaternion.Inverse(newGrabber.transform.rotation) * transform.rotation;
    grabberWhileTakingAuthority = newGrabber;

    // 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 = localPositionOffsetWhileTakingAuthority;
    LocalRotationOffset = localRotationOffsetWhileTakingAuthority;

    // Update the CurrentGrabber in order to start following position in the FixedUpdateNetwork
    CurrentGrabber = grabberWhileTakingAuthority;
}

CurrentGrabberLocalPositionOffsetLocalRotationOffsetはネットワーク変数として宣言されていることに注意してください。また、on changeコールバックにより、すべてのプレイヤーがgrabとungrabイベント時にオブジェクトを設定することができます(必要に応じて、主に運動状態を編集/復元します(上図*の(3))。

注意: 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;
}

追従

NetworkHandColliderGrabbable FixedUpdateNetwork() では、プレイヤーがオブジェクトの権限を持っていてオブジェクトを掴んでいるとき(上図の*(4)*)、オブジェクトの位置を掴む手に従うように更新されます。そして、その上にある NetworkTransform コンポーネントは、すべてのプレイヤーに対して位置が同期するようにします。

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
    Follow(followingtransform: transform, followedTransform: CurrentGrabber.transform, LocalPositionOffset, LocalRotationOffset);
}

c#

void Follow(Transform followingtransform, Transform followedTransform, Vector3 localPositionOffsetToFollowed, Quaternion localRotationOffsetTofollowed)
{
    followingtransform.position = followedTransform.TransformPoint(localPositionOffsetToFollowed);
    followingtransform.rotation = followedTransform.rotation * localRotationOffsetTofollowed;
}

レンダリング

NetworkRigNetworkHand と同様に、NetworkHandColliderGrabbable は外挿処理を行い、Render() の間に掴んだオブジェクトのビジュアルの位置を、ネットワークのティック間で最新の位置に更新します (*上図では (5) *)。クラス内の様々な [OrderAfter] タグは、 NetworkGrabbble Render()NetworkTransform メソッドの後に呼ばれることを保証し、これらのクラスで NetworkTransform の補間ターゲットの処理をオーバーライドします。

しかし、この外挿は、以前のものと比較して2つの特異性があります。

  • まず、外挿はローカルユーザーに制限されません。オブジェクトが掴まれたとき、すべてのユーザーは、(掴まれたことを記述するネットワーク化されたバーのおかげで)掴まれた手に従うべきであることを「認識」しています。たとえ、掴まれたオブジェクトと掴むプレイヤーのネットワーク位置が少しずれていたとしても、視覚は(プロキシ上でオブジェクトが手の周りにわずかに浮くことを避けるために)一致しなければなりません。
  • 第二に、最高のユーザー体験を提供するために、権限を取得する間に外挿するオプション(デフォルトで有効)が追加されました:これは、権限が受信されるまで掴まれたオブジェクトが静止することを防ぎます(それが非常に短い時間であっても、ユーザーはVRでわずかにそれを知覚することができます)。 そのため、権限を要求している間、掴むプレイヤーと掴むポイントの位置は、これらのデータを使用して特定の外挿を行うために、一時的なローカルバーに格納されます。

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
    Follow(followingtransform: networkTransform.InterpolationTarget.transform, followedTransform: CurrentGrabber.hand.networkTransform.InterpolationTarget.transform, LocalPositionOffset, LocalRotationOffset);
}

void ExtrapolateWhileTakingAuthority()
{
    // No need to extrapolate if the object is not really grabbed
    if (grabberWhileTakingAuthority == null) return;

    // 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
    Follow(followingtransform: networkTransform.InterpolationTarget.transform, followedTransform: grabberWhileTakingAuthority.hand.networkTransform.InterpolationTarget.transform, localPositionOffsetWhileTakingAuthority, localRotationOffsetWhileTakingAuthority);
}

サードパーティー

Back to top