This document is about: FUSION 1
SWITCH TO

VR ホスト

Level 4

概要

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

共有またはホスト/サーバーのトポロジーを選択するには、ゲームの特殊性によって駆動する必要があります。このサンプルでは、ホストモード を使用しています。

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

Fusion VR Host

はじめに

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

ダウンロード

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

入力処理

メタクエスト

  • テレポート : 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コールバックを使って、プレイヤーがセッションに参加したときにユーザープレファブをホスト上に生成し、同じプレイヤーがセッションを抜けるときに OnPlayerLeft でそれをデスポーンしています。

C#

public void OnPlayerJoined(NetworkRunner runner, PlayerRef player)
{
    // The user's prefab has to be spawned by the host
    if (runner.IsServer)
    {
        Debug.Log($"OnPlayerJoined {player.PlayerId}/Local id: ({runner.LocalPlayer.PlayerId})");
        // We make sure to give the input authority to the connecting player for their user's object
        NetworkObject networkPlayerObject = runner.Spawn(userPrefab, position: transform.position, rotation: transform.rotation, inputAuthority: player, (runner, obj) => {
        });

        // Keep track of the player avatars so we can remove it when they disconnect
        _spawnedUsers.Add(player, networkPlayerObject);
    }
}

// Despawn the user object upon disconnection
public void OnPlayerLeft(NetworkRunner runner, PlayerRef player)
{
    // Find and remove the players avatar (only the host would have stored the spawned game object)
    if (_spawnedUsers.TryGetValue(player, out NetworkObject networkObject))
    {
        runner.Despawn(networkObject);
        _spawnedUsers.Remove(player);
    }
}

UnityのゲームオブジェクトConnection Managerで、「Auto Host or Client」が選択されていることを確認してください。

Fusion VR Host Auto Host or Client
なお、「ホスト」または「クライアント」を選択することも可能で、テスト時などに特定の役割を確実に果たすことができます。

リグ

概要

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

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

Fusion VR Host 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;
    public GrabInfo leftGrabInfo;
    public GrabInfo rightGrabInfo;
}

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

C#

public void OnInput(NetworkRunner runner, NetworkInput input)
{
    // Prepare the input, that will be read by NetworkRig in the FixedUpdateNetwork
    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;
    rigInput.leftGrabInfo = leftHand.grabber.GrabInfo;
    rigInput.rightGrabInfo = rightHand.grabber.GrabInfo;
    input.Set(rigInput);
}

そして、それらの入力を送ったユーザーに関連するネットワーク上のリグがそれらを受け取ります。つまり、ホスト(状態の権威として)とそれらの入力を送ったユーザー(入力の権威として)の両方がそれらを受け取る.他のユーザーは受け取りません(ここではプロキシです)。

これはユーザープレファブにある NetworkRig コンポーネントで、 FixedUpdateNetwork() (FUN) の間に GetInput (これは状態や 入力権限に対する入力のみを返します) を通して行われます。

FUNの間、ネットワーク化されたすべてのリグパーツは、マッチングしたハードウェアリグパーツから来る入力パラメータに従うように設定されます。

ホストモードでは、入力がホストによって処理されると、プロキシに転送され、ユーザーの動きを複製することができます。次のいずれかです。

  • 手のポーズと掴み方に関しては、[Networked] 変数によって処理されます: 状態制御機関 (ホスト) がネットワーク変数の値を変更すると、この値は各ユーザーに複製されます。
  • または、位置と回転に関しては、状態制御機構(ホスト)の NetworkTransform コンポーネントによって処理され、他のユーザへの複製が処理されます。

C#

// As we are in host topology, we use the input authority to track which player is the local user
public bool IsLocalNetworkRig => Object.HasInputAuthority;
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))
    {
        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;
        // 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;

        leftGrabber.GrabInfo = input.leftGrabInfo;
        rightGrabber.GrabInfo = input.rightGrabInfo;
    }
}

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

