VR Shared

概要
Fusion VR Sharedは、VRを使用したマルチプレイヤーゲームやアプリケーションを簡単かつ迅速に開始するためのアプローチを示しています。
共有またはホスト/サーバートポロジーの選択は、ゲームの特性によって決定されるべきです。このサンプルでは、共有モードが使用されています。
このサンプルの目的は、VRリグの扱い方を明確にし、基本的なテレポートとグラブの例を提供することです。

始める前に
- このプロジェクトは、Unity 2021.3およびFusion 2.0で開発されています。
- サンプルを実行するには、まずPhotonEngine DashboardでFusion AppIdを作成し、それをリアルタイム設定の
App Id Fusion
フィールドに貼り付けてください(Fusionメニューからアクセスできます)。次に、Launch
シーンをロードし、Play
を押します。
ダウンロード
バージョン | リリース日 | ダウンロード | |
---|---|---|---|
2.0.5 | Feb 25, 2025 | Fusion VR Shared 2.0.5 Build 781 |
入力の処理
Meta Quest
- テレポート: A、B、X、Yのいずれかのボタンやスティックを押すと、ポインターが表示されます。放すと、受け入れ可能なターゲットにテレポートします。
- グラブ: まずオブジェクトの上に手を置き、コントローラーのグラブボタンを使用して掴みます。
マウス
プロジェクトには基本的なデスクトップリグが含まれています。これは、マウスを使用した基本的なインタラクションがあることを意味します。
- 移動: マウスの左クリックでポインターを表示します。放すと、受け入れ可能なターゲットにテレポートします。
- 回転: 右マウスボタンを押し続けながらマウスを動かすと、視点が回転します。
- グラブ: オブジェクトをマウスの左クリックで掴みます。
接続マネージャー
NetworkRunner
は接続マネージャー
ゲームオブジェクトにインストールされています。接続マネージャー
は、ゲーム設定の構成や接続の開始を担当します。
C#
private async void Start()
{
// Launch the connection at start
if (connectOnStart) await Connect();
}
public async Task Connect()
{
// Create the scene manager if it does not exist
if (sceneManager == null) sceneManager = gameObject.AddComponent<NetworkSceneManagerDefault>();
if (onWillConnect != null) onWillConnect.Invoke();
// Start or join (depends on gamemode) a session with a specific name
var args = new StartGameArgs()
{
GameMode = GameMode.Shared,
Scene = CurrentSceneInfo(),
SceneManager = sceneManager
};
// Connexion criteria (note: actual project code contains alternative options)
args.SessionName = roomName;
await runner.StartGame(args);
}
public virtual NetworkSceneInfo CurrentSceneInfo()
{
var activeScene = SceneManager.GetActiveScene();
SceneRef sceneRef = default;
if (activeScene.buildIndex < 0 || activeScene.buildIndex >= SceneManager.sceneCountInBuildSettings)
{
Debug.LogError("Current scene is not part of the build settings");
}
else
{
sceneRef = SceneRef.FromIndex(activeScene.buildIndex);
}
var sceneInfo = new NetworkSceneInfo();
if (sceneRef.IsValid)
{
sceneInfo.AddSceneRef(sceneRef, LoadSceneMode.Single);
}
return sceneInfo;
}
INetworkRunnerCallbacks
を実装することで、FusionのNetworkRunner
が接続マネージャー
クラスと相互作用できるようになります。このサンプルでは、プレイヤーがセッションに参加したときにローカルユーザープレハブを生成するために、OnPlayerJoined
コールバックが使用されています。
C#
public void OnPlayerJoined(NetworkRunner runner, PlayerRef player)
{
if (player == runner.LocalPlayer && userPrefab != null)
{
// Spawn the user prefab for the local user
NetworkObject networkPlayerObject = runner.Spawn(userPrefab, position: transform.position, rotation: transform.rotation, player, (runner, obj) => {
});
}
}
接続が切断された際に作成されたプレイヤーオブジェクトが破棄されることを確認するために、プレハブのNetworkObjectにおいて「Shared Mode Settings > State Authorityが離れたときに破棄」がチェックされていること(または「Allow State Authority Overrideがチェックされていないこと)を確認してください。
リグ
概要
没入型アプリケーションにおいて、リグはユーザーを表現するために必要なすべての可動部分を指します。通常は両手、頭、プレイエリア(ユーザーがテレポートする際に移動できる個人的な空間)を含みます。
ネットワークセッション中は、各ユーザーはネットワーク化されたリグによって表現され、その各部分の位置はネットワークを通じて同期されます。

