This document is about: FUSION 1
SWITCH TO

VR Host

Level 4

개요

Fusion VR Host는 VR로 멀티플레이어 게임이나 애플리케이션을 빠르고 쉽게 시작할 수 있는 방법입니다.

공유 토폴로지 또는 호스트/서버 토폴로지 중 하나의 선택은 게임 사양에 따라 결정해야 합니다. 이 샘플에서는 호스트 모드를 사용합니다.

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

fusion vr host

시작하기 전에

  • 이 프로젝트는 유니티 2021.3.7f1 및 Fusion 1.1.3으로 개발되었습니다.
  • 샘플을 실행하려면 먼저 PhotonEngine 관리 화면에서 Fusion AppId를 생성하여 Real Time 설정(Fusion 메뉴에서 접근 가능)의 App Id Fusion 필드에 붙여 넣습니다. 그런 다음 Launch 씬을 로드하고 Play를 누릅니다.

다운로드

버전 릴리즈 일자 다운로드
1.1.8 Sep 21, 2023 Fusion VR Host 1.1.8 Build 278

입력 처리

Meta Quest

  • 텔레포트 : 포인터를 표시하려면 A, B, X, Y 또는 아무 스틱이나 누르십시오. 떼었을 때 허용된 목표물에 텔레포트하게 됩니다.
  • 잡기 : 먼저 손을 물체 위에 놓고 컨트롤러 잡기 버튼을 사용하여 물체를 잡습니다.

마우스

기본 데스크톱 리그가 프로젝트에 포함되어 있습니다. 마우스를 사용하여 기본적인 상호작용을 한다는 것을 의미합니다.

  • 이동 : 마우스 왼쪽 버튼으로 클릭하여 포인터를 표시합니다. 해제 시 승인된 대상에 순간이동합니다.
  • 회전 : 마우스 오른쪽 버튼을 누른 상태에서 마우스를 움직여 시점을 회전합니다.
  • 잡기 : 마우스를 마우스 왼쪽 버튼을 클릭하여 물체를 잡습니다.

연결 관리자

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 콜백은 플레이어가 세션에 참여할 때 호스트에서 사용자 프리팹을 스폰 하는 데 사용되고, OnPlayerLeft는 동일한 플레이어가 세션을 종료할 때 해당 프리팹을 제거하는 데 사용됩니다.

C#

public void OnPlayerJoined(NetworkRunner runner, PlayerRef player)
{
    // The user's prefab has to be spawned by the host
    if (runner.IsServer)
    {
        Debug.Log($"OnPlayerJoined {player.PlayerId}/Local id: ({runner.LocalPlayer.PlayerId})");
        // We make sure to give the input authority to the connecting player for their user's object
        NetworkObject networkPlayerObject = runner.Spawn(userPrefab, position: transform.position, rotation: transform.rotation, inputAuthority: player, (runner, obj) => { 
        });

        // Keep track of the player avatars so we can remove it when they disconnect
        _spawnedUsers.Add(player, networkPlayerObject);
    }
}

// Despawn the user object upon disconnection
public void OnPlayerLeft(NetworkRunner runner, PlayerRef player)
{
    // Find and remove the players avatar (only the host would have stored the spawned game object)
    if (_spawnedUsers.TryGetValue(player, out NetworkObject networkObject))
    {
        runner.Despawn(networkObject);
        _spawnedUsers.Remove(player);
    }
}

유니티 Connection Manager 게임 오브젝트에서 "Auto Host or Client"가 선택되었는지 확인하십시오.

fusion vr host auto host or client
예를 들어 테스트 중에 특정 역할이 있는지 확인하기 위해 "호스트" 또는 "클라이언트"를 선택할 수도 있습니다.

리그

개요

몰입형 애플리케이션에서, 리그는 사용자를 표현하는 데 필요한 모든 이동식 부품(예를 들어 사용자가 텔레포트할 때 이동할 수 있는 개인 공간)을 말합니다.

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

