This document is about: FUSION 2
SWITCH TO

VR Shared

Level 4

概要

Fusion VR Sharedは、VRを使用したマルチプレイヤーゲームやアプリケーションを簡単かつ迅速に開始するためのアプローチを示しています。

共有またはホスト/サーバートポロジーの選択は、ゲームの特性によって決定されるべきです。このサンプルでは、共有モードが使用されています。

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

Fusion VR Shared

始める前に

  • このプロジェクトは、Unity 2021.3およびFusion 2.0で開発されています。
  • サンプルを実行するには、まずPhotonEngine DashboardでFusion AppIdを作成し、それをリアルタイム設定のApp Id Fusionフィールドに貼り付けてください(Fusionメニューからアクセスできます)。次に、Launchシーンをロードし、Playを押します。

ダウンロード

バージョン リリース日 ダウンロード
2.0.5 Feb 25, 2025 Fusion VR Shared 2.0.5 Build 781

入力の処理

Meta Quest

  • テレポート: A、B、X、Yのいずれかのボタンやスティックを押すと、ポインターが表示されます。放すと、受け入れ可能なターゲットにテレポートします。
  • グラブ: まずオブジェクトの上に手を置き、コントローラーのグラブボタンを使用して掴みます。

マウス

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

  • 移動: マウスの左クリックでポインターを表示します。放すと、受け入れ可能なターゲットにテレポートします。
  • 回転: 右マウスボタンを押し続けながらマウスを動かすと、視点が回転します。
  • グラブ: オブジェクトをマウスの左クリックで掴みます。

接続マネージャー

NetworkRunner接続マネージャーゲームオブジェクトにインストールされています。接続マネージャーは、ゲーム設定の構成や接続の開始を担当します。

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 = GameMode.Shared,
        Scene = CurrentSceneInfo(),
        SceneManager = sceneManager
    };

    // Connexion criteria (note: actual project code contains alternative options)
    args.SessionName = roomName;

    await runner.StartGame(args);
}

public virtual NetworkSceneInfo CurrentSceneInfo()
{
    var activeScene = SceneManager.GetActiveScene();
    SceneRef sceneRef = default;

    if (activeScene.buildIndex < 0 || activeScene.buildIndex >= SceneManager.sceneCountInBuildSettings)
    {
        Debug.LogError("Current scene is not part of the build settings");
    }
    else
    {
        sceneRef = SceneRef.FromIndex(activeScene.buildIndex);
    }

    var sceneInfo = new NetworkSceneInfo();
    if (sceneRef.IsValid)
    {
        sceneInfo.AddSceneRef(sceneRef, LoadSceneMode.Single);
    }
    return sceneInfo;
}

INetworkRunnerCallbacksを実装することで、FusionのNetworkRunner接続マネージャークラスと相互作用できるようになります。このサンプルでは、プレイヤーがセッションに参加したときにローカルユーザープレハブを生成するために、OnPlayerJoinedコールバックが使用されています。

C#