リグの部分がどのように構成され、同期されるかについては、いくつかのアーキテクチャが可能であり、有効です。ここでは、ユーザーが単一のNetworkObject
によって表現され、各リグの部分に対していくつかのネストされたNetworkTransforms
があります。
ローカルユーザーを表すネットワークリグの特定のケースに関しては、このリグはハードウェアの入力によって動かされる必要があります。このプロセスを簡素化するために、「Hardware rig
」と呼ばれる別のネットワーク化されていないリグが作成されています。これは、Unity InputDevice APIを使用してハードウェアの入力を収集します。
詳細
リグ
リグを駆動するすべてのパラメーター(空間内の位置や手のポーズ)は、RigState
構造体に含まれています。
C#
public struct RigState
{
public Vector3 playAreaPosition;
public Quaternion playAreaRotation;
public Vector3 leftHandPosition;
public Quaternion leftHandRotation;
public Vector3 rightHandPosition;
public Quaternion rightHandRotation;
public Vector3 headsetPosition;
public Quaternion headsetRotation;
public HandCommand leftHandCommand;
public HandCommand rightHandCommand;
}
HardwareRig
クラスは、要求された際に構造体を更新します。そのために、さまざまなハードウェアリグの部分から入力パラメーターを収集します。
C#
RigState _rigState = default;
public virtual RigState RigState
{
get
{
_rigState.playAreaPosition = transform.position;
_rigState.playAreaRotation = transform.rotation;
_rigState.leftHandPosition = leftHand.transform.position;
_rigState.leftHandRotation = leftHand.transform.rotation;
_rigState.rightHandPosition = rightHand.transform.position;
_rigState.rightHandRotation = rightHand.transform.rotation;
_rigState.headsetPosition = headset.transform.position;
_rigState.headsetRotation = headset.transform.rotation;
_rigState.leftHandCommand = leftHand.handCommand;
_rigState.rightHandCommand = rightHand.handCommand;
return _rigState;
}
}
ローカルユーザーに関連付けられている場合、ユーザープレハブにあるNetworkRig
コンポーネントがこれらの入力を要求し、対応するハードウェアリグの部分からの入力データに従って、すべてのネットワーク化されたリグの部分を構成します。
これは、状態権限を持つローカルユーザーのNetworkRig
のみで行われます。これらの変更がプロキシ(他のプレイヤーのアプリケーションでのこのプレイヤーオブジェクトのインスタンス)に複製されることを確実にするために、他にいくつかのことが行われる必要があります。
- リグの部分の位置と回転については、これらのリグの部分に
NetworkTransform
コンポーネントがあり、Transform
の位置または回転が更新されるときに、この同期を処理します。 - 手のポーズのようなアプリケーション固有のデータについては、ネットワーク化された変数(
[Networked]
タグが付けられた属性)が設定され、その値の変更に対してコールバックがトリガーされ、新しい値を処理します。
C#
// As we are in shared topology, having the StateAuthority means we are the local user
public bool IsLocalNetworkRig => Object && Object.HasStateAuthority;
public override void Spawned()
{
base.Spawned();
if (IsLocalNetworkRig)
{
hardwareRig = FindObjectOfType<HardwareRig>();
if (hardwareRig == null) Debug.LogError("Missing HardwareRig in the scene");
}
}
public override void FixedUpdateNetwork()
{
base.FixedUpdateNetwork();
// Update the rig at each network tick for local player. The NetworkTransform will forward this to other players
if (IsLocalNetworkRig && hardwareRig)
{
RigState rigState = hardwareRig.RigState;
ApplyLocalStateToRigParts(rigState);
ApplyLocalStateToHandPoses(rigState);
}
}
protected virtual void ApplyLocalStateToRigParts(RigState rigState)
{
transform.position = rigState.playAreaPosition;
transform.rotation = rigState.playAreaRotation;
leftHand.transform.position = rigState.leftHandPosition;
leftHand.transform.rotation = rigState.leftHandRotation;
rightHand.transform.position = rigState.rightHandPosition;
rightHand.transform.rotation = rigState.rightHandRotation;
headset.transform.position = rigState.headsetPosition;
headset.transform.rotation = rigState.headsetRotation;
}
protected virtual void ApplyLocalStateToHandPoses(RigState rigState)
{
// we update the hand pose info. It will trigger on network hands OnHandCommandChange on all clients, and update the hand representation accordingly
leftHand.HandCommand = rigState.leftHandCommand;
rightHand.HandCommand = rigState.rightHandCommand;
}
FixedUpdateNetwork()
内でネットワークリグの部分の位置を移動させるだけでなく、NetworkRig
コンポーネントはローカル外挿も処理します。Render()
中、ローカルユーザーがこのオブジェクトの権限を持つ場合、NetworkTransform
は最新のローカルハードウェアリグ部分のデータを使用して移動します。
これにより、ローカルユーザーは常に自分の手の最新の位置を保持し(潜在的な不快感を避けるため)、画面のリフレッシュレートがネットワークティックレートよりも高い場合でも、この効果が得られます。
[DefaultExecutionOrder(NetworkRig.EXECUTION_ORDER)]
およびpublic const int EXECUTION_ORDER = 100;
の指示により、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
RigState rigState = hardwareRig.RigState;
transform.position = rigState.playAreaPosition;
transform.rotation = rigState.playAreaRotation;
leftHand.transform.position = rigState.leftHandPosition;
leftHand.transform.rotation = rigState.leftHandRotation;
rightHand.transform.position = rigState.rightHandPosition;
rightHand.transform.rotation = rigState.rightHandRotation;
headset.transform.position = rigState.headsetPosition;
headset.transform.rotation = rigState.headsetRotation;
}
ヘッドセット
The NetworkHeadset
class is very simple : it provides an access to the headset NetworkTransform
for the NetworkRig
class
C#
[DefaultExecutionOrder(NetworkHeadset.EXECUTION_ORDER)]
public class NetworkHeadset : NetworkBehaviour
{
public const int EXECUTION_ORDER = NetworkRig.EXECUTION_ORDER + 10;
[HideInInspector]
public NetworkTransform networkTransform;
private void Awake()
{
if (networkTransform == null) networkTransform = GetComponent<NetworkTransform>();
}
}
ハンド
NetworkHeadset
クラスと同様に、NetworkHand
クラスはNetworkRig
クラスのために手のNetwork Transform
へのアクセスを提供します。
手のポーズを同期させるために、HardwareHand
クラス内にHandCommand
というネットワーク構造が作成されています。
C#
// Structure representing the inputs driving a hand pose
[System.Serializable]
public struct HandCommand : INetworkStruct
{
public float thumbTouchedCommand;
public float indexTouchedCommand;
public float gripCommand;
public float triggerCommand;
// Optionnal commands
public int poseCommand;
public float pinchCommand;// Can be computed from triggerCommand by default
}
このHandCommand
構造は、手のポーズを含むさまざまな手のプロパティを設定するIHandRepresentation
インターフェースで使用されます。NetworkHand
は子としてIHandRepresentation
を持つことができ、手のポーズデータをその子に転送します。
C#
public interface IHandRepresentation
{
public void SetHandCommand(HandCommand command);
public GameObject gameObject { get; }
public void SetHandColor(Color color);
public void SetHandMaterial(Material material);
public void DisplayMesh(bool shouldDisplay);
public bool IsMeshDisplayed { get; }
}
各手に配置されているOSFHandRepresentation
クラスは、このインターフェースを実装し、提供されたハンドアニメーター(ApplyCommand(HandCommand command)
関数)を利用して指の位置を変更します。


