355 lines
11 KiB
C#
355 lines
11 KiB
C#
using System;
|
|
using System.Collections;
|
|
using UnityEngine;
|
|
using UnityEngine.XR;
|
|
using UnityEngine.XR.Interaction.Toolkit;
|
|
using UnityEngine.XR.Interaction.Toolkit.Interactors;
|
|
|
|
/// <summary>
|
|
/// 컨트롤러의 손 방향(왼손/오른손)을 구분하기 위한 열거형
|
|
/// </summary>
|
|
public enum HandSide { Left, Right }
|
|
|
|
/// <summary>
|
|
/// VR 컨트롤러(왼손/오른손)의 상호작용을 담당하는 뷰 클래스
|
|
/// XR Interaction Toolkit의 이벤트를 수신하여 로봇 잡기, 포인트 클릭/드래그, 호버링 등의
|
|
/// 사용자 입력을 Presenter에게 전달. 또한 햅틱(진동) 피드백 기능을 제공
|
|
/// </summary>
|
|
|
|
public class InteractionView : MonoBehaviour
|
|
{
|
|
// --- 사용자 입력 이벤트 (ProgramPresenter로 전달됨) ---
|
|
public event Action<bool> OnRobotHoverStateChanged; // 로봇 위에 레이가 올라갔거나 나갔을 때
|
|
public event Action<Vector3> OnRobotGrabStart; // 로봇을 잡기 시작했을 때
|
|
public event Action<Vector3, Quaternion> OnRobotGrabbed; // 로봇을 잡고 움직이는 중일 때 (매 프레임)
|
|
public event Action<RobotData> OnRobotReleased; // 로봇을 놓았을 때
|
|
|
|
public event Action<int, Vector3> OnPointClicked; // 포인트를 짧게 클릭했을 때
|
|
public event Action<int> OnPointDragStart; // 포인트를 잡고 드래그를 시작했을 때
|
|
public event Action<int, Vector3, Quaternion> OnPointDragUpdate; // 포인트 드래그 중일 때
|
|
public event Action<int> OnPointDragEnd; // 포인트 드래그가 끝났을 때
|
|
|
|
// --- XR 컴포넌트 참조 ---
|
|
[SerializeField] private NearFarInteractor nearFarInteractor; // 근거리/원거리 상호작용 통합 컴포넌트
|
|
public HandSide handSide; // 이 스크립트가 붙은 손의 방향
|
|
|
|
private XRBaseInteractor baseInteractor; // 인터랙터 기본 클래스 (이벤트 구독용)
|
|
private IXRRayProvider rayProvider; // 레이(Ray) 정보 제공자 (끝점 좌표 확인용)
|
|
|
|
// --- 햅틱(진동) 관련 ---
|
|
private InputDevice hapticDevice; // 진동을 발생시킬 컨트롤러 하드웨어 장치
|
|
private bool isHapticDeviceFound = false;
|
|
|
|
// --- 시각적 피드백 ---
|
|
[SerializeField]
|
|
[Tooltip("드래그 시 위치를 표시할 반투명 로봇 모델")]
|
|
private GameObject ghostRobot;
|
|
|
|
[SerializeField]
|
|
[Tooltip("드래그용 마우스 이미지(화살표 등)")]
|
|
private GameObject dragArrow;
|
|
|
|
// --- 입력 판정 임계값 ---
|
|
[SerializeField] private float clickTimeThreshold = 0.5f; // 이 시간보다 짧으면 클릭, 길면 드래그로 판정
|
|
[SerializeField] private float dragMovementThreshold = 0.05f; // 이 거리 이상 움직이면 즉시 드래그로 판정
|
|
|
|
private Coroutine clickOrDragCoroutine = null; // 클릭/드래그 판정 타이머 코루틴
|
|
public Vector3 startGrabPosition; // 잡기 시작한 위치
|
|
public float distance; // 잡은 후 이동한 거리
|
|
|
|
// --- 상태 플래그 ---
|
|
private bool isInitialized = false;
|
|
private bool isGrabbingPoint = false; // 포인트를 잡고 있는가?
|
|
private int currentGrabbedPointIndex = -1; // 잡고 있는 포인트의 ID
|
|
|
|
public Vector3 currentTargetPosition = Vector3.zero; // 현재 레이가 닿은 위치
|
|
Quaternion currentTargetRotation = Quaternion.identity;
|
|
|
|
public bool isGrabbingRobot = false; // 로봇을 잡고 있는가?
|
|
|
|
void Update()
|
|
{
|
|
// 1. 초기화 로직 (XR 컴포넌트가 런타임에 준비될 때까지 대기)
|
|
if (!isInitialized || rayProvider == null)
|
|
{
|
|
if (nearFarInteractor == null)
|
|
{
|
|
nearFarInteractor = GetComponentInChildren<NearFarInteractor>(true);
|
|
baseInteractor = nearFarInteractor as XRBaseInteractor;
|
|
rayProvider = nearFarInteractor as IXRRayProvider;
|
|
}
|
|
if (nearFarInteractor != null)
|
|
{
|
|
InitializeInteraction();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// 2. 매 프레임 레이의 끝점과 회전값 갱신
|
|
currentTargetPosition = rayProvider.rayEndPoint;
|
|
currentTargetRotation = baseInteractor.attachTransform.rotation;
|
|
|
|
// 3. 로봇을 잡고 있는 경우 -> Grabbed 이벤트 계속 발생
|
|
if (isGrabbingRobot)
|
|
{
|
|
OnRobotGrabbed?.Invoke(currentTargetPosition, currentTargetRotation);
|
|
return;
|
|
}
|
|
|
|
// 4. 포인트를 잡고 있는 경우 -> DragUpdate 이벤트 계속 발생
|
|
if (isGrabbingPoint)
|
|
{
|
|
OnPointDragUpdate?.Invoke(currentGrabbedPointIndex, currentTargetPosition, currentTargetRotation);
|
|
}
|
|
// 5. 아직 판정 중인 경우 (클릭인지 드래그인지)
|
|
else if (clickOrDragCoroutine != null)
|
|
{
|
|
// 이동 거리 계산
|
|
distance = Vector3.Distance(startGrabPosition, currentTargetPosition);
|
|
|
|
// 임계값 이상 움직였다면 시간과 관계없이 즉시 드래그로 간주
|
|
if (distance > dragMovementThreshold)
|
|
{
|
|
StartDragMode();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// XR 인터랙터 이벤트를 구독하고 AppManager에 뷰를 등록
|
|
/// </summary>
|
|
private void InitializeInteraction()
|
|
{
|
|
// XRI 이벤트 구독
|
|
baseInteractor.selectEntered.AddListener(HandleGrabStart); // 버튼 누름 (Grab)
|
|
baseInteractor.selectExited.AddListener(HandleGrabEnd); // 버튼 뗌 (Release)
|
|
baseInteractor.hoverEntered.AddListener(OnHoverEntered); // 레이가 닿음
|
|
baseInteractor.hoverExited.AddListener(OnHoverExited); // 레이가 나감
|
|
|
|
TryInitializeHapticDevice(); // 진동 장치 초기화 시도
|
|
|
|
isInitialized = true;
|
|
|
|
// AppManager에 자신(View)을 등록하여 Presenter와 연결될 수 있게 함
|
|
AppManager.Instance.RegisterView(this);
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
if (baseInteractor != null)
|
|
{
|
|
baseInteractor.selectEntered.RemoveListener(HandleGrabStart);
|
|
baseInteractor.selectExited.RemoveListener(HandleGrabEnd);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 현재 손에 해당하는 XR 입력 장치(컨트롤러)를 찾아 햅틱 기능을 활성화
|
|
/// </summary>
|
|
private void TryInitializeHapticDevice()
|
|
{
|
|
XRNode node = (this.handSide == HandSide.Left) ? XRNode.LeftHand : XRNode.RightHand;
|
|
InputDevice device = InputDevices.GetDeviceAtXRNode(node);
|
|
|
|
if (device.isValid)
|
|
{
|
|
hapticDevice = device;
|
|
isHapticDeviceFound = true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// InputDevice를 사용한 햅틱
|
|
/// </summary>
|
|
public void SendHapticImpulse(float amplitude, float duration)
|
|
{
|
|
// 컨트롤러가 연결되지 않았거나(isValid) 꺼져있을 수 있으므로 매번 확인
|
|
if (!isHapticDeviceFound || !hapticDevice.isValid)
|
|
{
|
|
TryInitializeHapticDevice(); // 다시 찾기 시도
|
|
if (!isHapticDeviceFound) return; // 그래도 없으면 중단
|
|
}
|
|
|
|
// 진동 기능이 있는지 확인
|
|
HapticCapabilities capabilities;
|
|
if (hapticDevice.TryGetHapticCapabilities(out capabilities))
|
|
{
|
|
if (capabilities.supportsImpulse)
|
|
{
|
|
// 진동 전송
|
|
hapticDevice.SendHapticImpulse(0, amplitude, duration);
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- XRI 이벤트 핸들러 ---
|
|
/// <summary>
|
|
/// 오브젝트를 잡았을 때(Grip/Trigger 버튼 누름) 호출
|
|
/// 대상이 '포인트'인지 '로봇'인지 구분하여 처리
|
|
/// </summary>
|
|
private void HandleGrabStart(SelectEnterEventArgs args)
|
|
{
|
|
GameObject grabbedGO = args.interactableObject.transform.gameObject;
|
|
|
|
RobotPoint point = grabbedGO.GetComponent<RobotPoint>();
|
|
if (point != null)
|
|
{
|
|
isGrabbingPoint = true;
|
|
isGrabbingRobot = false;
|
|
currentGrabbedPointIndex = point.pointIndex;
|
|
|
|
// 타이머 시작
|
|
startGrabPosition = rayProvider.rayEndPoint;
|
|
if (clickOrDragCoroutine != null)
|
|
StopCoroutine(clickOrDragCoroutine);
|
|
clickOrDragCoroutine = StartCoroutine(ClickOrDragTimer());
|
|
}
|
|
else if (grabbedGO.CompareTag("RobotArm"))
|
|
{
|
|
isGrabbingPoint = false;
|
|
isGrabbingRobot = true;
|
|
currentGrabbedPointIndex = -1;
|
|
|
|
OnRobotGrabStart?.Invoke(startGrabPosition);
|
|
OnPointDragStart?.Invoke(currentGrabbedPointIndex); // (인덱스 -1 전달)
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 오브젝트를 놓았을 때(버튼 뗌) 호출
|
|
/// </summary>
|
|
private void HandleGrabEnd(SelectExitEventArgs args)
|
|
{
|
|
if (isGrabbingRobot)
|
|
{
|
|
OnRobotReleased?.Invoke(new RobotData());
|
|
}
|
|
else
|
|
{
|
|
if (clickOrDragCoroutine != null)
|
|
{
|
|
StopCoroutine(clickOrDragCoroutine);
|
|
clickOrDragCoroutine = null;
|
|
|
|
OnPointClicked?.Invoke(currentGrabbedPointIndex, currentTargetPosition);
|
|
}
|
|
else if (isGrabbingPoint)
|
|
{
|
|
OnPointDragEnd?.Invoke(currentGrabbedPointIndex);
|
|
}
|
|
}
|
|
|
|
// 상태 초기화
|
|
clickOrDragCoroutine = null;
|
|
isGrabbingPoint = false;
|
|
isGrabbingRobot = false;
|
|
currentGrabbedPointIndex = -1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 레이가 물체 위에 올라갔을 때 호출
|
|
/// </summary>
|
|
private void OnHoverEntered(HoverEnterEventArgs args)
|
|
{
|
|
// 호버한 물체가 '로봇'인지 확인
|
|
if (args.interactableObject.transform.CompareTag("RobotArm"))
|
|
{
|
|
OnRobotHoverStateChanged?.Invoke(true);
|
|
}
|
|
}
|
|
|
|
// 호버 종료 시 호출
|
|
private void OnHoverExited(HoverExitEventArgs args)
|
|
{
|
|
if (args.interactableObject.transform.CompareTag("RobotArm"))
|
|
{
|
|
OnRobotHoverStateChanged?.Invoke(false);
|
|
}
|
|
}
|
|
|
|
// 클릭/드래그 구분하는 타이머 코루틴
|
|
private IEnumerator ClickOrDragTimer()
|
|
{
|
|
yield return new WaitForSeconds(clickTimeThreshold);
|
|
|
|
if (clickOrDragCoroutine != null)
|
|
{
|
|
Debug.Log("시간 초과로 드래그 시작 (OnPointDragStart)");
|
|
StartDragMode();
|
|
}
|
|
}
|
|
|
|
// 타이머 중지하고 드래그 모드로 전환
|
|
private void StartDragMode()
|
|
{
|
|
if (clickOrDragCoroutine != null)
|
|
{
|
|
StopCoroutine(clickOrDragCoroutine);
|
|
clickOrDragCoroutine = null;
|
|
}
|
|
|
|
isGrabbingPoint = true;
|
|
OnPointDragStart?.Invoke(currentGrabbedPointIndex);
|
|
}
|
|
|
|
// 고스트 로봇의 위치/회전 설정
|
|
public void ShowGhostRobot()
|
|
{
|
|
if (ghostRobot == null)
|
|
{
|
|
Debug.LogWarning("Ghost Robot이 Inspector에 할당되지 않았습니다.");
|
|
return;
|
|
}
|
|
|
|
ghostRobot.SetActive(true);
|
|
}
|
|
|
|
// 고스트 로봇 비활성화
|
|
public void HideGhostRobot()
|
|
{
|
|
if (ghostRobot != null)
|
|
{
|
|
ghostRobot.SetActive(false);
|
|
}
|
|
}
|
|
|
|
// 드래그 화살표 활성화 및 위치 설정
|
|
public void ShowDragArrow(Vector3 position)
|
|
{
|
|
if (dragArrow == null)
|
|
{
|
|
Debug.LogWarning("Drag Arrow가 Inspector에 할당되지 않았습니다.");
|
|
return;
|
|
}
|
|
|
|
dragArrow.SetActive(true);
|
|
dragArrow.transform.position = position;
|
|
}
|
|
|
|
// 드래그 화살표 비활성화
|
|
public void HideDragArrow()
|
|
{
|
|
if (dragArrow != null)
|
|
{
|
|
dragArrow.SetActive(false);
|
|
}
|
|
}
|
|
|
|
// 광선 끝 위치 반환
|
|
public Vector3 GetCurrentRayPosition()
|
|
{
|
|
if (rayProvider != null)
|
|
{
|
|
return rayProvider.rayEndPoint;
|
|
}
|
|
|
|
if (baseInteractor != null)
|
|
{
|
|
return baseInteractor.attachTransform.position;
|
|
}
|
|
|
|
return transform.position;
|
|
}
|
|
}
|