This document is about: FUSION 1
SWITCH TO

VR Shared

Level 4

개요

Fusion VR Shared 는 VR 기능이 있는 멀티플레이어 게임 또는 애플리케이션의 쉽고 빠른 시작법에 대해 설명합니다.

공유 토폴로지 또는 호스트/서버 토폴로지 중 어떤 것을 선택할지는 게임 사양에 따라 결정해야 합니다. 이 샘플에서는 공유 모드가 사용됩니다.

이 샘플의 목적은 VR 리그를 다루는 방법을 명확히 하고 기본적인 텔레포트 및 잡기 예제를 제공하는 것에 있습니다.

Fusion VR Shared

시작하기 전에

  • 이 프로젝트는 유니티 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

NetworkRunnerConnection 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 NetworkRunnerConnection 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"가 선택되어 있는지 확인하세요.

리그

개요

몰입형 애플리케이션에서, 리그는 사용자를 표현하는 데 필요한 모든 이동할 수 있는 파트, 일반적으로 손, 머리 그리고 플레이 영역(예를 들어 사용자가 텔레포트할 때 이동할 수 있는 개인 공간)을 설명합니다.

네트워크 세션에 연결되어 있는 동안 모든 사용자는 다양한 부품 위치가 네트워크를 통해 동기화되는 네트워크 리그로 표시됩니다.

Fusion VR Shared Rigs Logic

리그 부품의 구성 및 동기화 방법과 관련하여 여러 아키텍처가 가능하며 유효합니다. 여기서 사용자는 각 리그 부품마다 하나씩 여러 개의 중첩된 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) 함수) 덕분에 손가락 위치를 수정하기 위해 이 인터페이스를 구현합니다.

Fusion VR Shared 손 표현
Fusion VR Shared 손 애니메이터

이제 어떻게 동기화되는지 알아보겠습니다.

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

텔레포트 & 로코모션

Fusion VR Shared 텔레포트

각 하드웨어 장치 핸드에 위치한 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);
}

잡기

Fusion VR Shared 잡기

개요

잡기 로직은 NetworkHandColliderGrabberNetworkHandColliderGrabbable이라는 두 가지 네트워크 컴포넌트를 기반으로 합니다:

  • NetworkHandColliderGrabber는 하드웨어 손이 잡을 수 있는 객체에 대해 잡기 동작을 트리거 할 때 잡기 및 놓기를 트리거 합니다
  • NetworkHandColliderGrabbable은 다른 네트워크의 잡는 정보를 네트워크 변수와 동기화하여 잡을 수 있는 객체가 각 플레이어 애플리케이션의 잡는 도구를 따릅니다.

참고: 리그 부품 위치 및 손 모양 처리는 호스트 또는 서버 토폴로지에서 수행되는 작업과 매우 유사하지만, 가능한 쉽게 파악할 수 있도록 여기에서 잡는 방법은 공유 토폴로지를 따라 작성되었습니다

이 현재 페이지에서는 네트워크 리그에 강력하게 연결된 매우 간단하고 구현하기 쉬운 잡기 시스템에 대해 설명합니다. 하드웨어 리그에 더 의존하는 대체 구현은 여기에서 찾을 수 있습니다: VR Shared - 로컬 리그 잡기

상세 내용

Fusion VR Shared 원격 잡기 로직

잡기

각 손에 위치한 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, LocalPositionOffsetLocalRotationOffset은 네트워크 변수로 선언됩니다. 즉, 모든 플레이어는 업데이트되는 즉시 새 값을 받게 되며(위 다이어그램의 (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);
}

타사 제공

다음

다음은 이 프로젝트에서 연습할 수 있는 몇 가지 수정 또는 개선 제안입니다:

Back to top