では、どのように同期されるか見てみましょう。
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 for local player. The NetworkTransform will forward this to other players
if (IsLocalNetworkRig && hardwareRig)
{
RigState rigState = hardwareRig.RigState;
ApplyLocalStateToHandPoses(rigState);
ApplyLocalStateToRigParts(rigState);
}
}
protected virtual void ApplyLocalStateToHandPoses(RigState rigState)
{
// we update the hand pose info. It will trigger on network hands OnHandCommandChange on all clients, and update the hand representation accordingly
leftHand.HandCommand = rigState.leftHandCommand;
rightHand.HandCommand = rigState.rightHandCommand;
}
ユーザープレハブの各手に配置されているNetworkHand
コンポーネントは、手の表現の更新を管理します。
そのために、このクラスにはHandCommand
というネットワーク構造が含まれており、変更を検出するためのChangeDetector
が設定されています。
C#
[Networked]
public HandCommand HandCommand { get; set; }
ChangeDetector changeDetector;
public override void Spawned()
{
base.Spawned();
changeDetector = GetChangeDetector(ChangeDetector.Source.SimulationState);
}
NetworkRig
がリグの部分の位置に対して行うのと同様に、Render()
の間にNetworkHand
はローカルプレイヤーのために外挿を処理し、ローカルハードウェアの手を使用して手のポーズを更新します。他のすべてのプレイヤーに対しては、changeDetector
を使用してネットワーク構造が変更されたかどうかを確認し、それに応じて手の表現を更新します。
C#
public override void Render()
{
base.Render();
if (IsLocalNetworkRig)
{
// Extrapolate for local user : we want to have the visual at the good position as soon as possible, so we force the visuals to follow the most fresh hand pose
UpdateRepresentationWithLocalHardwareState();
}
else
{
foreach (var changedNetworkedVarName in changeDetector.DetectChanges(this))
{
if (changedNetworkedVarName == nameof(HandCommand))
{
// Will be called when the local user change the hand pose structure
// We trigger here the actual animation update
UpdateHandRepresentationWithNetworkState();
}
}
}
}
// Update the hand representation each time the network structure HandCommand is updated
void UpdateRepresentationWithLocalHardwareState()
{
if (handRepresentation != null) handRepresentation.SetHandCommand(LocalHardwareHand.handCommand);
}
// Update the hand representation with local hardware HandCommand
void UpdateHandRepresentationWithNetworkState()
{
if (handRepresentation != null) handRepresentation.SetHandCommand(HandCommand);
}
テレポートとロコモーション