ヘッドセット

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

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 Host Hand Representation
Fusion VR Host 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))
            {
                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;
                // we update the hand pose info. It will trigger on network hands OnHandCommandChange on each client, and update the hand representation accordingly
                leftHand.HandCommand = input.leftHandCommand;
                rightHand.HandCommand = input.rightHandCommand;

                leftGrabber.GrabInfo = input.leftGrabInfo;
                rightGrabber.GrabInfo = input.rightGrabInfo;
            }
        }

ユーザープレファブの各手にある 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 Host 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;
    transform.position = position - headsetOffet;
}

前述したように、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);
}

掴む

概要

この掴み方のロジックは、2つのパートに分かれています。

  • ハードウェアハンドが掴めるオブジェクトに対して掴みアクションを起こしたときに、実際の掴みと掴み解除を検出するローカルな部分 (GrabberGrabbable クラス)
  • ネットワーク化された部分:すべてのプレイヤーが掴んだ状態を認識できるようにし、掴んだ手を追うために実際の位置変更を管理する (NetworkGrabberNetworkGrabbable クラス).

*注:このコードには、オフラインで使用する際にローカルパートが追従を管理するための行がいくつか含まれています。例えば、同じコンポーネントがオフラインのロビーに使用されるような使用例です。しかし、このドキュメントでは、実際にネットワークで使用することに焦点を当てます。

Fusion VR Host Grabbing Logic

このサンプルでは、2種類の掴み方が用意されています (GrabbableNetworkGrabbable クラスは抽象クラスで、サブクラスでそれぞれのロジックを実装しています)。

  • キネマティックオブジェクトの掴み:その位置は単純に掴んだ手の位置に従います。他のオブジェクトと物理的なインタラクションを持つことはできません。KinematicGrabbableNetworkKinematicGrabbable クラスで実装されています。
  • 物理オブジェクトの掴み:掴んだ手に追従するように速度が変更されます。他のオブジェクトと物理的なインタラクションを持つことができ、発射することもできます。この実装では、サーバーの物理モードがクライアント側の予測に設定されていることが必要ですPhysicsGrabbableNetworkPhysicsGrabbable クラスで実装されています。

*注:運動オブジェクトにリリース速度を与えることが可能ですが、追加のコードが必要になるため、このサンプルでは追加していません(この別種のグラブは、単純なグラブのユースケースのための非常にシンプルなコードベースを示すためにここにあります)

Fusion VR Host Grabbing Classes

詳細

各手にある HardwareHand クラスは、isGrabbing bool を更新するたびに更新します。ユーザーがグリップ ボタンを押すと、bool は true になります。
updateGrabWithAction ブールは、マウスとキーボードで操作できるリグである deskop rig をサポートするために使用されます (このブールは、デスクトップモードでは 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;
}

掴むことが可能なオブジェクトとの衝突を検出するために、単純なボックスコライダーが各ハードウェアハンドに配置され、このハンド上に配置された Grabber コンポーネントによって使用されます。衝突が発生すると、メソッド OnTriggerStay() が呼び出されます。

ホストトポロジーでは、いくつかのティックはフォワードティック(実際の新しいティック)であり、他のティックはリシミュレーション(過去のインスタントのリプレイ)であることに注意してください。掴んだり離したりするのは、現在の位置に対応するforward tickの間だけであるべきです。そのため、OnTriggerStay()は再シミュレーションのティックでは起動しません。

C#

