using System; using System.Collections; using UnityEngine; using UnityEngine.XR; using UnityEngine.XR.Interaction.Toolkit; using UnityEngine.XR.Interaction.Toolkit.Interactors; /// /// ÄÁÆ®·Ñ·¯ÀÇ ¼Õ ¹æÇâ(¿Þ¼Õ/¿À¸¥¼Õ)À» ±¸ºÐÇϱâ À§ÇÑ ¿­°ÅÇü /// public enum HandSide { Left, Right } /// /// VR ÄÁÆ®·Ñ·¯(¿Þ¼Õ/¿À¸¥¼Õ)ÀÇ »óÈ£ÀÛ¿ëÀ» ´ã´çÇÏ´Â ºä Ŭ·¡½º /// XR Interaction ToolkitÀÇ À̺¥Æ®¸¦ ¼ö½ÅÇÏ¿© ·Îº¿ Àâ±â, Æ÷ÀÎÆ® Ŭ¸¯/µå·¡±×, È£¹ö¸µ µîÀÇ /// »ç¿ëÀÚ ÀÔ·ÂÀ» Presenter¿¡°Ô Àü´Þ. ¶ÇÇÑ ÇÝÆ½(Áøµ¿) Çǵå¹é ±â´ÉÀ» Á¦°ø /// public class InteractionView : MonoBehaviour { // --- »ç¿ëÀÚ ÀÔ·Â À̺¥Æ® (ProgramPresenter·Î Àü´ÞµÊ) --- public event Action OnRobotHoverStateChanged; // ·Îº¿ À§¿¡ ·¹À̰¡ ¿Ã¶ó°¬°Å³ª ³ª°¬À» ¶§ public event Action OnRobotGrabStart; // ·Îº¿À» Àâ±â ½ÃÀÛÇßÀ» ¶§ public event Action OnRobotGrabbed; // ·Îº¿À» Àâ°í ¿òÁ÷ÀÌ´Â ÁßÀÏ ¶§ (¸Å ÇÁ·¹ÀÓ) public event Action OnRobotReleased; // ·Îº¿À» ³õ¾ÒÀ» ¶§ public event Action OnPointClicked; // Æ÷ÀÎÆ®¸¦ ª°Ô Ŭ¸¯ÇßÀ» ¶§ public event Action OnPointDragStart; // Æ÷ÀÎÆ®¸¦ Àâ°í µå·¡±×¸¦ ½ÃÀÛÇßÀ» ¶§ public event Action OnPointDragUpdate; // Æ÷ÀÎÆ® µå·¡±× ÁßÀÏ ¶§ public event Action 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(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(); } } } } /// /// XR ÀÎÅÍ·¢ÅÍ À̺¥Æ®¸¦ ±¸µ¶Çϰí AppManager¿¡ ºä¸¦ µî·Ï /// 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); } } /// /// ÇöÀç ¼Õ¿¡ ÇØ´çÇÏ´Â XR ÀÔ·Â ÀåÄ¡(ÄÁÆ®·Ñ·¯)¸¦ ã¾Æ ÇÝÆ½ ±â´ÉÀ» Ȱ¼ºÈ­ /// 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; } } /// /// InputDevice¸¦ »ç¿ëÇÑ ÇÝÆ½ /// 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 À̺¥Æ® Çڵ鷯 --- /// /// ¿ÀºêÁ§Æ®¸¦ Àâ¾ÒÀ» ¶§(Grip/Trigger ¹öư ´©¸§) È£Ãâ /// ´ë»óÀÌ 'Æ÷ÀÎÆ®'ÀÎÁö '·Îº¿'ÀÎÁö ±¸ºÐÇÏ¿© ó¸® /// private void HandleGrabStart(SelectEnterEventArgs args) { GameObject grabbedGO = args.interactableObject.transform.gameObject; RobotPoint point = grabbedGO.GetComponent(); 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 Àü´Þ) } } /// /// ¿ÀºêÁ§Æ®¸¦ ³õ¾ÒÀ» ¶§(¹öư ¶À) È£Ãâ /// 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; } /// /// ·¹À̰¡ ¹°Ã¼ À§¿¡ ¿Ã¶ó°¬À» ¶§ È£Ãâ /// 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; } }