fusion vr host 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;
    public GrabInfo leftGrabInfo;
    public GrabInfo rightGrabInfo;
}

HardwareRig 클래스는 Fusion NetworkRunner가 사용자 입력을 폴링 할 때 구조체를 업데이트합니다. 이를 위해 다양한 하드웨어 리그 부품에서 입력 파라미터를 수집합니다.

C#

public void OnInput(NetworkRunner runner, NetworkInput input) 
{
    // Prepare the input, that will be read by NetworkRig in the FixedUpdateNetwork
    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;
    rigInput.leftGrabInfo = leftHand.grabber.GrabInfo;
    rigInput.rightGrabInfo = rightHand.grabber.GrabInfo;
    input.Set(rigInput);
}

그런 다음 이러한 입력을 보낸 사용자와 연결된 네트워크 리그가 이를 수신합니다. 호스트(상태 권한)와 해당 입력을 보낸 사용자(입력 권한)가 모두 이를 수신합니다. 다른 사용자는 그렇지 않습니다(여기에서 다른 사용자들은 프록시입니다).

이는 FixedUpdateNetwork() (FUN) 중에 사용자 프리팹에 있는 NetworkRig 컴포넌트에서 GetInput(상태 및 입력 권한에 대한 입력만 반환)을 통해 발생합니다.

FUN이 진행되는 동안 네트워크에 연결된 모든 리그 부품은 일치하는 하드웨어 리그 부품의 입력 매개 변수를 따르도록 구성됩니다.

호스트 모드에서 입력이 호스트에 의해 처리될 때 입력은 프록시로 전달되어 사용자의 이동을 복제할 수 있습니다. 다음 중 하나입니다.

  • [Networked] 변수(핸드 포즈 및 잡기 정보)를 통해 처리됨: 상태 권한(호스트)이 네트워크 변숫값을 변경하면 이 값이 각 사용자에게 복제됩니다.
  • 또는 다른 사용자에 대한 복제를 처리하는 상태 당국(호스트) NetworkTransform 컴포넌트에 의해 처리되는 위치 및 회전 관련

C#

// As we are in host topology, we use the input authority to track which player is the local user
public bool IsLocalNetworkRig => Object.HasInputAuthority;
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))
    {
        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;
        // 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;
        
        leftGrabber.GrabInfo = input.leftGrabInfo;
        rightGrabber.GrabInfo = input.rightGrabInfo;
    }
}

FixedUpdateNetwork() 동안 네트워크 리그 부품 위치를 이동하는 것 외에도, NetworkRig 컴포넌트는 로컬 외삽도 처리합니다. 이 객체에 대한 입력 권한을 가진 로컬 사용자에 대한 Render() 동안 다양한 리그 부품의 NetworkTransforms의 그래픽 표현을 처리하는 보간 대상은 최신 로컬 하드웨어 리그 부품 데이터를 사용하여 이동합니다.

화면 새로 고침 빈도가 네트워크 틱 속도보다 높은 경우에도 로컬 사용자가 항상 가능한 최신 위치를 직접 사용할 수 있도록 합니다(잠재적인 우려 방지).

클래스 앞에 있는 [OrderAfter] 태그는 NetworkRig Render()NetworkTransform 메소드 뒤에 호출되도록 하여 NetworkRig가 보간 대상의 원래 처리를 재정의할 수 있도록 합니다.

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에 접근을 제공합니다.

핸드 포즈를 동기화하기 위해 하드웨어 핸드 클래스에 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 host hand representation
fusion vr host hand animator

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

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))
            {
                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;
                // we update the hand pose info. It will trigger on network hands OnHandCommandChange on each client, and update the hand representation accordingly
                leftHand.HandCommand = input.leftHandCommand;
                rightHand.HandCommand = input.rightHandCommand;

                leftGrabber.GrabInfo = input.leftGrabInfo;
                rightGrabber.GrabInfo = input.rightGrabInfo;
            }
        }

