This document is about: FUSION 1
SWITCH TO

VR Host

Level 4

概述

Fusion VR主機端 展示了一個快速且方便的方法,以透過VR開始多人玩家遊戲或應用程式。

共享的或主機端/伺服器拓撲之間的選擇,必須由您的遊戲特性決定。在這個範例中,使用 主機端模式

這個範例的目的在於闡明處理VR裝置的方式,並且提供一個基礎的傳送及拿取的示例。

fusion vr host

在您開始之前

  • 專案已透過Unity 2021.3.7f1及Fusion 1.1.3進行開發
  • 為了運行範例,首先在PhotonEngine儀表板中建立一個Fusion應用程式帳號,並且將它貼上到即時設定(可從Fusion選單中到達)中的App Id Fusion欄位之中。然後載入Launch場景並且按下Play

下載

版本 發佈日期 下載
1.1.8 Sep 21, 2023 Fusion VR Host 1.1.8 Build 278

處理輸入

中繼任務

  • 傳送:按下A、B、X、Y,或任何搖桿以顯示一個指標。您將在放開時傳送到任何已接受的目標
  • 拿取:首先將您的手放在物件上,然後使用控制器拿取按鈕來拿取它

滑鼠

在專案中包含一個基礎的桌面裝置。它意味著您可以使用滑鼠來進行基礎的互動。

  • 移動:按下您的滑鼠左鍵以顯示一個指標。您將在放開時傳送到任何已接受的目標
  • 旋轉:持續按下滑鼠右鍵,並且移動滑鼠以旋轉視角
  • 拿取:在一個物件上按下您的滑鼠左鍵以拿取它。

連線管理器

Connection Manager遊戲物件上安裝NetworkRunnerConnection 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 NetworkRunner來和Connection 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遊戲物件上是否選擇了「自動主機端或客戶端」。

fusion vr host auto host or client
請注意,也可以選擇「主機端」或「客戶端」,以確保比如在測試的時候有一個特定的角色。

裝置

概述

在一個沉浸式的應用程式中,裝置描述了代表一個使用者的,所需的所有可移動部件,通常是雙手、頭,以及遊玩區域(舉例而言,在使用者傳送時,是可以移動的個人空間),

在一個已連線工作階段,每位使用者由一個已連線裝置所代表,其中透過網路來同步它的各個部件位置。

fusion vr host rigs logic

在組織及同步裝置部件方面,有幾種架構是可行且有效的。在此,一個單一NetworkObject代表一個使用者,其中包含多個巢狀NetworkTransforms,每個裝置部件附有一個。

在代表本機玩家的網路裝置的特定案例方面,這個裝置必須由硬體輸入來驅動。為了簡化這個流程,已建立了一個獨立的、非連線的裝置,稱為「Hardware rig」。它使用傳統的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;
}

Fusion NetworkRunner輪詢使用者輸入時,HardwareRig類別更新架構。為了做到這點,它從多種硬體裝置部件來收集輸入參數。

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

然後,與發送這些輸入的使用者相關的已連線裝置接收這些輸入:主機端(作為狀態授權)及已發送這些輸入的使用者(作為輸入授權)都接收這些輸入。其他使用者不接收(在這裡它們是代理)。

它在FixedUpdateNetwork() (FUN)期間,透過GetInput(其只傳回針對狀態及輸入授權的輸入),發生於位於使用者預製件上的NetworkRig元件之中。

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

除了在FixedUpdateNetwork()期間移動網路裝置部件位置之外,NetworkRig元件也處理本機外插:在Render()期間,針對在這個物件上有輸入授權的本機使用者,處理各種裝置部件的NetworkTransforms的圖形代表的內插補點目標,將使用最新的本機硬體裝置部件資料來移動。

它確保了本機使用者總是有它們自己的手的最新的位置(以避免潛在的不穩定),就算螢幕更新率比網路刷新率更高的情況也是如此。

在類別之前的[OrderAfter]標籤確保將在NetworkTransform方法之後調用NetworkRig Render(),以便NetworkRig可以覆寫內插補點目標的原始處理。

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)功能)來修改手指位置。

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

在這個範例中提供兩種不同的拿取類型(GrabbableNetworkGrabbable類別是抽象類別,附有子類別以執行各個特定邏輯):

  • 針對運動學的物件的拿取:它們的位置簡單地跟隨拿取手的位置。它們無法與其他物件有物理互動。在KinematicGrabbableNetworkKinematicGrabbable類別中執行。
  • 針對物理物件的拿取:它們的速度發生變化,從而跟隨拿取手。它們可以與其他物件有物理互動,並且可以被發射。這個執行方式需要伺服器物理模式被設定為客戶端側預測。在PhysicsGrabbableNetworkPhysicsGrabbable類別中執行。

注意事項:雖然可以給予運動學的物件一個脫離速度(以發射它們),在這個範例中沒有新增這個功能,因為它將需要額外的程式碼(而這另一種的拿取,在這裡是為了展示針對簡單的拿取使用案例的一個非常簡單的程式碼庫),並且在這裡也提供物理拿取,因此在任何情況下都會針對這種使用案例提供更合乎邏輯的執行方式及更準確的結果。