public void OnPlayerJoined(NetworkRunner runner, PlayerRef player)
{
    if (player == runner.LocalPlayer && userPrefab != null)
    {
        // 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 > State Authorityが離れたときに破棄」がチェックされていること(または「Allow State Authority Overrideがチェックされていないこと)を確認してください。

リグ

概要

没入型アプリケーションにおいて、リグはユーザーを表現するために必要なすべての可動部分を指します。通常は両手、頭、プレイエリア(ユーザーがテレポートする際に移動できる個人的な空間)を含みます。

ネットワークセッション中は、各ユーザーはネットワーク化されたリグによって表現され、その各部分の位置はネットワークを通じて同期されます。

Fusion VR Shared Rigs Logic

リグの部分がどのように構成され、同期されるかについては、いくつかのアーキテクチャが可能であり、有効です。ここでは、ユーザーが単一のNetworkObjectによって表現され、各リグの部分に対していくつかのネストされたNetworkTransformsがあります。

ローカルユーザーを表すネットワークリグの特定のケースに関しては、このリグはハードウェアの入力によって動かされる必要があります。このプロセスを簡素化するために、「Hardware rig」と呼ばれる別のネットワーク化されていないリグが作成されています。これは、Unity InputDevice APIを使用してハードウェアの入力を収集します。

詳細

リグ

リグを駆動するすべてのパラメーター(空間内の位置や手のポーズ)は、RigState構造体に含まれています。

C#

public struct RigState
{
    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クラスは、要求された際に構造体を更新します。そのために、さまざまなハードウェアリグの部分から入力パラメーターを収集します。

C#

 RigState _rigState = default;

public virtual RigState RigState
{
    get
    {
        _rigState.playAreaPosition = transform.position;
        _rigState.playAreaRotation = transform.rotation;
        _rigState.leftHandPosition = leftHand.transform.position;
        _rigState.leftHandRotation = leftHand.transform.rotation;
        _rigState.rightHandPosition = rightHand.transform.position;
        _rigState.rightHandRotation = rightHand.transform.rotation;
        _rigState.headsetPosition = headset.transform.position;
        _rigState.headsetRotation = headset.transform.rotation;
        _rigState.leftHandCommand = leftHand.handCommand;
        _rigState.rightHandCommand = rightHand.handCommand;
        return _rigState;
    }
}

ローカルユーザーに関連付けられている場合、ユーザープレハブにある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 && 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 for local player. The NetworkTransform will forward this to other players
    if (IsLocalNetworkRig && hardwareRig)
    {
        RigState rigState = hardwareRig.RigState;
        ApplyLocalStateToRigParts(rigState);
        ApplyLocalStateToHandPoses(rigState);
    }
}

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

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

FixedUpdateNetwork()内でネットワークリグの部分の位置を移動させるだけでなく、NetworkRigコンポーネントはローカル外挿も処理します。Render()中、ローカルユーザーがこのオブジェクトの権限を持つ場合、NetworkTransformは最新のローカルハードウェアリグ部分のデータを使用して移動します。

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

[DefaultExecutionOrder(NetworkRig.EXECUTION_ORDER)]およびpublic const int EXECUTION_ORDER = 100;の指示により、NetworkRigRender()NetworkTransformメソッドの後に呼び出されるため、NetworkRigはオブジェクトのNetworkTransformを上書きできます。

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
        RigState rigState = hardwareRig.RigState;

        transform.position = rigState.playAreaPosition;
        transform.rotation = rigState.playAreaRotation;
        leftHand.transform.position = rigState.leftHandPosition;
        leftHand.transform.rotation = rigState.leftHandRotation;
        rightHand.transform.position = rigState.rightHandPosition;
        rightHand.transform.rotation = rigState.rightHandRotation;
        headset.transform.position = rigState.headsetPosition;
        headset.transform.rotation = rigState.headsetRotation;
    }

ヘッドセット

The NetworkHeadset class is very simple : it provides an access to the headset NetworkTransform for the NetworkRig class

C#

[DefaultExecutionOrder(NetworkHeadset.EXECUTION_ORDER)]
public class NetworkHeadset : NetworkBehaviour
{
    public const int EXECUTION_ORDER = NetworkRig.EXECUTION_ORDER + 10;

    [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)関数)を利用して指の位置を変更します。

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

NetworkRigFixedUpdateNetwork()で、手のポーズデータがローカルユーザーにおいて他のリグの部分とともに更新されます。

C#

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

    // Update the rig at each network tick for local player. The NetworkTransform will forward this to other players
    if (IsLocalNetworkRig && hardwareRig)
    {
        RigState rigState = hardwareRig.RigState;
        ApplyLocalStateToHandPoses(rigState);
        ApplyLocalStateToRigParts(rigState);
    }
}


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

ユーザープレハブの各手に配置されているNetworkHandコンポーネントは、手の表現の更新を管理します。

そのために、このクラスにはHandCommandというネットワーク構造が含まれており、変更を検出するためのChangeDetectorが設定されています。

C#

[Networked]
public HandCommand HandCommand { get; set; }

ChangeDetector changeDetector;

public override void Spawned()
{
    base.Spawned();
    changeDetector = GetChangeDetector(ChangeDetector.Source.SimulationState);
}

NetworkRigがリグの部分の位置に対して行うのと同様に、Render()の間にNetworkHandはローカルプレイヤーのために外挿を処理し、ローカルハードウェアの手を使用して手のポーズを更新します。他のすべてのプレイヤーに対しては、changeDetectorを使用してネットワーク構造が変更されたかどうかを確認し、それに応じて手の表現を更新します。

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();
    }
    else
    {
        foreach (var changedNetworkedVarName in changeDetector.DetectChanges(this))
        {
            if (changedNetworkedVarName == nameof(HandCommand))
            {
                // Will be called when the local user change the hand pose structure
                // We trigger here the actual animation update
                UpdateHandRepresentationWithNetworkState();
            }
        }
    }
}