사용자 프리팹의 각 손에 있는 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 host teleport

각 하드웨어 장치 핸드에 위치한 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;
    transform.position = position - headsetOffet;
}

앞에서 본 것처럼 하드웨어 리그 위치에 대한 이 수정은 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);
}

잡기

개요

여기서 잡기 로직은 두 부분으로 구분됩니다:

  • 하드웨어 핸드가 붙잡을 수 있는 물체(GrabberGrabbable 클래스)에 대한 잡기 동작을 트리거 할 때 실제 잡기 및 놓기를 감지하는 비네트워크 로컬 파트
  • 모든 플레이어가 잡는 상태를 인식하도록 하고 잡는 손을 따르도록 실제 위치 변경을 관리하는 네트워크 부분(NetworkGrabberNetworkGrabbable 클래스).

노트: 코드에는 오프라인으로 사용할 때 로컬 파트가 다음을 관리할 수 있도록 몇 줄이 포함되어 있습니다. 예를 들어, 오프라인 로비에 동일한 컴포넌트가 사용되는 경우가 있습니다. 그러나 이 문서에서는 실제 네트워크 사용에 초점을 맞출 것입니다.

fusion vr host grabbing logic

이 샘플에서는 두 가지 다른 종류의 잡기를 사용할 수 있습니다(GrabbableNetworkGrabbable 클래스는 추상 클래스이며 각 특정 로직을 구현하는 하위 클래스입니다).

  • 운동학적 물체를 찾는 방법: 잡는 손의 위치를 따라가기만 하는 손의 위치입니다. 다른 물체와 물리적 상호작용을 할 수 없습니다. KinematicGrabbableNetworkKinematicGrabbable 클래스에서 구현됩니다.
  • 물리학적 물체에 대한 측정: 잡는 손을 따르도록 속도가 변경됩니다. 다른 물체들과 물리적 상호작용을 할 수 있고, 런칭할 수 있습니다. 이 구현을 위해서는 서버 물리 모드를 클라이언트 측 예측으로 설정해야 합니다. PhysicsGrabbableNetworkPhysicsGrabbable 클래스에서 구현됩니다.

참고: 운동학적 물체에 떼는 속도를 부여하는 것이 가능하지만, 추가 코드가 필요하기 때문에 이 샘플에 추가되지 않았습니다(단순한 잡기 사용 사례에 대한 매우 간단한 코드 기반을 보여주기 위해 별도의 잡기). 그리고 여기에 제공된 물리학적 잡기는 어떤 경우에도 제공됩니다. 이러한 사용 사례에 대해 보다 논리적인 구현과 보다 정확한 결과를 얻을 수 있습니다.

fusion vr host grabbing classes

상세 내용

각 손에 위치한 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;
}

잡을 수 있는 물체와의 충돌을 감지하기 위해 간단한 박스 콜라이더가 각 하드웨어 손에 위치하고 있으며, 이 손에 놓인 Grabber 컴포넌트가 이를 사용합니다. 충돌이 발생하면 OnTriggerStay()라는 메소드가 호출됩니다.

호스트 토폴로지에서 일부 틱은 정방향 틱(실제 새 눈금)이고 다른 틱은 재시뮬레이션(과거 인스턴스 재생)입니다. 잡기 및 놓기는 현재 위치에 해당하는 전진 틱 중에만 감지해야 합니다. 따라서 OnTriggerStay()는 재시뮬레이션 틱에 대해 실행되지 않습니다.

C#

