VR Shared
概述
Fusion VR共享 展示了一個快速且方便的方法,以透過VR開始多人玩家遊戲或應用程式。
共享的或主機端/伺服器拓撲之間的選擇,必須由您的遊戲特性決定。在這個範例中,使用 共享模式。
這個範例的目的在於闡明處理VR裝置的方式,並且提供一個基礎的傳送及拿取的示例。
在您開始之前
- 專案已透過Unity 2021.3.7f1及Fusion 1.1.3f組建599進行開發
- 為了運行範例,首先在PhotonEngine儀表板中建立一個Fusion應用程式帳號,並且將它貼上到即時設定(可從Fusion選單中到達)中的
App Id Fusion
欄位之中。然後載入Launch
場景並且按下Play
。
下載
版本 | 發佈日期 | 下載 | |
---|---|---|---|
1.1.8 | Sep 21, 2023 | Fusion VR Shared 1.1.8 Build 276 |
處理輸入
中繼任務
- 傳送:按下A、B、X、Y,或任何搖桿以顯示一個指標。您將在放開時傳送到任何已接受的目標
- 拿取:首先將您的手放在物件上,然後使用控制器拿取按鈕來拿取它
滑鼠
在專案中包含一個基礎的桌面裝置。它意味著您可以使用滑鼠來進行基礎的互動。
- 移動:按下您的滑鼠左鍵以顯示一個指標。您將在放開時傳送到任何已接受的目標
- 旋轉:持續按下滑鼠右鍵,並且移動滑鼠以旋轉視角
- 拿取:在一個物件上按下您的滑鼠左鍵以拿取它。
連線管理器
在Connection Manager
遊戲物件上安裝NetworkRunner
。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 NetworkRunner
來和Connection 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
代表一個使用者,其中包含多個巢狀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;
}
當Fusion NetworkRunner
輪詢使用者輸入時,HardwareRig
類別更新架構。為了做到這點,它從多種硬體裝置部件來收集輸入參數。
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;
}
除了在FixedUpdateNetwork()
期間移動網路裝置部件位置之外,NetworkRig
元件也處理本機外插:在Render()
期間,針對在這個物件上有輸入授權的本機使用者,處理各種裝置部件的NetworkTransforms
的圖形代表的內插補點目標,將使用最新的本機硬體裝置部件資料來移動。
它確保了本機使用者總是有它們自己的手的最新的位置(以避免潛在的不穩定),就算畫面重新整理率比網路刷新率更高的情況也是如此。
在類別之前的[OrderAfter]
標籤確保將在NetworkTransform
方法之後調用NetworkRig
Render()
,以便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
// 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)
功能)來修改手指位置。
現在,讓我們看一下它被同步的方式。
HandCommand
架構隨手指的位置而更新為HardwareHand
的Update()
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);
}
傳送及運動
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);
}
拿取
概述
這裡的拿取邏輯基於兩個已連線元件,NetworkHandColliderGrabber
及NetworkHandColliderGrabbable
:
NetworkHandColliderGrabber
當硬體手已經在一個可拿取物件上觸發一個拿取動作時觸發拿取及取消拿取NetworkHandColliderGrabbable
以網路變數來同步其他網路及拿取資訊,這樣可拿取物件在各個玩家應用程式上跟隨其拿取者。
注意事項:雖然裝置部件位置及手姿勢處理,與主機端或伺服器拓撲中的操作非常相似,但這裡的處理拿取方式非常特定於共享拓撲,以使其盡可能簡單地讀取
詳細資訊
拿取
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;
}
為了偵測與可拿取物件的碰撞,各個網路手上都有一個簡單的方盒碰撞器。
為了在網路上同步拿取動作,在各個網路手上新增一個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 Grab
方法以要求可拿取的物件來跟隨手(在上述圖表中的(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
為真時,跟隨物件位置是啟用的,也就是當設定了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;
}
請注意,CurrentGrabber
、LocalPositionOffset
及LocalRotationOffset
被宣告為一個已連線變數。它意味著所有玩家將在它們被更新時收到它們的新值(上述圖表中的(2)),以及意味著在改變時回調將允許所有玩家在拿取及取消拿取事件時配置物件(主要在需要時編輯/恢復其運動學上的狀態(上述圖表中的(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;
}
轉譯
類似於NetworkRig
及NetworkHand
,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);
}
第三方
下一步
這裡是一些您可以在這個專案上練習的修改或改進的建議:
- 顯示本機傳送射線給其他玩家(透過使用已連線變數或
INetworkInput
) - 新增聲音功能。如需更多Photon Voice與Fusion整合的細節,請參見這個頁面:https://doc.photonengine.com/en-us/voice/current/getting-started/voice-for-fusion
- 建立一個按鈕以在運行階段繁衍額外的已連線物件