// Update the hand representation each time the network structure HandCommand is updated
void UpdateRepresentationWithLocalHardwareState()
{
    if (handRepresentation != null) handRepresentation.SetHandCommand(LocalHardwareHand.handCommand);
}

// Update the hand representation with local hardware HandCommand
void UpdateHandRepresentationWithNetworkState()
{
    if (handRepresentation != null) handRepresentation.SetHandCommand(HandCommand);
}

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

Fusion VR Shared Teleport

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

C#

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

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

C#

    beamer.onRelease.AddListener(OnBeamRelease);

Then, it calls the rig teleport coroutine…

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

前述のように、ハードウェアリグの位置の変更は、NetworkRigFixedUpdateNetwork()中にネットワークを通じて同期されます。

リグの回転に対しても同様の戦略が適用され、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は、ネットワーク変数を使用して他のネットワークでグラブ情報を同期し、グラブ可能なオブジェクトが各プレイヤーのアプリケーションでそのグラブを行ったオブジェクトに従うようにします。

VRSharedにおけるグラブは、オブジェクトをグラブしているユーザーへの状態権限の移譲に基づいています。
そのため、グラブ可能なオブジェクトのNetworkObjectでは、"Allow State Authority Override"がチェックされていて、"Destroy when state authority leaves"が未チェックであることが重要です(最後のグラバーがオブジェクトを破壊するのを避けるため)

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

このページでは、ネットワークリグに強く結びついた非常にシンプルで実装しやすいグラブシステムについて説明しています。ハードウェアリグにより依存した代替実装については、こちらを参照してください: VR Shared - ローカルリググラビング

詳細

Fusion VR Shared Remote Grab grabbing logic

グラブ

各手に配置されているHardwareHandクラスは、毎回の更新時にisGrabbingブール値を更新します。このブール値は、ユーザーがグリップボタンを押したときに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コンポーネントを持っている)
  • ユーザーがグリップボタンを押していること

これらの条件が満たされると、グラブされたオブジェクトに対して手に従うように要求します(上の図の*(1)*)。これは、NetworkHandColliderGrabbable Grabメソッドによって行われます。

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

Grabbing synchronization

The grab status is saved in a enum :

C#

enum Status {
    NotGrabbed,
    Grabbed,
    WillBeGrabbedUponAuthorityReception
    }
Status status = Status.NotGrabbed;

このサンプルは共有モードを使用しているため、すべてのプレイヤーがオブジェクトに対して状態権限を要求し、グラブの状態を示すネットワーク変数を変更することができます。そのため、プレイヤーがオブジェクトをグラブしようとする際に、そのオブジェクトに対する権限を持っていない可能性があります。

そのため、NetworkHandColliderGrabbable Grabメソッドは、現在のグラバー(およびグラブポイントオフセット)を保存する前に、まず状態権限を要求します。オブジェクトの位置がアクティブになるのは、IsGrabbedtrueの場合、つまり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
    status = Status.WillBeGrabbedUponAuthorityReception;
    isTakingAuthority = true;
    await Object.WaitForStateAuthority();
    isTakingAuthority = false;

    if (status == Status.NotGrabbed)    
        {
        // Object has been already ungrabbed while waiting for state authority
        return;
        }
    if (Object.HasStateAuthority == false)
        {
            Debug.LogError("Unable to receive state authority");
            return;
        }
    status = Status.Grabbed;

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

CurrentGrabberLocalPositionOffset、およびLocalRotationOffsetはネットワーク変数として宣言されていることに注意してください。

FixedUpdateNetwork()はプロキシ(ネットワークオブジェクトに対する状態権限を持たないリモートプレイヤー)では呼び出されないため、ネットワーク変数の変更を検出するために2つのChangeDetectorが必要です。グラブのロジックは状態権限を持つプレイヤーのFixedUpdateNetwork()内で処理され、一方でコールバックはすべてのユーザーによってRender()中に呼び出されます。

補助メソッドTryDetectGrabberChange()は、両方のケースで使用されます。