private void OnTriggerStay(Collider other)
{
    if (rig && rig.runner && rig.runner.IsResimulation)
    {
        // We only manage grabbing during forward ticks, to avoid detecting past positions of the grabbable object
        return;
    }

먼저 OnTriggerStay는 객체가 이미 잡혀있는지 확인합니다. 단순화를 위해 이 샘플에서는 다중 그립이 허용되지 않습니다.

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

이후 다음을 확인합니다:

  • 충돌한 물체를 잡을 수 있습니다(Grabbable 구성 요소가 있음).
  • 사용자가 그립 버튼을 누릅니다.

이 조건이 충족되면 붙잡은 물체가 Grabbable Grab 메소드로 손을 따르도록 요구됩니다.

C#

Grabbable grabbable;

if (lastCheckedCollider == other)
{
    grabbable = lastCheckColliderGrabbable;
}
else
{
    grabbable = other.GetComponentInParent<Grabbable>();
}
// To limit the number of GetComponent calls, we cache the latest checked collider grabbable result
lastCheckedCollider = other;
lastCheckColliderGrabbable = grabbable;
if (grabbable != null)
{
    if (hand.isGrabbing) Grab(grabbable);
}

Grabbable Grab() 메소드는 잡은 위치 오프셋을 저장합니다.

C#

public virtual void Grab(Grabber newGrabber)
{
    // Find grabbable position/rotation in grabber referential
    localPositionOffset = newGrabber.transform.InverseTransformPoint(transform.position);
    localRotationOffset = Quaternion.Inverse(newGrabber.transform.rotation) * transform.rotation;
    currentGrabber = newGrabber;
}

마찬가지로 객체를 더 이상 잡지 않으면 Grabbable Ungrab() 호출은 객체에 대한 세부 정보가 저장됩니다.

C#

public virtual void Ungrab()
{
    currentGrabber = null;
    if (networkGrabbable)
    {
        ungrabPosition = networkGrabbable.networkTransform.InterpolationTarget.transform.position;
        ungrabRotation = networkGrabbable.networkTransform.InterpolationTarget.transform.rotation;
        ungrabVelocity = Velocity;
        ungrabAngularVelocity = AngularVelocity;
    }
}

실제로 사용되는 그립 유형 하위 클래스에 따라 일부 필드는 관련이 없습니다(예: 물리적 그립에 놓기 위치는 사용되지 않음).

그런 다음 그립(잡히는 객체의 네트워크 ID, 오프셋, 최종 놓기 속도 및 위치)에 대한 모든 데이터가 GrabInfo 구조체를 통한 입력 전송에서 공유됩니다.

C#

    // Store the info describbing a grabbing state
    public struct GrabInfo : INetworkStruct
    {
        public NetworkBehaviourId grabbedObjectId;
        public Vector3 localPositionOffset;
        public Quaternion localRotationOffset;
        // We want the local user accurate ungrab position to be enforced on the network, and so shared in the input (to avoid the grabbable following "too long" the grabber)
        public Vector3 ungrabPosition;
        public Quaternion ungrabRotation; 
        public Vector3 ungrabVelocity;
        public Vector3 ungrabAngularVelocity;
    }

입력을 작성할 때, 잡는 자는 다음과 같은 최신 파악 정보를 제공하라는 메시지를 받습니다.

C#

public GrabInfo GrabInfo
{
    get
    {
        if (grabbedObject)
        {
            _grabInfo.grabbedObjectId = grabbedObject.networkGrabbable.Id;
            _grabInfo.localPositionOffset = grabbedObject.localPositionOffset;
            _grabInfo.localRotationOffset = grabbedObject.localRotationOffset;

        } 
        else
        {
            _grabInfo.grabbedObjectId = NetworkBehaviourId.None;
            _grabInfo.ungrabPosition = ungrabPosition;
            _grabInfo.ungrabRotation = ungrabRotation; 
            _grabInfo.ungrabVelocity = ungrabVelocity;
            _grabInfo.ungrabAngularVelocity = ungrabAngularVelocity;
        }

        return _grabInfo;
    }
}

그런 다음 호스트가 NetworkRig에서 수신하면 호스트는 이를 NetworkGrabber GrabInfo [Networked] 변수에 저장합니다.

각 클라이언트에서 FixedUpdateNetwork() 중에 클래스가 수집 정보가 변경되었는지 확인합니다. 이 작업은 재시뮬레이션 중에 잡기/놓기가 플레이되지 않도록 전방 틱에서만 수행됩니다. HandleGrabInfoChange를 호출하여 이전과 현재의 잡기 상태를 비교합니다. 그런 다음 필요할 때 NetworkGrabbable에서 실제 GrabUngrab 메소드를 트리거 합니다.

새 객체를 잡으려면 먼저 네트워크 ID로 Object.Runner.TryFindBehaviourNetworkGrabbable로 검색하여 찾습니다. 러너, 행동을 찾아보세요.

C#

void HandleGrabInfoChange(GrabInfo previousGrabInfo, GrabInfo newGrabInfo)
{
    if (previousGrabInfo.grabbedObjectId !=  newGrabInfo.grabbedObjectId)
    {
        if (grabbedObject != null)
        {
            grabbedObject.Ungrab(newGrabInfo);
            grabbedObject = null;
        }
        // We have to look for the grabbed object has it has changed
        NetworkGrabbable newGrabbedObject;

        // If an object is grabbed, we look for it through the runner with its Id
        if (newGrabInfo.grabbedObjectId != NetworkBehaviourId.None && Object.Runner.TryFindBehaviour(newGrabInfo.grabbedObjectId, out newGrabbedObject))
        {
            grabbedObject = newGrabbedObject;
            if (grabbedObject != null)
            {
                grabbedObject.Grab(this, newGrabInfo);
            }
        }
    }
}

실제 네트워크 잡기, 놓기 및 네트워크 잡기 추적은 선택한 잡기 유형에 따라 다릅니다.

운동학적 붙잡기 유형

따라가기

운동학적 잡기 유형의 경우, 현재 잡기를 따라가는 것은 단순히 실제 위치로 순간이동하는 것입니다.

C#

public void Follow(Transform followingtransform, Transform followedTransform)
{
    followingtransform.position = followedTransform.TransformPoint(localPositionOffset);
    followingtransform.rotation = followedTransform.rotation * localRotationOffset;
}
FixedupdateNetwork

온라인에서 FixedUpdateNetwork 호출 중에 다음 코드가 호출됩니다.

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
    grabbable.Follow(followingtransform: transform, followedTransform: currentGrabber.transform);
}