fusion vr host grabbing classes

詳細資訊

HardwareHand類別位於各個手上,其在各個更新時更新isGrabbing布林值:當使用者按下底框按鈕時,布林值為真。 請注意,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;
}

為了偵測與可拿取物件的碰撞,各個硬體手上都有一個簡單的方盒碰撞器,其由位於這個手上的Grabber元件所使用:當一個碰撞發生時,調用OnTriggerStay()方法。

請注意,在主機端拓撲中,有些刷新將是向前刷新(實際上的新的刷新),而其他刷新是重新模擬(重新播放過去的片刻)。應該只在與目前位置相應的向前刷新時偵測拿取及取消拿取。所以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;
    }
}

請注意,取決於實際使用的拿取類型子類別,某些欄位可能無關(舉例而言,對於物理拿取,不使用取消拿取位置)。

所有這些關於拿取的資料(被拿取的物件的網路帳號、位移、最終的脫離速度及位置)之後透過拿取資訊架構在輸入傳輸中被共享。

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]變數中儲存它們。

在那裡,在各個客戶端上,在FixedUpdateNetwork()期間,該類別檢查拿取資訊是否已經改變。它只在向前的刷新中完成,以避免在重新模擬期間重新播放拿取/取消拿取。透過調用HandleGrabInfoChange來完成它,以比較手的先前的及目前的拿取狀態。當需要的時候,它接著觸發在NetworkGrabbable上的實際的GrabUngrab方法。

為了拿取一個新的物件,該方法首先以Object.Runner.TryFindBehaviour,以物件的網路帳號來搜尋物件,來找到這個被拿取的NetworkGrabbable

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;
}
固定更新網路

當在線上的時候,在固定更新網路調用期間來調用以下的程式碼

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()NetworkKinematic類別作為一個OrderAfter指示詞以按照需要來覆寫NetworkTransform內插補點)期間完成的外插,在這裡需要處理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;
        }
    }
}

注意事項:可以針對額外的邊際案例來完成一些額外的外推,舉例而言,在客戶端拿取物件時,在實際拿取及第一個刷新之間設定了[已連線]變數:手視覺效果可以先跟隨一點(幾毫秒),然後再跟隨

物理拿取類型

跟隨

針對運動學的拿取類型,跟隨目前的拿取器意味著改變已拿取物件的速度,這樣它最終重新加入拿取器。 可以透過直接改變速度,或是使用力量改變速度,來完成它,這取決於所需的整體物理的類型。 範例描繪了兩種邏輯,在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說明中所解釋的。所以輸入授權在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

  • 在向前刷新期間(第一次來計算刷新,並且其試著預測在最新的可靠的主機端資料之後發生的事情),
  • 同時也在重新模擬刷新期間(當新資料從伺服器到達時重新計算的已預測刷新,可能與之前的已預測刷新相衝突)

當使用者拿取物件時,它沒有很重要,因為Follow程式碼與各個刷新都是獨立相關的。 但是當使用者放開可拿取物件時,在重新模擬刷新期間,一些刷新發生於物件仍然被拿取的時候,而一些刷新則發生於它不再被拿取的時候。但是currentGrabber變數在Ungrab()調用中已經被設定為空,所以它再也不適用於取消拿取之前的重新模擬刷新。 所以,為了確保在刷新期間知道實際的拿取狀態,在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邏輯:

  • 被拿取的物件視覺效果不是停留在手視覺效果上,而是強迫手視覺效果位置跟隨被拿取的物件視覺效果位置
  • 在碰撞的情況下,這可能會導致在現實生活中的手位置和顯示的手位置的一些差異。為了讓它更容易被接受,在現實生活中的手位置上顯示一個「幽靈」手
  • 為了讓使用者感受到這種不和諧(特別是在碰撞期間),控制器發送一個與顯示手及實際手之間的不和諧距離成比例的震動。它提供一個輕微的阻力感。
  • 放開物件時不需要努力平穩地恢復手位置(但可以按照需要來新增它)

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

第三方

下一步

這裡是一些修改及改進的建議,您可以在這個專案上練習:

更改記錄

  • Fusion VR 主機端 1.1.2 組建 5
    • 修復了拿取/取消拿取偵測,以限制它們在向前的刷新
    • 修復了當使用者試著交換拿取手時的錯誤
    • 新增了可選的虛擬手感可拿取物轉譯,以針對物理可拿取物件,在虛擬手感回饋期間(也就是,當幽靈IRL手出現)顯示一個幽靈已拿取物件
    • 在網路拿取器中,使用固定更新網路,而非在改變時,以提早1個刷新取得拿取資訊
    • 移除次要的跟隨類型,並且在物理可拿取物件中設定Follow()為虛擬,以允許開發者來執行他們自己的跟隨物理
    • 允許代理使用者拿取物理:輸入授權不再改變,內插補點資料來源也不再改變,但是取而代之地,記憶拿取/取消拿取刷新,以在重新模擬期間在各個客戶端上確定拿取狀態
Back to top