C#


    ChangeDetector funChangeDetector;
    ChangeDetector renderChangeDetector;

    public override void Spawned()
    {
    [...]
        funChangeDetector = GetChangeDetector(NetworkBehaviour.ChangeDetector.Source.SimulationState);
        renderChangeDetector = GetChangeDetector(NetworkBehaviour.ChangeDetector.Source.SnapshotFrom);
    }



    public override void FixedUpdateNetwork()
    {
        // Check if the grabber changed
        if (TryDetectGrabberChange(funChangeDetector, out var previousGrabber, out var currentGrabber))
        {
            if (previousGrabber)
            {
                // Object ungrabbed
                UnlockObjectPhysics();
            }
            if (currentGrabber)
            {
                // Object grabbed
                LockObjectPhysics();
            }
        }
    [...]
    }

    public override void Render()
    {
        // Check if the grabber changed, to trigger callbacks only (actual grabbing logic in handled in FUN for the state authority)
        // Those callbacks can't be called in FUN, as FUN is not called on proxies, while render is called for everybody
        if (TryDetectGrabberChange(renderChangeDetector, out var previousGrabber, out var currentGrabber))
        {
            if (previousGrabber)
            {
                if (onDidUngrab != null) onDidUngrab.Invoke();
            }
            if (currentGrabber)
            {
                if (onDidGrab != null) onDidGrab.Invoke(currentGrabber);
            }
        }
    [...]
    }


    bool TryDetectGrabberChange(ChangeDetector changeDetector, out NetworkHandColliderGrabber previousGrabber, out NetworkHandColliderGrabber currentGrabber)
    {
        previousGrabber = null;
        currentGrabber = null;
        foreach (var changedNetworkedVarName in changeDetector.DetectChanges(this, out var previous, out var current))
        {
            if (changedNetworkedVarName == nameof(CurrentGrabber))
            {
                var grabberReader = GetBehaviourReader<NetworkHandColliderGrabber>(changedNetworkedVarName);
                previousGrabber = grabberReader.Read(previous);
                currentGrabber = grabberReader.Read(current);
                return true;
            }
        }
        return false;
    }

これは、すべてのプレイヤーが新しい値が更新されるとすぐに受け取ることを意味します(上の図の*(2))。
また、コールバックによって、すべてのプレイヤーがグラブおよびアングラブイベントに基づいてオブジェクトを設定できるようになります(主にその運動状態を編集/復元するため、必要に応じて上の図の
(3)*)。

フォロー

NetworkHandColliderGrabbableFixedUpdateNetwork()では、プレイヤーがオブジェクトの権限を持ち、オブジェクトをグラブしている場合に、オブジェクトの位置がグラブした手に従って更新されます(上の図の*(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(followedTransform: CurrentGrabber.transform, LocalPositionOffset, LocalRotationOffset);}

C#

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

レンダリング

NetworkRigNetworkHandと同様に、NetworkHandColliderGrabbableは外挿を処理し、ネットワークリックの間であってもRender()中にグラブされたオブジェクトの最新の位置を更新します(上の図の*(5)*)。クラス内のさまざまな[DefaultExecutionOrder]およびpublic const int EXECUTION_ORDERの指示により、NetworkGrabbbleRender()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(followedTransform: CurrentGrabber.hand.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(followedTransform: grabberWhileTakingAuthority.hand.transform, localPositionOffsetWhileTakingAuthority, localRotationOffsetWhileTakingAuthority);
}

VR インターポレーションと物理グラビタブル

VRにおいて、非キネマティックオブジェクトのいくつかの動作では、物理計算によって計算されたさまざまな位置間のジャンプを感じることがあります。

これを隠すためにはインターポレーションが必要です。これには2つのオプションがあります:

  1. Fusionが物理計算を実行する場合、レンダリングコール中にインターポレーションを処理します。

  2. Fusionが物理を処理しない場合、Unityのリジッドボディにこのインターポレーションを追加するように設定できます。

  3. の場合、ランナーのゲームオブジェクトにRunnerSimulatePhysics3Dを追加する必要があります。非ネットワーク親(シーンの整理のため)のあるNetworkRigidBody3Dは、インターポレーションが正しく機能するためにSync parentをチェックしないでください。

  4. の場合、RigidBodyコンポーネントでinterpolateオプションを選択するだけです。

このサンプルでは、簡素化のために2番目のオプションが使用されています。

サードパーティ

次に

このプロジェクトで行うことができる変更や改善の提案をいくつか示します:

  • 他のプレイヤーにローカルテレポートレイを表示する(ネットワーク変数またはINetworkInputを使用)
  • ボイス機能を追加する。FusionへのPhoton Voice統合の詳細については、こちらのページを参照してください:https://doc.photonengine.com/en-us/voice/current/getting-started/voice-for-fusion
  • ランタイムで追加のネットワークオブジェクトを生成するボタンを作成する。
Back to top