VR Shared
개요
Fusion VR Shared 는 VR 기능이 있는 멀티플레이어 게임 또는 애플리케이션의 쉽고 빠른 시작법에 대해 설명합니다.
공유 토폴로지 또는 호스트/서버 토폴로지 중 어떤 것을 선택할지는 게임 사양에 따라 결정해야 합니다. 이 샘플에서는 공유 모드가 사용됩니다.
이 샘플의 목적은 VR 리그를 다루는 방법을 명확히 하고 기본적인 텔레포트 및 잡기 예제를 제공하는 것에 있습니다.
시작하기 전에
- 이 프로젝트는 유니티 2021.3.7f1 및 Fusion 1.1.3f 빌드 599로 개발되었습니다
- 샘플을 실행하려면 먼저 PhotonEngine 관리 화면에서 Fusion AppId를 생성하여 Real Time(Fusion 메뉴에서 접근 가능)의
App Id Fusion
필드에 붙여 넣습니다. 그런 다음Launch
씬을 로드하고Play
를 누릅니다.
다운로드
버전 | 릴리즈 일자 | 다운로드 | |
---|---|---|---|
1.1.8 | Sep 21, 2023 | Fusion VR Shared 1.1.8 Build 276 |
입력 처리
Meta Quest
- 텔레포트 : A, B, X, Y를 누르거나, 아무 스틱이나 눌러 포인터를 표시합니다. 떼면 대상 이동 가능한 목표물로 텔레포트합니다.
- 잡기 : 먼저 손을 물체 위에 놓고 컨트롤러 잡기 버튼을 사용하여 물체를 잡습니다
마우스
기본 데스크톱 리그가 프로젝트에 포함되어 있습니다. 마우스를 사용하여 기본적인 상호작용을 한다는 것을 의미합니다.
- 이동: 마우스를 사용하여 마우스 왼쪽 버튼을 클릭하여 포인터를 표시합니다. 떼면 이동 가능한 목표물로 텔레포트합니다
- 회전: 마우스 오른쪽 버튼을 누른 상태에서 마우스를 움직여 시점을 회전합니다
- 잡기: 마우스로 물체를 왼쪽 클릭하여 잡습니다.
Connection Manager
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"가 선택되어 있는지 확인하세요.
리그
개요
몰입형 애플리케이션에서, 리그는 사용자를 표현하는 데 필요한 모든 이동할 수 있는 파트, 일반적으로 손, 머리 그리고 플레이 영역(예를 들어 사용자가 텔레포트할 때 이동할 수 있는 개인 공간)을 설명합니다.
네트워크 세션에 연결되어 있는 동안 모든 사용자는 다양한 부품 위치가 네트워크를 통해 동기화되는 네트워크 리그로 표시됩니다.
리그 부품의 구성 및 동기화 방법과 관련하여 여러 아키텍처가 가능하며 유효합니다. 여기서 사용자는 각 리그 부품마다 하나씩 여러 개의 중첩된 NetworkTransforms
이 있는 단일 NetworkObject
로 표시됩니다.
로컬 사용자를 대표하는 네트워크 리그의 특정 사례와 관련하여, 이 리그는 하드웨어 입력에 의해 구동되어야 합니다. 이 프로세스를 단순화하기 위해 별도의 비 네트워크 장치인 “Hardware rig
”가 만들어졌습니다. 고전적인 유니티 컴포넌트를 사용하여 하드웨어 입력(예: 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
로의 입력 전송은 상태 권한이 있는 로컬 사용자에서만 수행됩니다. 이러한 변경 사항이 프록시(다른 플레이어 애플리케이션의 이 플레이어 인스턴스)에서 복제되도록 하려면 다음과 같은 다른 작업을 수행해야 합니다:
- 리그 부품 위치 및 회전의 경우, 해당 리그 부품에는
Transform
위치 또는 회전이 업데이트될 때 이미 이 동기화를 처리NetworkTransform
컴포넌트가 있습니다. - 손 동작와 같은 애플리케이션별 데이터의 경우 네트워크 변수
[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]
태그는 NetworkRig
Render()
가 NetworkTransform
메소드 뒤에 호출되도록 하여 NetworkTransform
이 보간 대상의 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
부울 값을 업데이트합니다. 부울은 사용자가 그립 버튼을 누르면 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 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
가 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
은 네트워크 변수로 선언됩니다. 즉, 모든 플레이어는 업데이트되는 즉시 새 값을 받게 되며(위 다이어그램의 (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
은 외삽을 처리하고 네트워크 틱(위 다이어그램의 (5)) 사이에서도 가장 최근 위치의 Render()
동안 잡힌 물체의 시각적 위치를 업데이트합니다. 클래스의 다양한 [OrderAfter]
태그는 NetworkGrabbble
Render()
가 NetworkTransform
메소드 뒤에 호출되도록 하여 해당 클래스에서 NetworkTransform
의 보간 대상 처리를 재정의합니다.
그러나 이 외삽법은 이전의 것과 비교할 때 다음과 같은 두 가지 특수성이 있습니다:
- 첫째, 외삽은 로컬 사용자로 제한되지 않습니다. 물체를 잡을 때 모든 사용자는 잡힌 손을 따라야 한다는 것을 "알고" 있습니다(잡는 것을 설명하는 네트워크 변수 덕분에). 잡힌 물체와 잡는 사람의 네트워크 위치가 약간 일치하지 않더라도 시각적으로 일치해야 합니다(프록시에서 손 주위에 물체가 약간 떠 있는 것을 방지합니다).
- 둘째로, 최상의 사용자 경험을 제공하기 위해 권한을 취하는 동안 추정하는 옵션(기본적으로 활성화됨)이 추가되었습니다. 권한을 받을 때까지 잡힌 객체가 가만히 있지 않도록 합니다(매우 짧은 기간이라도 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
을 사용 ) - 음성 기능 추가. Fusion과 Photon Voice의 통합은 다음 페이지를 참고하세요: https://doc.photonengine.com/en-us/voice/current/getting-started/voice-for-fusion
- 런타임에 추가 네트워크 개체를 스폰하는 버튼을 만듭니다