各ハードウェアリグの手に配置されているRayBeamer
クラスは、ユーザーがボタンを押したときにレイを表示する役割を担っています。ユーザーがボタンを放すと、レイのターゲットが有効であれば、イベントがトリガーされます。
C#
if (onRelease != null) onRelease.Invoke(lastHitCollider, lastHit);
このイベントは、ハードウェアリグに配置されているRig Locomotion
クラスによってリッスンされています。
C#
beamer.onRelease.AddListener(OnBeamRelease);
Then, it calls the rig teleport coroutine…
C#
protected virtual void OnBeamRelease(Collider lastHitCollider, Vector3 position)
{
[...]
if (ValidLocomotionSurface(lastHitCollider))
{
StartCoroutine(rig.FadedTeleport(position));
}
}
これにより、ハードウェアリグの位置が更新され、ハードウェアヘッドセットにあるFader
コンポーネントに対して、テレポート中に視界をフェードインおよびフェードアウトさせるよう要求します(サイバーシックネスを避けるため)。
C#
public virtual IEnumerator FadedTeleport(Vector3 position)
{
if (headset.fader) yield return headset.fader.FadeIn();
Teleport(position);
if (headset.fader) yield return headset.fader.WaitBlinkDuration();
if (headset.fader) yield return headset.fader.FadeOut();
}
public virtual void Teleport(Vector3 position)
{
Vector3 headsetOffet = headset.transform.position - transform.position;
headsetOffet.y = 0;
Vector3 previousPosition = transform.position;
transform.position = position - headsetOffet;
if (onTeleport != null) onTeleport.Invoke(previousPosition, transform.position);
}
前述のように、ハードウェアリグの位置の変更は、NetworkRig
のFixedUpdateNetwork()
中にネットワークを通じて同期されます。
リグの回転に対しても同様の戦略が適用され、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
は、ネットワーク変数を使用して他のネットワークでグラブ情報を同期し、グラブ可能なオブジェクトが各プレイヤーのアプリケーションでそのグラブを行ったオブジェクトに従うようにします。
VRSharedにおけるグラブは、オブジェクトをグラブしているユーザーへの状態権限の移譲に基づいています。
そのため、グラブ可能なオブジェクトのNetworkObject
では、"Allow State Authority Override"がチェックされていて、"Destroy when state authority leaves"が未チェックであることが重要です(最後のグラバーがオブジェクトを破壊するのを避けるため)。
注:リグの部分の位置や手のポーズの処理は、ホストまたはサーバートポロジーで行われるものと非常に似ていますが、ここでのグラブ処理の方法は共有トポロジーに特有であり、できるだけ読みやすくするためです。
詳細