위치 변경은 호스트(상태 권한)에서만 수행되며 NetworkTransform은 모든 플레이어가 위치 업데이트를 받도록 합니다.

렌더링

Render()(필요한 경우 NetworkTransform 보간을 오버라이드 하는 OrderAfter 지시어로서의 NetworkKinematic 클래스) 중에 이루어진 외삽과 관련하여, 2가지 사례를 여기서 처리해야 합니다.

  • 물체를 잡는 동안 외삽법: 물체 예상 위치가 알려져 있으므로 손 위치에 있어야 합니다. 그래서 붙잡을 수 있는 시각적인 것입니다. NetworkTransform의 보간 대상)은 손 시각적 위치에 있어야 합니다.
  • 네트워크 변환 보간은 여전히 객체가 잡히는 동안 수행된 외삽과 같지 않습니다. 그래서 짧은 순간 동안, 외삽은 계속되어야 합니다. 물체는 붙잡지 않은 위치에서 가만히 있어야 합니다). 그렇지 않으면 물체가 과거보다 약간 점프할 것입니다.

C#

public override void Render()
{
    if (IsGrabbed)
    {
        // Extrapolation: Make visual representation follow grabber visual representation, 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
        grabbable.Follow(followingtransform: networkTransform.InterpolationTarget.transform, followedTransform: currentGrabber.networkTransform.InterpolationTarget.transform);
    } 
    else if (grabbable.ungrabTime != -1)
    {
        if ((Time.time - grabbable.ungrabTime) < ungrabResyncDuration)
        {
            // When the local user just ungrabbed the object, the network transform interpolation is still not the same as the extrapolation 
            //  we were doing while the object was grabbed. So for a few frames, we need to ensure that the extrapolation continues
            //  (ie. the object stay still)
            //  until the network transform offers the same visual conclusion that the one we used to do
            // Other ways to determine this extended extrapolation duration do exist (based on interpolation distance, number of ticks, ...)
            networkTransform.InterpolationTarget.transform.position = grabbable.ungrabPosition;
            networkTransform.InterpolationTarget.transform.rotation = grabbable.ungrabRotation;
        }
        else
        {
            // We'll let the NetworkTransform do its normal interpolation again
            grabbable.ungrabTime = -1;
        }
    }
}

