using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; using UnityEngine.XR; namespace VRBeats { [RequireComponent(typeof(LineRenderer))] public class VRPointerController : MonoBehaviour { [SerializeField] private bool isRightHand = true; [SerializeField] private float maxDistance = 50f; [SerializeField] private bool debugLogging = false; [SerializeField] private float scrollSpeed = 2.4f; [SerializeField] private float scrollDeadZone = 0.15f; [SerializeField] private float dragScrollSpeed = 1.25f; [SerializeField] private float dragClickThreshold = 0.025f; private LineRenderer _line; private bool _prevTrigger; private Selectable _currentHover; private ScrollRect _dragScrollRect; private Selectable _triggerPressSelectable; private Vector2 _dragStartLocalPoint; private float _dragStartNormalizedPosition; private float _dragMaxNormalizedDelta; private static readonly Color NormalColor = new Color(1f, 1f, 1f, 0.8f); private static readonly Color HoverColor = new Color(0.3f, 0.8f, 1f, 1f); private float _deviceLogTimer; // 버튼별 이전 상태 private bool _prevGrip; private bool _prevPrimary; private bool _prevSecondary; private bool _prevThumbstick; private void Awake() { _line = GetComponent(); _line.positionCount = 2; _line.startWidth = 0.005f; _line.endWidth = 0.001f; _line.useWorldSpace = true; // enabled 상태는 OnEnable/OnDisable이 관리 — Awake에서 건드리지 않음 } private void Start() { // Awake 이후 reflection으로 isRightHand가 설정되므로 Start에서 로그 if (debugLogging) Debug.Log($"[VRPointer] Start — {gameObject.name} / isRightHand={isRightHand}"); } private void OnEnable() { if (_line != null) _line.enabled = true; } private void OnDisable() { if (_line != null) _line.enabled = false; } private void Update() { // 3초마다 연결된 디바이스 목록 출력 if (debugLogging) { _deviceLogTimer += Time.deltaTime; if (_deviceLogTimer >= 3f) { _deviceLogTimer = 0f; LogConnectedDevices(); } } bool trigger = GetButton(CommonUsages.triggerButton); bool grip = GetButton(CommonUsages.gripButton); bool primary = GetButton(CommonUsages.primaryButton); bool secondary = GetButton(CommonUsages.secondaryButton); bool thumbstick = GetButton(CommonUsages.primary2DAxisClick); bool triggerDown = trigger && !_prevTrigger; bool triggerUp = !trigger && _prevTrigger; bool gripDown = grip && !_prevGrip; bool primaryDown = primary && !_prevPrimary; bool secondaryDown = secondary && !_prevSecondary; bool thumbstickDown = thumbstick && !_prevThumbstick; string hand = isRightHand ? "R" : "L"; if (debugLogging) { if (triggerDown) Debug.Log($"[VRPointer:{hand}] 검지 트리거 눌림"); if (gripDown) Debug.Log($"[VRPointer:{hand}] 그립(중지) 눌림"); if (primaryDown) Debug.Log($"[VRPointer:{hand}] {(isRightHand ? "A" : "X")} 버튼 눌림"); if (secondaryDown) Debug.Log($"[VRPointer:{hand}] {(isRightHand ? "B" : "Y")} 버튼 눌림"); if (thumbstickDown) Debug.Log($"[VRPointer:{hand}] 조이스틱 클릭 눌림"); } Ray ray = new Ray(transform.position, transform.forward); float selectableHitDist = maxDistance; float scrollHitDist = maxDistance; Selectable hit = FindSelectableUnderRay(ray, ref selectableHitDist); ScrollRect scrollRect = FindScrollRectUnderRay(ray, ref scrollHitDist); float hitDist = Mathf.Min(selectableHitDist, scrollHitDist); bool beganScrollDrag = false; if (triggerDown) beganScrollDrag = TryBeginScrollDrag(scrollRect, hit, ray); if (_dragScrollRect != null && trigger) UpdateScrollDrag(ray); else if (_dragScrollRect == null) HandleScroll(scrollRect); // 호버 변화 로그 if (debugLogging && hit != _currentHover) { Debug.Log(hit != null ? $"[VRPointer] HOVER → {hit.gameObject.name}" : $"[VRPointer] HOVER → (없음)"); } UpdateHoverState(hit); if (triggerUp && _dragScrollRect != null) EndScrollDrag(hand, ray); // 검지 트리거 또는 A/X 버튼으로 클릭. // ScrollRect 위의 검지 트리거는 드래그/클릭 판별을 위해 release 시점에 처리한다. if ((triggerDown && !beganScrollDrag) || primaryDown) { if (_currentHover != null) { string btn = triggerDown && !beganScrollDrag ? "검지 트리거" : (isRightHand ? "A" : "X"); if (debugLogging) Debug.Log($"[VRPointer:{hand}] CLICK [{btn}] → {_currentHover.gameObject.name}"); Click(_currentHover); } else if (debugLogging) { Debug.Log($"[VRPointer:{hand}] CLICK — 레이 아래 버튼 없음 " + $"pos={transform.position:F2} fwd={transform.forward:F2}"); DebugRaycastAttempt(new Ray(transform.position, transform.forward)); } } DrawLine(hitDist); _prevTrigger = trigger; _prevGrip = grip; _prevPrimary = primary; _prevSecondary = secondary; _prevThumbstick = thumbstick; } private void LogConnectedDevices() { var all = new List(); InputDevices.GetDevices(all); if (all.Count == 0) { Debug.LogWarning($"[VRPointer] 연결된 XR 디바이스 없음 ({gameObject.name})"); return; } var chars = InputDeviceCharacteristics.Controller | (isRightHand ? InputDeviceCharacteristics.Right : InputDeviceCharacteristics.Left); var matched = new List(); InputDevices.GetDevicesWithCharacteristics(chars, matched); Debug.Log($"[VRPointer] 전체 디바이스 {all.Count}개 | " + $"{(isRightHand ? "오른손" : "왼손")} 컨트롤러 {matched.Count}개 ({gameObject.name})"); } private void UpdateHoverState(Selectable hit) { if (hit == _currentHover) return; var es = EventSystem.current; if (_currentHover != null) ExecuteEvents.Execute(_currentHover.gameObject, new PointerEventData(es), ExecuteEvents.pointerExitHandler); _currentHover = hit; if (_currentHover != null) ExecuteEvents.Execute(_currentHover.gameObject, new PointerEventData(es), ExecuteEvents.pointerEnterHandler); } private static void Click(Selectable sel) { var es = EventSystem.current; if (es == null) { Debug.LogWarning("[VRPointer] EventSystem.current is null — click 실패"); return; } var eventData = new PointerEventData(es); ExecuteEvents.Execute(sel.gameObject, eventData, ExecuteEvents.pointerDownHandler); ExecuteEvents.Execute(sel.gameObject, eventData, ExecuteEvents.pointerUpHandler); ExecuteEvents.Execute(sel.gameObject, eventData, ExecuteEvents.pointerClickHandler); var btn = sel.GetComponent