VR 게임을 개발하는 데 있어서는 양손을 사용한 콘텐츠 요소가 많습니다. 하지만 가상현실인 만큼 자연스럽게 양손작동을 하기 위해서는 신경 쓸게 많습니다.
Steam VR에서 제공하는 클래스를 사용해 간단하게 양손으로 객체를 잡고 상호작용할수있는 기능을 구현해 보겠습니다.
준비
Asset Store - Steam VR Plugin
절차
1. Player 설정
SteamVR/InteractionSystem/Core/Prefabs 안에 있는 Player를 사용합시다.
2. 잡을 객체 설정
이해하기 쉽도록 부모/자식으로 설명하겠습니다.
부모 : 최상단 객체
자식 : 최상단 객체 안에 기능을 가진 빈 객체
부모설정
1) 최상단 부모에 Rigidbody 추가 및 하부 골격에 Collider를 추가합니다.
2) SteamVR-Interactable.cs 추가
> hideHandOnAttach : False
물체를 잡았을 때 손모양을 보이게 하기 위해서 False 설정
> UseHandObjectAttachmentPoint : False
Throwable.cs에서 객체 잡는 포인트를 지정해 주기 때문에 False 설정
3) SteamVR-Throwable.cs 추가
> AttachmentFlags 설정
SnapOnAttach : 지정한 위치점에 잡기 위해 체크
DetachFromOtherHand : 같은 타깃지점을 잡을 시 동시에 잡지 못하게끔 하기 위해 체크
VelocityMovement : 잡은 손의 위치와 동일하게 회전하고 이동하기 위해 체크
TurnOffGravity : 다른 외부의 물리힘에 반응하지 못하도록 체크
추가로 OnPickUp(), OnDetachFromHand() 이벤트 추가 같은 경우에는 스크립트 작성 부분에서 설명해 드리겠습니다.
4) SteamVR-SteamVRSkeletonPoser.cs 추가
클래스를 추가한 후 ShowLeftPreview 또는 ShowRightPreview를 체크한 후 직접 잡는 손의 모양을 만들어줍니다.
왼손 또는 오른손을 작업을 마쳤으면 SteamVRSkeletonPoser.cs에 SavePose 버튼을 눌러 저장을 하고 반대쪽 손도 체크 후 왼손작업을 마쳤다면 CopyLeftposetoRighthand, 오른손작업을 마쳤다면 Copy Right pose to Left hand를 클릭해서 작업한 손을 복사해 한 번 더 SavePose!! 양손 전부 작업을 마칩니다.
자식 설정
1) 부모객체 안에 빈객체를 생성해 Collider를 설정합니다.
단, 부모와 Collider는 겹치지 않도록 설정해야 합니다.
2) 부모와 동일하게 Interactable.cs, SteamVRSkeletonPoser.cs 추가
동일하게 손모양도 제작해 줍니다.
이제 기본적인 클래스 설정 및 컴포넌트 추가작업은 마쳤습니다. 이제 스크립트에서 작업을 진행해서 두 손을 Grab 했을 때 잡힐 수 있도록 구현해 보겠습니다.
3. 스크립트 작업
마찬가지로 부모 스크립트, 자식 스크립트로 나누겠습니다.
------------------------------------------------------------------------------------------------------------------------------------------------
부모 스크립트 )
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Valve.VR;
using Valve.VR.InteractionSystem;
public class MainGun : MonoBehaviour
{
private Interactable interactable; //부모 Interactable (본인)
public Interactable sub_inter; //자식 Interactable
private void Start()
{
interactable = GetComponent<Interactable>();
}
//매프레임 마지막에 검사를 진행합니다.
private void LateUpdate()
{
//서브 손잡이를 잡고 있는 상태에서 메인 손잡이를 놨을 때
if (!interactable.attachedToHand && sub_inter.attachedToHand)
{
//자식을 손에 접근불가능하게 합니다.
sub_inter.DetachFromHand();
sub_inter.attachedToHand.HoverUnlock(sub_inter);
sub_inter.attachedToHand.DetachObject(sub_inter.gameObject);
}
//check if grabbed
if (interactable.attachedToHand != null)
{
// Get the hand source
SteamVR_Input_Sources source = interactable.attachedToHand.handType;
}
}
//부모 객체에 닿고 있는 상태
private void HandHoverUpdate(Hand hand)
{
//서브 손잡이를 잡고 있는 상태에서 메인 손잡이를 놨을 때
if (!interactable.attachedToHand)
{
DebugUI.instance.debug_txt.text = "Main Hand Release";
hand.DetachObject(sub_inter.gameObject);
hand.HoverUnlock(sub_inter);
}
}
}
------------------------------------------------------------------------------------------------------------------------------------------------
sub_inter.DetachFromHand();
잡혀 있는 손을 체크해서 손에 있는 객체를 떨어트리기 위해 Interactable.cs에 부모, 자식 스크립트에서 접근 가능하도록 아래 함수를 추가합니다.
public void DetachFromHand()
{
OnDetachedFromHand(attachedToHand);
}
위에서 언급했던 Throwable.cs에서 추가되었던 OnPickUp(), OnDetachFromHand() 이벤트는 부모가 객체를 잡았을 때만 자식을 활성화시켜 스크립트를 작동을 시켜야 하기 때문에 SetActve(true, false)를 추가했습니다.
------------------------------------------------------------------------------------------------------------------------------------------------
자식 스크립트 )
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Valve.VR.InteractionSystem;
public class SubGun : MonoBehaviour
{
public Interactable main_inter; //부모 Interactable
private Interactable interactable; //자식 Interactable (본인)
private Quaternion secondRotationOffset; //부모를 기준으로 회전값을 조절하기 위한 값
private void Start()
{
interactable = GetComponent<Interactable>();
}
//부모의 조건이 맞지 않으면 강제로(자식을 잡고 있는 손) 떼내기 위한 함수
public void ForceDetach()
{
if (interactable.attachedToHand)
{
interactable.attachedToHand.HoverUnlock(interactable);
interactable.attachedToHand.DetachObject(gameObject);
}
}
//부모의 장착 지점과 자식의 장착 지점을 계산하여 Quaternion값을 반환한다.
private Quaternion GetTargetRotation()
{
Vector3 mainHandUp = main_inter.attachedToHand.objectAttachmentPoint.up;
Vector3 secondHandUp = interactable.attachedToHand.objectAttachmentPoint.up;
return Quaternion.LookRotation(interactable.attachedToHand.transform.position -
main_inter.attachedToHand.transform.position, mainHandUp);
}
//부모와 자식 포인트에 잡고있는 상태 (둘 다)
private void HandAttachedUpdate(Hand hand)
{
if (main_inter.attachedToHand)
{
if (main_inter.skeletonPoser)
{
Quaternion customHandPoseRotation =
main_inter.skeletonPoser.GetBlendedPose(main_inter.attachedToHand.skeleton).rotation;
main_inter.transform.rotation = GetTargetRotation() * secondRotationOffset * customHandPoseRotation;
}
else
{
main_inter.attachedToHand.objectAttachmentPoint.rotation = GetTargetRotation() * secondRotationOffset;
}
}
}
//자식 포인트에 닿고 있는 상태
private void HandHoverUpdate(Hand hand)
{
GrabTypes grabType = hand.GetGrabStarting();
bool isGrabEnding = hand.IsGrabEnding(gameObject);
//Grab
if (interactable.attachedToHand == null && grabType != GrabTypes.None)
{
hand.AttachObject(gameObject, grabType, 0);
hand.HoverLock(interactable);
hand.HideGrabHint();
secondRotationOffset = Quaternion.Inverse(GetTargetRotation()) * main_inter.attachedToHand.
currentAttachedObjectInfo.Value.handAttachmentPointTransform.rotation;
}
// Release
else if (isGrabEnding)
{
DebugUI.instance.debug_txt.text = "Release";
hand.DetachObject(gameObject);
hand.HoverUnlock(interactable);
}
}
}
------------------------------------------------------------------------------------------------------------------------------------------------
두 스크립트를 부모 자식에 넣고 각자 Interatable.cs 할당을 시켜주고 실행
이렇게 가상현실(3D공간) 안에서 양손으로 한 객체를 집어 드는 것을 구현했습니다.