노트: 추가 에지의 경우, 예를 들어 객체를 잡는 클라이언트에서 실제 잡기와 첫 번째 틱 사이에 [Networked] 변수가 설정된 경우, 일부 추가 추정을 수행할 수 있습니다. 그렇지 않으면 손 위치가 약간(밀리초) 전으로 따라갈 수 있습니다.

물리 잡기 유형

따라가기

운동학적 잡기 유형의 경우, 현재 잡기를 따르는 것은 잡은 물체의 속도를 변경하여 결국 잡기에 다시 합류하는 것을 의미합니다. 이는 원하는 전체 물리의 종류에 따라 속도를 직접 변경하거나 힘을 사용하여 수행할 수 있습니다. 샘플은 두 로직을 모두 보여주며, PhysicsGrabbable에서 선택할 수 있는 옵션을 제공합니다. 직접적인 속도 변화는 더 간단한 방법입니다.

C#

void Follow(Transform followedTransform, float elapsedTime)
{
    // Compute the requested velocity to joined target position during a Runner.DeltaTime
    rb.VelocityFollow(target: followedTransform, localPositionOffset, localRotationOffset, elapsedTime);
    // To avoid a too aggressive move, we attenuate and limit a bit the expected velocity
    rb.velocity *= followVelocityAttenuation; // followVelocityAttenuation = 0.5F by default
    rb.velocity = Vector3.ClampMagnitude(rb.velocity, maxVelocity); // maxVelocity = 10f by default
}
FixedUpdateNetwork

물리를 제대로 계산할 수 있으려면 나중에 FixedUpdateNetwork에 설명된 것처럼 네트워크 입력 데이터가 캡처 클라이언트에 필요합니다. 따라서 입력 권한은 Grab() 중에 적절하게 할당되어야 합니다.

C#

public override void Grab(NetworkGrabber newGrabber, GrabInfo newGrabInfo)
{
    grabbable.localPositionOffset = newGrabInfo.localPositionOffset;
    grabbable.localRotationOffset = newGrabInfo.localRotationOffset;

    currentGrabber = newGrabber;
    if (currentGrabber != null)
    {
        lastGrabbingUser = currentGrabber.Object.InputAuthority;
    }

    lastGrabber = currentGrabber;

    DidGrab();

    // We store the precise grabbing tick to be able to determined if we are grabbing during resimulation tick, 
    //  where tha actual currentGrabber may have changed in the latest forward ticks
    startGrabbingTick = Runner.Tick;
    endGrabbingTick = -1;
}

FixedUpdateNetwork에서 각 클라이언트는 아래의 코드를 실행하여 잡은 물체의 속도가 잡은 손으로 이동하도록 합니다.

명심해야 할 중요한 점은 클라이언트에서 FixedUpdateNetwork가 호출된다는 것입니다.

  • 정방향 틱 중(최신 신뢰할 수 있는 호스트 데이터 이후에 발생하는 현상을 처음으로 계산하고 예측하려고 시도하는 경우).
  • 재시뮬레이션(서버에서 새 데이터가 도착할 때 재계산되는 예측 틱으로, 이전에 생성된 예측 틱과 잠재적으로 모순됨) 중에도 마찬가지입니다.