private void OnTriggerStay(Collider other)
{
    if (rig && rig.runner && rig.runner.IsResimulation)
    {
        // We only manage grabbing during forward ticks, to avoid detecting past positions of the grabbable object
        return;
    }

まず、OnTriggerStayはオブジェクトがすでに掴まれているかどうかをチェックします。このサンプルでは、簡略化のため、複数のオブジェクトをつかむことはできません。

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 メソッドによって、掴まれたオブジェクトは手についてくるように要求されます。

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() メソッドは、グラブ位置のオフセットを格納します。

C#

public virtual void Grab(Grabber newGrabber)
{
    // Find grabbable position/rotation in grabber referential
    localPositionOffset = newGrabber.transform.InverseTransformPoint(transform.position);
    localRotationOffset = Quaternion.Inverse(newGrabber.transform.rotation) * transform.rotation;
    currentGrabber = newGrabber;
}

同様に、オブジェクトが掴まれなくなったとき、 Grabbable Ungrab() コールはオブジェクトに関するいくつかの詳細情報を保存します。

C#

public virtual void Ungrab()
{
    currentGrabber = null;
    if (networkGrabbable)
    {
        ungrabPosition = networkGrabbable.networkTransform.InterpolationTarget.transform.position;
        ungrabRotation = networkGrabbable.networkTransform.InterpolationTarget.transform.rotation;
        ungrabVelocity = Velocity;
        ungrabAngularVelocity = AngularVelocity;
    }
}

*実際に使用される掴みタイプのサブクラスによって、いくつかの項目は関連性がありません(例えば、掴み解除位置は物理掴みには使用されません)ので注意してください。

掴みに関するすべてのデータ(掴まれたオブジェクトのネットワークID、オフセット、最終的なリリース速度と位置)は、GrabInfo構造体を通して入力転送で共有されます。

C#

    // Store the info describbing a grabbing state
    public struct GrabInfo : INetworkStruct
    {
        public NetworkBehaviourId grabbedObjectId;
        public Vector3 localPositionOffset;
        public Quaternion localRotationOffset;
        // We want the local user accurate ungrab position to be enforced on the network, and so shared in the input (to avoid the grabbable following "too long" the grabber)
        public Vector3 ungrabPosition;
        public Quaternion ungrabRotation;
        public Vector3 ungrabVelocity;
        public Vector3 ungrabAngularVelocity;
    }

入力を構築する際に、掴むユーザーは最新の掴み情報を提供するように要求されます。

C#

public GrabInfo GrabInfo
{
    get
    {
        if (grabbedObject)
        {
            _grabInfo.grabbedObjectId = grabbedObject.networkGrabbable.Id;
            _grabInfo.localPositionOffset = grabbedObject.localPositionOffset;
            _grabInfo.localRotationOffset = grabbedObject.localRotationOffset;

        }
        else
        {
            _grabInfo.grabbedObjectId = NetworkBehaviourId.None;
            _grabInfo.ungrabPosition = ungrabPosition;
            _grabInfo.ungrabRotation = ungrabRotation;
            _grabInfo.ungrabVelocity = ungrabVelocity;
            _grabInfo.ungrabAngularVelocity = ungrabAngularVelocity;
        }

        return _grabInfo;
    }
}

そして、NetworkRig で受信すると、それらを NetworkGrabber GrabInfo [Networked] var に格納します。

そこで、各クライアントで、FixedUpdateNetwork() の間に、クラスは掴み情報が変更されたかどうかを確認します。これは、再シミュレーション中に掴み/放すが再生されるのを避けるために、前方ティックでのみ行われます。これは HandleGrabInfoChange を呼び出すことで行われ、手の前回と今回の掴み状態を比較します。必要であれば、NetworkGrabbable の実際の GrabUngrab メソッドをトリガーします。

新しいオブジェクトを掴むには、まずこのメソッドはネットワーク ID で検索して NetworkGrabbable を見つけ、Object.Runner.TryFindBehaviour で検索します。

C#

void HandleGrabInfoChange(GrabInfo previousGrabInfo, GrabInfo newGrabInfo)
{
    if (previousGrabInfo.grabbedObjectId !=  newGrabInfo.grabbedObjectId)
    {
        if (grabbedObject != null)
        {
            grabbedObject.Ungrab(newGrabInfo);
            grabbedObject = null;
        }
        // We have to look for the grabbed object has it has changed
        NetworkGrabbable newGrabbedObject;

        // If an object is grabbed, we look for it through the runner with its Id
        if (newGrabInfo.grabbedObjectId != NetworkBehaviourId.None && Object.Runner.TryFindBehaviour(newGrabInfo.grabbedObjectId, out newGrabbedObject))
        {
            grabbedObject = newGrabbedObject;
            if (grabbedObject != null)
            {
                grabbedObject.Grab(this, newGrabInfo);
            }
        }
    }
}

実際の掴み、解き、掴むプレイヤーの追従は、選択された掴みタイプによって異なります。

キネマティック掴みタイプ

追従

キネマティック掴みタイプの場合、現在の掴むプレイヤーに従うことは、単にその実際の位置へテレポートすることです。

C#

public void Follow(Transform followingtransform, Transform followedTransform)
{
    followingtransform.position = followedTransform.TransformPoint(localPositionOffset);
    followingtransform.rotation = followedTransform.rotation * localRotationOffset;
}
FixedupdateNetwork

オンライン時には、FixedUpdateNetwork 呼び出し時に以下のコードが呼び出されます。

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

位置の変更はホスト (状態の権限) でのみ行われ、その後 NetworkTransform によってすべてのプレーヤーが位置の更新を受け取るようにします。

レンダリング

Render() (必要に応じて NetworkTransform の補間をオーバーライドする OrderAfter ディレクティブとして NetworkKinematic クラスがあります) の間に行われる外挿について、ここでは2つのケースを処理する必要があります。

  • オブジェクトが掴まれた状態での外挿:オブジェクトの予想位置は分かっており、手の位置と同じであるべきです。そのため、掴めるビジュアル(つまり NetworkTransform の補間ターゲット)は手のビジュアルの位置でなければなりません。
  • オブジェクトが掴まれていないときの外挿:ネットワークトランスフォームの外挿は、オブジェクトが掴まれているときに行われた外挿とまだ同じではありません。そのため、少しの間、外挿を継続しなければなりません(つまり、オブジェクトは掴まれていない位置で静止していなければなりません)、そうしないと、オブジェクトは過去に少しジャンプしてしまいます。

C#

public override void Render()
{
    if (IsGrabbed)
    {
        // Extrapolation: Make visual representation follow grabber visual representation, 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.networkTransform.InterpolationTarget.transform);
    }
    else if (grabbable.ungrabTime != -1)
    {
        if ((Time.time - grabbable.ungrabTime) < ungrabResyncDuration)
        {
            // When the local user just ungrabbed the object, the network transform interpolation is still not the same as the extrapolation
            //  we were doing while the object was grabbed. So for a few frames, we need to ensure that the extrapolation continues
            //  (ie. the object stay still)
            //  until the network transform offers the same visual conclusion that the one we used to do
            // Other ways to determine this extended extrapolation duration do exist (based on interpolation distance, number of ticks, ...)
            networkTransform.InterpolationTarget.transform.position = grabbable.ungrabPosition;
            networkTransform.InterpolationTarget.transform.rotation = grabbable.ungrabRotation;
        }
        else
        {
            // We'll let the NetworkTransform do its normal interpolation again
            grabbable.ungrabTime = -1;
        }
    }
}

注意: 例えば、クライアントがオブジェクトを掴むとき、実際に掴んでから [Networked] 変数が設定された最初のティックの間に、追加のエッジケースのために、いくつかの追加の外挿が行われるかもしれません。手の視覚は、そうなる前に少し (数ミリ秒) 追跡できます

物理的な掴み方のタイプ

追従

キネマティックな掴み方の場合、現在の掴むプレイヤーに従うということは、掴まれたオブジェクトの速度を変え、最終的に掴むプレイヤーに戻るということを意味します。
これは、直接速度を変えるか、力を使って行うかなど、どのような物理を求めるかによって決まります。
サンプルは両方のロジックを示しており、PhysicsGrabbableでそれを選択するオプションがあります。直接速度を変化させる方が簡単な方法です。

C#

void Follow(Transform followedTransform, float elapsedTime)
{
    // Compute the requested velocity to joined target position during a Runner.DeltaTime
    rb.VelocityFollow(target: followedTransform, localPositionOffset, localRotationOffset, elapsedTime);
    // To avoid a too aggressive move, we attenuate and limit a bit the expected velocity
    rb.velocity *= followVelocityAttenuation; // followVelocityAttenuation = 0.5F by default
    rb.velocity = Vector3.ClampMagnitude(rb.velocity, maxVelocity); // maxVelocity = 10f by default
}
FixedUpdateNetwork

物理計算を適切に行うためには、後述の FixedUpdateNetwork の説明で述べるように、ネットワーク入力データが掴む側のクライアントに必要です。そのため、Grab() の際に入力権限を適切に割り当てる必要があります。

C#

public override void Grab(NetworkGrabber newGrabber, GrabInfo newGrabInfo)
{
    grabbable.localPositionOffset = newGrabInfo.localPositionOffset;
    grabbable.localRotationOffset = newGrabInfo.localRotationOffset;

    currentGrabber = newGrabber;
    if (currentGrabber != null)
    {
        lastGrabbingUser = currentGrabber.Object.InputAuthority;
    }

    lastGrabber = currentGrabber;

    DidGrab();

    // We store the precise grabbing tick to be able to determined if we are grabbing during resimulation tick,
    //  where tha actual currentGrabber may have changed in the latest forward ticks
    startGrabbingTick = Runner.Tick;
    endGrabbingTick = -1;
}

FixedUpdateNetwork の間、各クライアントは以下のコードを実行し、掴んだオブジェクトの速度が掴んだ手のところまで移動するようにします。

注意すべき重要な点は、FixedUpdateNetworkはクライアントで呼ばれることです。

  • フォワードティック(初めて計算されるティックで、信頼できる最新のホストデータの後に何が起こるかを予測しようとするもの)の間。
  • resim ティック (新しいデータがサーバから到着したときに再計算される予測ティック。以前の予測ティックと矛盾する可能性がある)

ユーザーがオブジェクトを掴んでいる間は、Follow コードは各ティックに独立して関連しているので、あまり重要ではありません。
しかし、ユーザーが掴んでいたオブジェクトを放すと、resim ティックの間に、オブジェクトがまだ掴まれている間に発生したティックもあれば、まだ掴まれていない間に発生したティックもあります。しかし、Ungrab() の呼び出しで currentGrabber 変数が null に設定されたので、ungrab の前の resim ティックにはもう適しません。
そこで、1ティックの間に実際の掴み状態を確実に知るために、掴みと掴み解除に関連するティックを startGrabbingTickendGrabbingTick という変数に格納します。そして、FixedUpdateNetwork() の中で、再シミュレーションの間、これらの変数が、オブジェクトがこのティックの間に実際につかまれたかどうかを判断するために使用されます。

C#

public override void FixedUpdateNetwork()
{
    if (Runner.IsForward)
    {
        // during forward tick, the IsGrabbed is reliable as it is changed during forward ticks
        //  (more precisely, it is one tick late, due to OnChanged being called AFTER FixedUpdateNetwork,but this way every client, including proxies, can apply the same physics)
        if (IsGrabbed)
        {
            grabbable.Follow(followedTransform: currentGrabber.transform, elapsedTime: Runner.DeltaTime);
        }
    }
    if (Runner.IsResimulation)
    {
        bool isGrabbedDuringTick = false;
        if (startGrabbingTick != -1 && Runner.Tick >= startGrabbingTick)
        {
            if (Runner.Tick < endGrabbingTick || endGrabbingTick == -1)
            {
                isGrabbedDuringTick = true;
            }
        }

        if (isGrabbedDuringTick)
        {
            grabbable.Follow(followedTransform: lastGrabber.transform, elapsedTime: Runner.DeltaTime);
        }

        // For resim, we reapply the release velocity on the Ungrab tick, like it was done in the Forward tick where it occurred first.
        if (endGrabbingTick == Runner.Tick)
        {
            grabbable.rb.velocity = lastUngrabVelocity;
            grabbable.rb.angularVelocity = lastUngrabAngularVelocity;
        }
    }
}
レンダリング

物理計算によって補間された位置の混乱を避けるため、ここでのRender()ロジックは、掴まれたオブジェクトの視覚的位置を手の視覚的位置に強制しないようにしています。

いくつかのオプションが利用可能です(何もしないこともできますが、その場合、例えば衝突したときに手が掴んだオブジェクトを通過してしまうなど、関連する選択肢が生じる可能性があります)。

現在のサンプルの実装では、以下の Render ロジックを使用しています。

  • 掴んだオブジェクトのビジュアルが手のビジュアルの上にあるのではなく、手のビジュアル位置が掴んだオブジェクトのビジュアル位置に従うように強制されます。
  • このため、衝突した場合、実際の手の位置と表示される手の位置が異なることがあります。そこで、現実の手の位置に "ゴースト "の手を表示することで、違和感を感じさせないようにしました。
  • また、衝突時の衝撃を緩和するために、表示されている手の位置と実際の手の位置のズレに比例した振動をコントローラーに与え、わずかな抵抗感を与えています。
  • オブジェクトを放すときに、手の位置をスムーズに復元するための努力はしていません(ただし、必要に応じて追加することができます)。

C#

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

    if (IsGrabbed)
    {
        var handVisual = currentGrabber.hand.networkTransform.InterpolationTarget.transform;
        var grabbableVisual = networkTransform.InterpolationTarget.transform;

        // On remote user, we want the hand to stay glued to the object, even though the hand and the grabbed object may have various interpolation
        handVisual.rotation = grabbableVisual.rotation * Quaternion.Inverse(grabbable.localRotationOffset);
        handVisual.position = grabbableVisual.position - (handVisual.TransformPoint(grabbable.localPositionOffset) - handVisual.position);

        // Add pseudo haptic feedback if needed
        ApplyPseudoHapticFeedback();
    }
}

