VR 共有
概要
Fusion VR 共有 では、VR を使ったマルチプレイヤーゲームやアプリケーションを素早く簡単に開始する方法を紹介します。
共有トポロジーかホスト/サーバートポロジーのどちらを選択するかは、ゲームの特殊性によって決定すべきです。このサンプルでは、Sharedモード を使用しています。
このサンプルの目的は、VRリグの処理方法を明確にし、基本的なテレポートとグラブの例を提供することです。
はじめに
- このプロジェクトは、Unity 2021.3.7f1 と Fusion 1.1.2f Build 579 で開発されています。
- サンプルを実行するには、まず、PhotonEngine Dashboard で Fusion AppId を作成し、リアルタイム設定 (Fusion メニュー) の
App Id Fusion
欄にペーストしてください。次に、Launch
シーンを読み込んで、Play
を押してください。
ダウンロード
バージョン | リリース日 | ダウンロード | |
---|---|---|---|
1.1.8 | Sep 21, 2023 | Fusion VR Shared 1.1.8 Build 276 |
入力処理
メタクエスト
- テレポート : A、B、X、Y、またはスティックを押してポインターを表示します。ポインタを離すと、任意のターゲットにテレポートします。
- 掴む : まず対象物に手をかざし、コントローラーのグラブボタンで掴みます。
マウス
基本的なデスクトップリグは、プロジェクトに含まれています。これは、マウスを使った基本的なインタラクションがあることを意味します。
- 移動 : マウスを左クリックするとポインタが表示されます。ポインタを離すと、任意のターゲットにテレポートします。
- 回転 : マウスの右ボタンを押したまま、マウスを動かすと視点が回転します。
- 掴む : オブジェクト上でマウスを左クリックすると、オブジェクトを掴みます。
接続マネージャー
NetworkRunner
は Connection 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 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で「Shared Mode Settings > Destroy when state authority leaves」にチェックが入っていることを確認してください。
リグ
概要
没入型アプリケーションでは、リグはユーザーを表現するために必要なすべての可動部、通常は両手、頭、プレイエリア(例えばユーザーがテレポートしたときに移動できるパーソナルスペースです)を記述します。
ネットワークセッション中、すべてのユーザーはネットワーク化されたリグによって表現され、その様々なパーツの位置はネットワーク上で同期されます。
リグパーツがどのように編成され、同期されるかについては、いくつかのアーキテクチャが可能であり、有効です。ここでは、ユーザーを一つの NetworkObject
で表現し、リグパーツごとにいくつかの NetworkTransforms
をネストしています。
ローカルユーザーを表すネットワークリグの場合、このリグはハードウェア入力によって駆動される必要があります。このプロセスを単純化するために、ネットワークに接続されていない、別のリグを作成しました。ハードウェアの入力を収集するために、クラシックなUnityコンポーネントを使用します(TrackedPoseDriver
など)。
詳細
リグ
リグを動かす全てのパラメータ(空間での位置や手のポーズ)は RigInput
構造体に含まれています。
C#
public struct RigInput : INetworkInput
{
public Vector3 playAreaPosition;
public Quaternion playAreaRotation;
public Vector3 leftHandPosition;
public Quaternion leftHandRotation;
public Vector3 rightHandPosition;
public Quaternion rightHandRotation;
public Vector3 headsetPosition;
public Quaternion headsetRotation;
public HandCommand leftHandCommand;
public HandCommand rightHandCommand;
}
HardwareRig
クラスは、Fusion NetworkRunner
がユーザの入力をポーリングするときに、その構造を更新します。そのために、様々なハードウェアリグパーツから入力パラメータを収集します。
C#
public virtual void OnInput(NetworkRunner runner, NetworkInput input)
{
RigInput rigInput = PrepareRigInput();
input.Set(rigInput);
}
protected virtual RigInput PrepareRigInput()
{
RigInput rigInput = new RigInput();
rigInput.playAreaPosition = transform.position;
rigInput.playAreaRotation = transform.rotation;
rigInput.leftHandPosition = leftHand.transform.position;
rigInput.leftHandRotation = leftHand.transform.rotation;
rigInput.rightHandPosition = rightHand.transform.position;
rigInput.rightHandRotation = rightHand.transform.rotation;
rigInput.headsetPosition = headset.transform.position;
rigInput.headsetRotation = headset.transform.rotation;
rigInput.leftHandCommand = leftHand.handCommand;
rigInput.rightHandCommand = rightHand.handCommand;
return rigInput;
}
共有モードでは、ネットワーク Input
インターフェースを使用することは必須では ありません。しかし、こうすることで、後でホストモードやサーバーモードへの移行が必要になったときに、コードのリファクタリングを簡単にすることができます。
そして、ローカルユーザーに関連付けられたネットワークリグがこの入力を受け取り、一致するハードウェアリグパーツから来る入力パラメータに単純に従うように、すべてのネットワークリグパーツを設定します。
ユーザープレファブにある NetworkRig
コンポーネントは、ネストされたすべてのリグパーツに対してこのトラッキングを管理します。
共有モードでは、この NetworkRig
への入力の転送は、ローカルユーザーである状態権限者のみで行われます。プロキシ(他のプレイヤーのアプリケーションにあるこのプレイヤーオブジェクトのインスタンス)に変更が反映されるようにするために、他のことを行う必要があります。
- リグパーツの位置と回転については、リグパーツには
NetworkTransform
コンポーネントがあり、Transform
の位置や回転が更新されたときにこの同期をすでに処理します。 - 手のポーズのようなアプリケーション固有のデータには、ネットワーク化された変数(
[Networked]
タグの付いた属性)が設定され、その値の変更時にコールバックがトリガーされて、新しい値が処理されます。
C#
// As we are in shared topology, having the StateAuthority means we are the local user
public bool IsLocalNetworkRig => Object.HasStateAuthority;
public override void Spawned()
{
base.Spawned();
if (IsLocalNetworkRig)
{
hardwareRig = FindObjectOfType<HardwareRig>();
if (hardwareRig == null) Debug.LogError("Missing HardwareRig in the scene");
}
}
public override void FixedUpdateNetwork()
{
base.FixedUpdateNetwork();
// update the rig at each network tick
if (GetInput<RigInput>(out var input))
{
ApplyInputToRigParts(input);
ApplyInputToHandPoses(input);
}
}
protected virtual void ApplyInputToRigParts(RigInput input)
{
transform.position = input.playAreaPosition;
transform.rotation = input.playAreaRotation;
leftHand.transform.position = input.leftHandPosition;
leftHand.transform.rotation = input.leftHandRotation;
rightHand.transform.position = input.rightHandPosition;
rightHand.transform.rotation = input.rightHandRotation;
headset.transform.position = input.headsetPosition;
headset.transform.rotation = input.headsetRotation;
}
protected virtual void ApplyInputToHandPoses(RigInput input)
{
// we update the hand pose info. It will trigger on network hands OnHandCommandChange on all clients, and update the hand representation accordingly
leftHand.HandCommand = input.leftHandCommand;
rightHand.HandCommand = input.rightHandCommand;
}
NetworkRig
コンポーネントは、FixedUpdateNetwork()
の間にネットワークリグパーツの位置を移動する以外に、ローカルの外挿も処理します。Render()
中に、このオブジェクトに対する権限を持つローカルユーザに対して、様々なリグパーツの NetworkTransforms
のグラフィック表示を処理する内挿ターゲットが、最新のローカルハードウェア リグパーツデータを用いて移動されます。
画面のリフレッシュレートがネットワークのティックレートよりも高い場合でも、ローカルユーザーが自分の手の位置を常に最新に保つことを保証します(潜在的な不安を避けるため)。
クラスの前の [OrderAfter]
タグは、NetworkRig
の Render()
が 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
// To update the visual object, and not the actual networked position, we move the interpolation targets
networkTransform.InterpolationTarget.position = hardwareRig.transform.position;
networkTransform.InterpolationTarget.rotation = hardwareRig.transform.rotation;
leftHand.networkTransform.InterpolationTarget.position = hardwareRig.leftHand.transform.position;
leftHand.networkTransform.InterpolationTarget.rotation = hardwareRig.leftHand.transform.rotation;
rightHand.networkTransform.InterpolationTarget.position = hardwareRig.rightHand.transform.position;
rightHand.networkTransform.InterpolationTarget.rotation = hardwareRig.rightHand.transform.rotation;
headset.networkTransform.InterpolationTarget.position = hardwareRig.headset.transform.position;
headset.networkTransform.InterpolationTarget.rotation = hardwareRig.headset.transform.rotation;
}
ヘッドセット
NetworkHeadset
クラスは非常にシンプルで、NetworkRig
クラスのヘッドセット NetworkTransform
にアクセスするためのものです。
C#
public class NetworkHeadset : NetworkBehaviour
{
[HideInInspector]
public NetworkTransform networkTransform;
private void Awake()
{
if (networkTransform == null) networkTransform = GetComponent<NetworkTransform>();
}
}
手
NetworkHeadset
クラスと同様に、NetworkHand
クラスは NetworkRig
クラスの手の Network Transform
にアクセスするためのクラスです。
手のポーズを同期させるために、HardwareHand
クラスに HandCommand
というネットワーク構造体を作成しました。
C#
// Structure representing the inputs driving a hand pose
[System.Serializable]
public struct HandCommand : INetworkStruct
{
public float thumbTouchedCommand;
public float indexTouchedCommand;
public float gripCommand;
public float triggerCommand;
// Optionnal commands
public int poseCommand;
public float pinchCommand;// Can be computed from triggerCommand by default
}
この HandCommand
構造体は IHandRepresentation
インターフェースで使用され、手のポーズを含む様々な手のプロパティを設定します。NetworkHand
は子オブジェクトとして IHandRepresentation
を持つことができ、その子オブジェクトに手のポーズデータを転送することができる。
C#
public interface IHandRepresentation
{
public void SetHandCommand(HandCommand command);
public GameObject gameObject { get; }
public void SetHandColor(Color color);
public void SetHandMaterial(Material material);
public void DisplayMesh(bool shouldDisplay);
public bool IsMeshDisplayed { get; }
}
各手にある OSFHandRepresentation
クラスは、提供されたハンドアニメーター (ApplyCommand(HandCommand command)
function) によって指の位置を変更するために、このインターフェースを実装しています。
では、どのように同期しているのかを見てみましょう。
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);
}
掴む
概要
この掴み方のロジックは、2つのネットワークコンポーネント NetworkHandColliderGrabber
と NetworkHandColliderGrabbable
をベースにしています。
NetworkHandColliderGrabber
は、ハードウェアハンドが掴めるオブジェクトに対して掴みアクションを起こしたときに、掴んだり離したりするトリガーとなります。NetworkHandColliderGrabbable
はネットワーク変数でグラブ情報をネットワークに同期させ、プレイヤーのアプリケーションで掴めるオブジェクトが掴むプレイヤーに追従するようにします。
注:リグのパーツの位置や手のポーズの処理は、ホストやサーバーのトポロジーで行われるものと非常に似ていますが、ここでの掴みの処理方法は、できるだけ読みやすくするために、共有トポロジーに非常に特化されています。
詳細
掴む
各手にある HardwareHand
クラスは、更新のたびに isGrabbing
bool を更新します:この bool は、ユーザーがグリップボタンを押したときにtrueになります。
updateGrabWithAction
ブールは、マウスとキーボードで操作できるデスクトップリグをサポートするために使用されることに注意してください(このブールはデスクトップモードでは False
に、VR モードでは True
に設定しなければなりません)。
C#
protected virtual void Update()
{
// update hand pose
handCommand.thumbTouchedCommand = thumbAction.action.ReadValue<float>();
handCommand.indexTouchedCommand = indexAction.action.ReadValue<float>();
handCommand.gripCommand = gripAction.action.ReadValue<float>();
handCommand.triggerCommand = triggerAction.action.ReadValue<float>();
handCommand.poseCommand = handPose;
handCommand.pinchCommand = 0;
// update hand interaction
if(updateGrabWithAction) isGrabbing = grabAction.action.ReadValue<float>() > grabThreshold;
}
グラブ可能なオブジェクトとの衝突を検出するために、単純なボックスコライダーが各ネットワークハンドに配置されます。
ネットワーク上でつかむ動作を同期させるために、NetworkHandColliderGrabber
クラスが各ネットワークハンドに追加されます。衝突が起きると、メソッド OnTriggerStay(Collider other)
が呼び出されます。
まず、コライダーは各ネットワークハンドにあるので、他のプレイヤーではなく、ローカルハンドに関係する衝突に制限する必要があります。
C#
// We only trigger grabbing for our local hands
if (!hand.IsLocalNetworkRig || !hand.LocalHardwareHand) return;
次に、オブジェクトがすでに掴まれているかどうかをチェックします。本サンプルでは、簡略化のため、複数個の把持は許可していません。
C#
// Exit if an object is already grabbed
if (GrabbedObject != null)
{
// It is already the grabbed object or another, but we don't allow shared grabbing here
return;
}
そして、以下のことを確認します。
- 衝突したオブジェクトは掴むことができる (それは
NetworkHandColliderGrabbable
コンポーネントを持っている) - ユーザがグリップボタンを押した
これらの条件が満たされた場合、NetworkHandColliderGrabbable
メソッドにより、掴まれたオブジェクトは手に従うように求められます (上図 の (1) の部分)
C#
NetworkHandColliderGrabbable grabbable;
if (lastCheckedCollider == other)
{
grabbable = lastCheckColliderGrabbable;
}
else
{
grabbable = other.GetComponentInParent<NetworkHandColliderGrabbable>();
}
// To limit the number of GetComponent calls, we cache the latest checked collider grabbable result
lastCheckedCollider = other;
lastCheckColliderGrabbable = grabbable;
if (grabbable != null)
{
if (hand.LocalHardwareHand.isGrabbing) Grab(grabbable);
}
掴む動作の同期
本サンプルでは共有モードを使用しているため,すべてのプレイヤーがオブジェクトの状態権限を要求し,掴み状態を記述したネットワークバーを変更することが可能です。そのため、プレイヤーが掴んだオブジェクトを掴もうとしたときに、そのオブジェクトの権限を持っていないことがあり得ます。そこで、NetworkHandColliderGrabbable Grab
メソッドは、現在のグラブ(とグラブポイントのオフセット)を保存する前に、まず状態権限を要求します。オブジェクトの位置を追うのは IsGrabbed
が true のとき、つまり CurrentGrabber
が設定されたときにアクティブになります。
C#
public async void Grab(NetworkHandColliderGrabber newGrabber)
{
if (onWillGrab != null) onWillGrab.Invoke(newGrabber);
// Find grabbable position/rotation in grabber referential
localPositionOffsetWhileTakingAuthority = newGrabber.transform.InverseTransformPoint(transform.position);
localRotationOffsetWhileTakingAuthority = Quaternion.Inverse(newGrabber.transform.rotation) * transform.rotation;
grabberWhileTakingAuthority = newGrabber;
// Ask and wait to receive the stateAuthority to move the object
isTakingAuthority = true;
await Object.WaitForStateAuthority();
isTakingAuthority = false;
// We waited to have the state authority before setting Networked vars
LocalPositionOffset = localPositionOffsetWhileTakingAuthority;
LocalRotationOffset = localRotationOffsetWhileTakingAuthority;
// Update the CurrentGrabber in order to start following position in the FixedUpdateNetwork
CurrentGrabber = grabberWhileTakingAuthority;
}
CurrentGrabber
、LocalPositionOffset
、LocalRotationOffset
はネットワーク変数として宣言されていることに注意してください。また、on changeコールバックにより、すべてのプレイヤーがgrabとungrabイベント時にオブジェクトを設定することができます(必要に応じて、主に運動状態を編集/復元します(上図*の(3))。
注意: WaitForStateAuthority
は、ヘルパー拡張メソッドです。
c#
public static async Task<bool> WaitForStateAuthority(this NetworkObject o, float maxWaitTime = 8)
{
float waitStartTime = Time.time;
o.RequestStateAuthority();
while (!o.HasStateAuthority && (Time.time - waitStartTime) < maxWaitTime)
{
await System.Threading.Tasks.Task.Delay(1);
}
return o.HasStateAuthority;
}
追従
NetworkHandColliderGrabbable
FixedUpdateNetwork()
では、プレイヤーがオブジェクトの権限を持っていてオブジェクトを掴んでいるとき(上図の*(4)*)、オブジェクトの位置を掴む手に従うように更新されます。そして、その上にある NetworkTransform
コンポーネントは、すべてのプレイヤーに対して位置が同期するようにします。
C#
public override void FixedUpdateNetwork()
{
// We only update the object position if we have the state authority
if (!Object.HasStateAuthority) return;
if (!IsGrabbed) return;
// Follow grabber, adding position/rotation offsets
Follow(followingtransform: transform, followedTransform: CurrentGrabber.transform, LocalPositionOffset, LocalRotationOffset);
}
c#
void Follow(Transform followingtransform, Transform followedTransform, Vector3 localPositionOffsetToFollowed, Quaternion localRotationOffsetTofollowed)
{
followingtransform.position = followedTransform.TransformPoint(localPositionOffsetToFollowed);
followingtransform.rotation = followedTransform.rotation * localRotationOffsetTofollowed;
}
レンダリング
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
- 実行時に追加のネットワークオブジェクトを生成するためのボタンを作成。
- ローカルテレポートレイを他のプレイヤーに表示する(ネットワーク変数または