VR Shared - 로컬 리그 잡기
기본 VR Shared 샘플의 잡기 로직과의 차이점
이 잡기 시스템은 VR Shared 기술 샘플에 설명된 것과 매우 유사하지만, 몇 가지 유용한 차이점이 있습니다:
- 잡을 수 있는 물체를 감지하는 콜라이더가 네트워크 리그가 아닌 하드웨어 리그에 있습니다.
- 이 방식으로 네트워크 리그가 아직 생성되지 않은 상황(예: 완전 오프라인 상태)에서도 사용할 수 있습니다.
- 네트워크 리그가 하드웨어 리그와 다른 위치에 고의적으로 배치된 경우에도 유용합니다(예: 텔레포트 중 네트워크 리그 위치가 부드럽게 전환될 때).
다운로드
이 샘플은 VR Shared 페이지 다운로드에 포함되어 있습니다. 로컬 리그 잡기 기능을 시연하는 장면은 Scenes/AlternativeHardwareBasedGrabbingDemo
폴더에 있습니다.
개요
이 잡기 논리는 두 가지 부분으로 나뉩니다:
- 로컬 부분: 네트워크 없이도 하드웨어 손이 물체를 잡거나 놓는 동작을 감지합니다(
Grabber
및Grabbable
클래스 사용). - 네트워크 부분: 모든 플레이어가 잡기 상태를 인식하게 하고, 잡은 손을 따라 물체의 위치를 관리합니다(
NetworkGrabber
및NetworkGrabbable
클래스 사용).
참고: 코드에는 오프라인 로비와 같은 오프라인 환경에서도 동일한 컴포넌트를 사용할 수 있도록 몇 가지 로컬 관리 코드가 포함되어 있습니다. 이 문서는 네트워크 사용에 초점을 맞추고 있습니다.
세부 사항
잡기
HardwareHand
클래스는 각 손에 배치되어 매 업데이트마다 isGrabbing
부울 값을 갱신합니다. 이 값은 사용자가 그립 버튼을 누를 때 true
로 설정됩니다.
데스크톱 리그(마우스와 키보드로 조작되는 리그)를 지원하기 위해 updateGrabWithAction
불리언이 사용됩니다. 데스크톱 모드에서는 False
로, VR 모드에서는 True
로 설정해야 합니다.
C#
protected virtual void Update()
{
// update hand pose
// (...)
// update hand interaction
if(updateGrabWithAction) isGrabbing = grabAction.action.ReadValue<float>() > grabThreshold;
}
각 하드웨어 손에는 단순한 박스 콜라이더가 있어, 물체와의 충돌을 감지합니다. 손에 있는 Grabber
컴포넌트는 충돌 시 OnTriggerStay(Collider other)
메소드를 호출합니다.
먼저, 물체가 이미 잡혀 있는지 확인합니다. 이 샘플에서는 여러 개의 물체를 동시에 잡는 것을 허용하지 않습니다.
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
컴포넌트가 있는지 - 사용자가 그립 버튼을 눌렀는지
이 조건이 만족되면 물체를 손이 따라가도록 요청합니다(위 다이어그램의 (1)). 이는 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(wasHovered || grabbable.allowedClosedHandGrabing)
{
Grab(grabbable);
}
}
잡기 동기화
Grabbable
클래스의 Grab()
메소드는 잡기 위치 오프셋을 저장합니다.
또한 로컬 사용자가 물체를 잡았음을 NetworkGrabbable
에 알립니다(위 다이어그램의 (2)). 이는 networkGrabbable.LocalGrab()
호출을 통해 수행됩니다.
C#
public virtual void Grab(Grabber newGrabber, Transform grabPointTransform = null)
{
if (onWillGrab != null) onWillGrab.Invoke(newGrabber);
// Find grabbable position/rotation in grabber referential
localPositionOffset = newGrabber.transform.InverseTransformPoint(transform.position);
localRotationOffset = Quaternion.Inverse(newGrabber.transform.rotation) * transform.rotation;
currentGrabber = newGrabber;
if (networkGrabbable)
{
networkGrabbable.LocalGrab();
}
else
{
// We handle the following if we are not online (online, the DidGrab will be called by the NetworkGrabbable DidGrab, itself called on all clients by HandleGrabberChange when the grabber networked var has changed)
LockObjectPhysics();
}
}
NetworkGrabbable
의 LocalGrab()
메소드는 다음 작업을 수행합니다:
- 상태 권한을 요청하여 네트워크 변숫값을 저장할 수 있게 합니다.
- 권한이 부여되면, 잡기 정보를 네트워크 변수에 저장합니다.
C#
public async virtual void LocalGrab()
{
// 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 = grabbable.localPositionOffset;
LocalRotationOffset = grabbable.localRotationOffset;
if(grabbable.currentGrabber==null)
{
// The grabbable has already been ungrabbed
return;
}
// Update the CurrentGrabber in order to start following position in the FixedUpdateNetwork
CurrentGrabber = grabbable.currentGrabber.networkGrabber;
}
노트: 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;
}
ChangeDetector
는 모든 클라이언트에서 네트워크 변수 CurrentGrabber
의 변경을 감지하는 데 사용됩니다(위 다이어그램의 (3) 지점*).
FixedUpdateNetwork()
는 프록시(네트워크 객체에 대한 상태 권한이 없는 원격 플레이어)에서는 호출되지 않기 때문에, 네트워크 변수 변경을 감지하기 위해 두 개의 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;
}
이 방법을 통해 상태 권한은 isKinematic
값을 적절히 설정할 수 있으며(잡힌 동안 물리 엔진이 비활성화됨), 놓을 때 속도를 적용할 수 있습니다.
C#
public virtual void LockObjectPhysics()
{
// While grabbed, we disable physics forces on the object, to force a position based tracking
if (rb) rb.isKinematic = true;
}
public virtual void UnlockObjectPhysics()
{
// We restore the default isKinematic state if needed
if (rb) rb.isKinematic = expectedIsKinematic;
// We apply release velocity if needed
if (rb && rb.isKinematic == false && applyVelocityOnRelease)
{
rb.velocity = Velocity;
rb.angularVelocity = AngularVelocity;
}
ResetVelocityTracking();
}
따라가기
물체가 잡혀 있는 동안 물리 엔진이 비활성화되기 때문에, NetworkGrabbable
에서 현재 그랩버의 위치로 단순히 이동(텔레포트)합니다.
이 작업은 상태 권한이 있는 플레이어의 FixedUpdateNetwork()
에서 수행되며, 이후 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
grabbable.Follow(followedTransform: CurrentGrabber.transform, LocalPositionOffset, LocalRotationOffset);
}
Follow()
메소드는 Grabbable
클래스에 위치해 있습니다.
C#
public virtual void Follow(Transform followingtransform, Transform followedTransform, Vector3 localPositionOffsetToFollowed, Quaternion localRotationOffsetTofollowed)
{
followingtransform.position = followedTransform.TransformPoint(localPositionOffsetToFollowed);
followingtransform.rotation = followedTransform.rotation * localRotationOffsetTofollowed;
}
렌더링
위 다이어그램의 (6) 에 해당하는 외삽은 Render()
중에 수행됩니다. NetworkGrabbable
클래스는 필요할 경우 NetworkTransform
보간을 무시하도록 OrderAfter
지시문을 포함합니다. 여기서는 두 가지 상황을 처리해야 합니다:
- 객체 권한 요청 중 외삽: 잡는 클라이언트에서 몇 프레임 동안
CurrentGrabber
가 설정되지 않습니다. 권한 요청이 아직 대기 중이기 때문에,[Networked]
변수는 상태 권한이 있을 때만 설정할 수 있습니다. 따라서IsGrabbed
가 아직true
를 반환하지 않으므로, 실제 따라갈NetworkGrabber
는 로컬Grabbable
및Grabber
컴포넌트를 통해 찾아야 합니다. - 잡힌 동안의 외삽: 모든 클라이언트에서 물체의 예상 위치는 손의 위치에 있어야 합니다.
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
grabbable.Follow(followedTransform: CurrentGrabber.hand.transform, LocalPositionOffset, LocalRotationOffset);
}
protected virtual void ExtrapolateWhileTakingAuthority()
{
// No need to extrapolate if the object is not really grabbed
if (grabbable.currentGrabber == null) return;
NetworkGrabber networkGrabber = grabbable.currentGrabber.networkGrabber;
// 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
grabbable.Follow(followedTransform: networkGrabber.hand.transform, grabbable.localPositionOffset, grabbable.localRotationOffset);
}
Back to top