// Display a ghost" hand at the position of the real life hand when the distance between the representation (glued to the grabbed object, and driven by forces) and the IRL hand becomes too great
//  Also apply a vibration proportionnal to this distance, so that the user can feel the dissonance between what they ask and what they can do
void ApplyPseudoHapticFeedback()
{
    if (pseudoHapticFeedbackConfiguration.enablePseudoHapticFeedback && IsGrabbed && IsLocalPlayerMostRecentGrabber)
    {
        if (currentGrabber.hand.LocalHardwareHand.localHandRepresentation != null)
        {
            var handVisual = currentGrabber.hand.networkTransform.InterpolationTarget.transform;
            Vector3 dissonanceVector = handVisual.position - currentGrabber.hand.LocalHardwareHand.transform.position;
            float dissonance = dissonanceVector.magnitude;
            bool isPseudoHapticNeeded = (isColliding && dissonance > pseudoHapticFeedbackConfiguration.minContactingDissonance);
            currentGrabber.hand.LocalHardwareHand.localHandRepresentation.DisplayMesh(isPseudoHapticNeeded);
            if (isPseudoHapticNeeded)
            {
                currentGrabber.hand.LocalHardwareHand.SendHapticImpulse(amplitude: Mathf.Clamp01(dissonance / pseudoHapticFeedbackConfiguration.maxDissonanceDistance), duration: pseudoHapticFeedbackConfiguration.vibrationDuration);
            }
        }
    }
}