사용자가 물체를 잡을 때 Follow 코드가 각 틱에 독립적으로 관련되어 있기 때문에 별로 중요하지 않습니다. 하지만 재시뮬레이션 틱 동안, 사용자가 잡을 수 있는 객체를 놓을 때 일부 틱은 물체가 여전히 잡혀 있는 동안 발생한 반면, 일부 틱은 더 이상 잡히지 않는 동안 발생했습니다. 그러나 Ungrab() 호출에서 currentGrabber 변수가 null로 설정되었기 때문에 더 이상 ungrab 이전의 재시뮬레이션 틱에는 적합하지 않습니다. 따라서 틱 중에 실제 그립 상태를 알 수 있도록 그립 및 언그랩과 관련된 틱은 startGrabbingTickendGrabbingTick 변수에 저장됩니다. 그런 다음 재시뮬레이션 중에 FixedUpdateNetwork()에서 이러한 변수를 사용하여 개체가 이 체크 틱 중에 실제로 잡혔는지 여부를 결정합니다.

C#

public override void FixedUpdateNetwork()
{
    if (Runner.IsForward)
    {
        // during forward tick, the IsGrabbed is reliable as it is changed during forward ticks
        //  (more precisely, it is one tick late, due to OnChanged being called AFTER FixedUpdateNetwork,but this way every client, including proxies, can apply the same physics)
        if (IsGrabbed)
        {
            grabbable.Follow(followedTransform: currentGrabber.transform, elapsedTime: Runner.DeltaTime);
        }
    }
    if (Runner.IsResimulation)
    {
        bool isGrabbedDuringTick = false;
        if (startGrabbingTick != -1 && Runner.Tick >= startGrabbingTick)
        {
            if (Runner.Tick < endGrabbingTick || endGrabbingTick == -1)
            {
                isGrabbedDuringTick = true;
            }
        }

        if (isGrabbedDuringTick)
        {
            grabbable.Follow(followedTransform: lastGrabber.transform, elapsedTime: Runner.DeltaTime);
        }

        // For resim, we reapply the release velocity on the Ungrab tick, like it was done in the Forward tick where it occurred first.
        if (endGrabbingTick == Runner.Tick)
        {
            grabbable.rb.velocity = lastUngrabVelocity;
            grabbable.rb.angularVelocity = lastUngrabAngularVelocity;
        }
    }
}
렌더링

물리 계산으로 인해 보간 된 위치가 교란되는 것을 방지하기 위해, 여기서 Render() 로직은 잡은 물체의 시각적 위치를 손의 시각적 위치에 강제로 적용하지 않습니다.

몇 가지 옵션을 사용할 수 있습니다(예를 들어 아무것도 하지 않음, 충돌 시 잡은 물체를 손이 통과하는 등).

샘플의 현재 구현은 다음과 같은 Render 로직을 사용합니다.

  • 잡은 물체 시각적 위치가 손 시각적 위치에 머무르는 대신, 잡힌 물체 시각적 위치를 따르도록 강제되는 손의 시각적 위치
  • 충돌 시 실제 손 위치와 표시된 손 위치 사이에 약간의 차이가 발생할 수 있습니다. 편안하게 사용할 수 있도록 실제 손 위치에 "유령" 손이 표시됩니다.
  • 사용자가 이러한 불협화음을 느낄 수 있도록(특히 충돌 시) 컨트롤러는 표시된 손과 실제 손 사이의 불협화음 거리에 비례하는 진동을 보냅니다. 그것은 약간의 저항감을 제공합니다.
  • 손 위치를 부드럽게 복원하기 위해 물체를 놓을 때 아무런 노력도 기울이지 않습니다(필요한 경우 추가할 수 있음).