グラブ
各手に配置されているHardwareHand
クラスは、毎回の更新時にisGrabbing
ブール値を更新します。このブール値は、ユーザーがグリップボタンを押したときにtrue
になります。なお、updateGrabWithAction
ブール値はデスクトップリグのサポートに使用され、このリグはマウスとキーボードで操作できるバージョンです(このブール値はデスクトップモードではFalse
に設定し、VRモードではTrue
に設定する必要があります)。
C#
protected virtual void Update()
{
// update hand pose
handCommand.thumbTouchedCommand = thumbAction.action.ReadValue<float>();
handCommand.indexTouchedCommand = indexAction.action.ReadValue<float>();
handCommand.gripCommand = gripAction.action.ReadValue<float>();
handCommand.triggerCommand = triggerAction.action.ReadValue<float>();
handCommand.poseCommand = handPose;
handCommand.pinchCommand = 0;
// update hand interaction
if(updateGrabWithAction) isGrabbing = grabAction.action.ReadValue<float>() > grabThreshold;
}
グラブ可能なオブジェクトとの衝突を検出するために、各ネットワークハンドにはシンプルなボックスコライダーが配置されています。
ネットワーク上でグラブアクションを同期するために、各ネットワークハンドにNetworkHandColliderGrabber
クラスが追加されます。衝突が発生した際、OnTriggerStay(Collider other)
メソッドが呼び出されます。
まず、コライダーが各ネットワークハンドに配置されているため、衝突をローカルハンドに関するものに制限し、他のプレイヤーのハンドには影響を与えないようにする必要があります。
C#
// We only trigger grabbing for our local hands
if (!hand.IsLocalNetworkRig || !hand.LocalHardwareHand) return;
次に、オブジェクトがすでにグラブされているかどうかを確認します。簡素化のために、このサンプルでは複数のグラブは許可されていません。
C#
// Exit if an object is already grabbed
if (GrabbedObject != null)
{
// It is already the grabbed object or another, but we don't allow shared grabbing here
return;
}
次に、以下のことを確認します:
- 衝突したオブジェクトがグラブ可能であること(
NetworkHandColliderGrabbable
コンポーネントを持っている) - ユーザーがグリップボタンを押していること
これらの条件が満たされると、グラブされたオブジェクトに対して手に従うように要求します(上の図の*(1)*)。これは、NetworkHandColliderGrabbable Grab
メソッドによって行われます。
C#
NetworkHandColliderGrabbable grabbable;
if (lastCheckedCollider == other)
{
grabbable = lastCheckColliderGrabbable;
}
else
{
grabbable = other.GetComponentInParent<NetworkHandColliderGrabbable>();
}
// To limit the number of GetComponent calls, we cache the latest checked collider grabbable result
lastCheckedCollider = other;
lastCheckColliderGrabbable = grabbable;
if (grabbable != null)
{
if (hand.LocalHardwareHand.isGrabbing) Grab(grabbable);
}
Grabbing synchronization
The grab status is saved in a enum :
C#
enum Status {
NotGrabbed,
Grabbed,
WillBeGrabbedUponAuthorityReception
}
Status status = Status.NotGrabbed;
このサンプルは共有モードを使用しているため、すべてのプレイヤーがオブジェクトに対して状態権限を要求し、グラブの状態を示すネットワーク変数を変更することができます。そのため、プレイヤーがオブジェクトをグラブしようとする際に、そのオブジェクトに対する権限を持っていない可能性があります。
そのため、NetworkHandColliderGrabbable Grab
メソッドは、現在のグラバー(およびグラブポイントオフセット)を保存する前に、まず状態権限を要求します。オブジェクトの位置がアクティブになるのは、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
status = Status.WillBeGrabbedUponAuthorityReception;
isTakingAuthority = true;
await Object.WaitForStateAuthority();
isTakingAuthority = false;
if (status == Status.NotGrabbed)
{
// Object has been already ungrabbed while waiting for state authority
return;
}
if (Object.HasStateAuthority == false)
{
Debug.LogError("Unable to receive state authority");
return;
}
status = Status.Grabbed;
// We waited to have the state authority before setting Networked vars
LocalPositionOffset = localPositionOffsetWhileTakingAuthority;
LocalRotationOffset = localRotationOffsetWhileTakingAuthority;
// Update the CurrentGrabber in order to start following position in the FixedUpdateNetwork
CurrentGrabber = grabberWhileTakingAuthority;
}
CurrentGrabber
、LocalPositionOffset
、およびLocalRotationOffset
はネットワーク変数として宣言されていることに注意してください。
FixedUpdateNetwork()
はプロキシ(ネットワークオブジェクトに対する状態権限を持たないリモートプレイヤー)では呼び出されないため、ネットワーク変数の変更を検出するために2つのChangeDetector
が必要です。グラブのロジックは状態権限を持つプレイヤーのFixedUpdateNetwork()
内で処理され、一方でコールバックはすべてのユーザーによってRender()
中に呼び出されます。
補助メソッドTryDetectGrabberChange()
は、両方のケースで使用されます。
C#
ChangeDetector funChangeDetector;
ChangeDetector renderChangeDetector;
public override void Spawned()
{
[...]
funChangeDetector = GetChangeDetector(NetworkBehaviour.ChangeDetector.Source.SimulationState);
renderChangeDetector = GetChangeDetector(NetworkBehaviour.ChangeDetector.Source.SnapshotFrom);
}
public override void FixedUpdateNetwork()
{
// Check if the grabber changed
if (TryDetectGrabberChange(funChangeDetector, out var previousGrabber, out var currentGrabber))
{
if (previousGrabber)
{
// Object ungrabbed
UnlockObjectPhysics();
}
if (currentGrabber)
{
// Object grabbed
LockObjectPhysics();
}
}
[...]
}
public override void Render()
{
// Check if the grabber changed, to trigger callbacks only (actual grabbing logic in handled in FUN for the state authority)
// Those callbacks can't be called in FUN, as FUN is not called on proxies, while render is called for everybody
if (TryDetectGrabberChange(renderChangeDetector, out var previousGrabber, out var currentGrabber))
{
if (previousGrabber)
{
if (onDidUngrab != null) onDidUngrab.Invoke();
}
if (currentGrabber)
{
if (onDidGrab != null) onDidGrab.Invoke(currentGrabber);
}
}
[...]
}
bool TryDetectGrabberChange(ChangeDetector changeDetector, out NetworkHandColliderGrabber previousGrabber, out NetworkHandColliderGrabber currentGrabber)
{
previousGrabber = null;
currentGrabber = null;
foreach (var changedNetworkedVarName in changeDetector.DetectChanges(this, out var previous, out var current))
{
if (changedNetworkedVarName == nameof(CurrentGrabber))
{
var grabberReader = GetBehaviourReader<NetworkHandColliderGrabber>(changedNetworkedVarName);
previousGrabber = grabberReader.Read(previous);
currentGrabber = grabberReader.Read(current);
return true;
}
}
return false;
}
これは、すべてのプレイヤーが新しい値が更新されるとすぐに受け取ることを意味します(上の図の*(2))。
また、コールバックによって、すべてのプレイヤーがグラブおよびアングラブイベントに基づいてオブジェクトを設定できるようになります(主にその運動状態を編集/復元するため、必要に応じて上の図の(3)*)。
フォロー
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(followedTransform: CurrentGrabber.transform, LocalPositionOffset, LocalRotationOffset);}
C#
void Follow(Transform followedTransform, Vector3 localPositionOffsetToFollowed, Quaternion localRotationOffsetTofollowed)
{
transform.position = followedTransform.TransformPoint(localPositionOffsetToFollowed);
transform.rotation = followedTransform.rotation * localRotationOffsetTofollowed;
}
レンダリング
NetworkRig
やNetworkHand
と同様に、NetworkHandColliderGrabbable
は外挿を処理し、ネットワークリックの間であってもRender()
中にグラブされたオブジェクトの最新の位置を更新します(上の図の*(5)*)。クラス内のさまざまな[DefaultExecutionOrder]
およびpublic const int EXECUTION_ORDER
の指示により、NetworkGrabbble
のRender()
はNetworkTransform
メソッドの後に呼び出され、オブジェクトの視覚的な位置を上書きします。
ただし、この外挿には前述のものと比較して2つの特異点があります:
- 最初に、外挿はローカルユーザーに制限されません。オブジェクトがグラブされると、すべてのユーザーはそれがグラブした手に従うべきであることを「知っています」(グラブを説明するネットワーク変数のおかげです):たとえグラブされたオブジェクトとグラバーのネットワーク位置が少し同期していなくても、視覚的には一致する必要があります(プロキシ上でオブジェクトが手の周りで少し浮いているのを避けるため)。
- 次に、権限を取得する際に外挿するオプション(デフォルトで有効)が追加され、最良のユーザー体験を提供するための措置となっています:これにより、グラブされたオブジェクトが権限を受信するまで静止しているのを避けることができます(たとえ非常に短い時間であっても、ユーザーはVRでそれをわずかに感じる可能性があります)。したがって、権限を要求している間、グラバーとグラブポイントの位置が一時的なローカル変数に格納され、これらのデータを利用して特定の外挿が行われます。
C#
public override void Render()
{
[...]
if (isTakingAuthority && extrapolateWhileTakingAuthority)
{
// If we are currently taking the authority on the object due to a grab, the network info are still not set
// but we will extrapolate anyway (if the option extrapolateWhileTakingAuthority is true) to avoid having the grabbed object staying still until we receive the authority
ExtrapolateWhileTakingAuthority();
return;
}
// No need to extrapolate if the object is not grabbed
if (!IsGrabbed) return;
// Extrapolation: Make visual representation follow grabber, adding position/rotation offsets
// We extrapolate for all users: we know that the grabbed object should follow accuratly the grabber, even if the network position might be a bit out of sync
Follow(followedTransform: CurrentGrabber.hand.transform, LocalPositionOffset, LocalRotationOffset);
}
void ExtrapolateWhileTakingAuthority()
{
// No need to extrapolate if the object is not really grabbed
if (grabberWhileTakingAuthority == null) return;
// Extrapolation: Make visual representation follow grabber, adding position/rotation offsets
// We use grabberWhileTakingAuthority instead of CurrentGrabber as we are currently waiting for the authority transfer: the network vars are not already set, so we use the temporary versions
Follow(followedTransform: grabberWhileTakingAuthority.hand.transform, localPositionOffsetWhileTakingAuthority, localRotationOffsetWhileTakingAuthority);
}
VR インターポレーションと物理グラビタブル
VRにおいて、非キネマティックオブジェクトのいくつかの動作では、物理計算によって計算されたさまざまな位置間のジャンプを感じることがあります。
これを隠すためにはインターポレーションが必要です。これには2つのオプションがあります:
Fusionが物理計算を実行する場合、レンダリングコール中にインターポレーションを処理します。
Fusionが物理を処理しない場合、Unityのリジッドボディにこのインターポレーションを追加するように設定できます。
の場合、ランナーのゲームオブジェクトに
RunnerSimulatePhysics3D
を追加する必要があります。非ネットワーク親(シーンの整理のため)のあるNetworkRigidBody3D
は、インターポレーションが正しく機能するためにSync parent
をチェックしないでください。の場合、
RigidBody
コンポーネントでinterpolate
オプションを選択するだけです。
このサンプルでは、簡素化のために2番目のオプションが使用されています。
サードパーティ
次に
このプロジェクトで行うことができる変更や改善の提案をいくつか示します:
- 他のプレイヤーにローカルテレポートレイを表示する(ネットワーク変数または
INetworkInput
を使用) - ボイス機能を追加する。FusionへのPhoton Voice統合の詳細については、こちらのページを参照してください:https://doc.photonengine.com/en-us/voice/current/getting-started/voice-for-fusion
- ランタイムで追加のネットワークオブジェクトを生成するボタンを作成する。