サードパーティー

次に

このプロジェクトで実践できる、いくつかの修正・改善案を紹介します。

  • ローカルテレポートレイを他のプレイヤーに表示する(ネットワーク変数または INetworkInput を使用する)
  • 音声機能を追加する。Photon VoiceとFusionの連携については、こちらのページを参照してください。https://doc.photonengine.com/en-us/voice/current/getting-started/voice-for-fusion
  • 実行時に追加のネットワークオブジェクトを生成するためのボタンを作成。

Changelog

  • Fusion VR Host 1.1.2 Build 5
    • fix to grab/ungrab detection to limit them to Forward ticks
    • fix to a bug when the user tries to swap the grabbing hands
    • added the optionnal PseudoHapticGrabbableRender to display a ghost grabbed object during pseudo-haptic feedback (ie. when ghost IRL hands appear) for physics grabbable objects
    • in NetworkGrabber, use FixedUpdateNetwork instead of OnChanged to have the grabbing info 1 tick earlier
    • remove secondary following type, and set Follow() as virtual in PhysicsGrabbable to allow developpers to implement their own following physics
    • allow proxy user grabbing physics: the input authority is not changed anymore, neither the InterpolationDataSource, but instead the grabbing/ungrabbing ticks are memorized to determine on each clients the grabbing status during resimulations
Back to top