C#

public override void Render()
{
    base.Render();

    if (IsGrabbed)
    {
        var handVisual = currentGrabber.hand.networkTransform.InterpolationTarget.transform;
        var grabbableVisual = networkTransform.InterpolationTarget.transform;

        // On remote user, we want the hand to stay glued to the object, even though the hand and the grabbed object may have various interpolation
        handVisual.rotation = grabbableVisual.rotation * Quaternion.Inverse(grabbable.localRotationOffset);
        handVisual.position = grabbableVisual.position - (handVisual.TransformPoint(grabbable.localPositionOffset) - handVisual.position);

        // Add pseudo haptic feedback if needed
        ApplyPseudoHapticFeedback();
    }
}

// Display a ghost" hand at the position of the real life hand when the distance between the representation (glued to the grabbed object, and driven by forces) and the IRL hand becomes too great
//  Also apply a vibration proportionnal to this distance, so that the user can feel the dissonance between what they ask and what they can do
void ApplyPseudoHapticFeedback()
{
    if (pseudoHapticFeedbackConfiguration.enablePseudoHapticFeedback && IsGrabbed && IsLocalPlayerMostRecentGrabber)
    {
        if (currentGrabber.hand.LocalHardwareHand.localHandRepresentation != null)
        {
            var handVisual = currentGrabber.hand.networkTransform.InterpolationTarget.transform;
            Vector3 dissonanceVector = handVisual.position - currentGrabber.hand.LocalHardwareHand.transform.position;
            float dissonance = dissonanceVector.magnitude;
            bool isPseudoHapticNeeded = (isColliding && dissonance > pseudoHapticFeedbackConfiguration.minContactingDissonance);
            currentGrabber.hand.LocalHardwareHand.localHandRepresentation.DisplayMesh(isPseudoHapticNeeded);
            if (isPseudoHapticNeeded)
            {
                currentGrabber.hand.LocalHardwareHand.SendHapticImpulse(amplitude: Mathf.Clamp01(dissonance / pseudoHapticFeedbackConfiguration.maxDissonanceDistance), duration: pseudoHapticFeedbackConfiguration.vibrationDuration);
            }
        }
    }
}

타사

향후

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

  • 로컬 텔레포트 광선을 다른 플레이어에게 표시합니다(OnInput 호출 중 공유된 RigInput 구조체를 추가).
  • 음성 기능을 추가합니다. Photon Voice와 Fusion의 통합에 대한 자세한 내용을 보려면 다음 페이지https://doc.photonengine.com/en-us/voice/current/getting-started/voice-for-fusion를 참고하세요.
  • 런타임에 추가 네트워크 개체를 생성하는 버튼을 만듭니다.

Changelog

  • Fusion VR Host 1.1.2 빌드 5
    • 전진 틱으로 제한하기 위해 잡기/놓기 탐지 수정
    • 사용자가 잡은 손을 바꾸려고 할 때의 버그 수정
    • PseudoHapticGrabbableRender 옵션을 추가하여 물리적으로 잡을 수 있는 객체에 대해 유사 햅틱 피드백(즉, 고스트 IRL 손이 나타날 때) 중에 고스트 붙잡힌 개체 표시
    • NetworkGrabber에서 OnChanged 대신 FixedUpdateNetwork를 사용하여 gracking info 1을 먼저 선택합니다.
    • 2차 팔로잉 타입을 제거하고 물리학 Grabable에서 Follow()를 가상으로 설정하여 개발자가 자체 팔로우 물리를 구현할 수 있도록 합니다.
    • 프록시 사용자 캡처 물리 허용: 입력 권한이 더 이상 변경되지 않고 InterpolationDataSource도 변경되지 않으며, 대신 잡기/놓기 틱이 기억되어 각 클라이언트에서 재시뮬레이션 중에 캡처 상태를 결정합니다.
Back to top