279 lines
11 KiB
C#
279 lines
11 KiB
C#
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;
|
|
|
|
private LineRenderer _line;
|
|
private bool _prevTrigger;
|
|
private Selectable _currentHover;
|
|
|
|
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<LineRenderer>();
|
|
_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에서 로그
|
|
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초마다 연결된 디바이스 목록 출력
|
|
_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 gripDown = grip && !_prevGrip;
|
|
bool primaryDown = primary && !_prevPrimary;
|
|
bool secondaryDown = secondary && !_prevSecondary;
|
|
bool thumbstickDown = thumbstick && !_prevThumbstick;
|
|
|
|
_prevTrigger = trigger;
|
|
_prevGrip = grip;
|
|
_prevPrimary = primary;
|
|
_prevSecondary = secondary;
|
|
_prevThumbstick = thumbstick;
|
|
|
|
string hand = isRightHand ? "R" : "L";
|
|
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 hitDist = maxDistance;
|
|
|
|
Selectable hit = FindSelectableUnderRay(ray, ref hitDist);
|
|
|
|
// 호버 변화 로그
|
|
if (hit != _currentHover)
|
|
{
|
|
Debug.Log(hit != null
|
|
? $"[VRPointer] HOVER → {hit.gameObject.name}"
|
|
: $"[VRPointer] HOVER → (없음)");
|
|
}
|
|
|
|
UpdateHoverState(hit);
|
|
|
|
// 검지 트리거 또는 A/X 버튼으로 클릭
|
|
if (triggerDown || primaryDown)
|
|
{
|
|
if (_currentHover != null)
|
|
{
|
|
string btn = triggerDown ? "검지 트리거" : (isRightHand ? "A" : "X");
|
|
Debug.Log($"[VRPointer:{hand}] CLICK [{btn}] → {_currentHover.gameObject.name}");
|
|
Click(_currentHover);
|
|
}
|
|
else
|
|
{
|
|
Debug.Log($"[VRPointer:{hand}] CLICK — 레이 아래 버튼 없음 " +
|
|
$"pos={transform.position:F2} fwd={transform.forward:F2}");
|
|
DebugRaycastAttempt(new Ray(transform.position, transform.forward));
|
|
}
|
|
}
|
|
|
|
DrawLine(hitDist);
|
|
}
|
|
|
|
private void LogConnectedDevices()
|
|
{
|
|
var all = new List<InputDevice>();
|
|
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<InputDevice>();
|
|
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<Button>();
|
|
if (btn != null) btn.onClick.Invoke();
|
|
}
|
|
|
|
private void DrawLine(float hitDist)
|
|
{
|
|
Color c = _currentHover != null ? HoverColor : NormalColor;
|
|
_line.startColor = c;
|
|
_line.endColor = new Color(c.r, c.g, c.b, 0f);
|
|
_line.SetPosition(0, transform.position);
|
|
_line.SetPosition(1, transform.position + transform.forward * hitDist);
|
|
}
|
|
|
|
private static void DebugRaycastAttempt(Ray ray)
|
|
{
|
|
var all = Selectable.allSelectablesArray;
|
|
Debug.Log($"[VRPointer:DEBUG] Selectable 총 {all.Length}개 | ray.origin={ray.origin:F2} ray.dir={ray.direction:F2}");
|
|
|
|
foreach (Selectable sel in all)
|
|
{
|
|
if (!sel.gameObject.activeInHierarchy) { Debug.Log($" SKIP(비활성) {sel.gameObject.name}"); continue; }
|
|
if (!sel.interactable) { Debug.Log($" SKIP(interactable=false) {sel.gameObject.name}"); continue; }
|
|
|
|
var rt = sel.GetComponent<RectTransform>();
|
|
if (rt == null) { Debug.Log($" SKIP(RectTransform없음) {sel.gameObject.name}"); continue; }
|
|
|
|
Vector3[] c = new Vector3[4];
|
|
rt.GetWorldCorners(c);
|
|
|
|
Vector3 normal = rt.forward;
|
|
if (Vector3.Dot(normal, ray.direction) >= 0f) normal = -normal;
|
|
|
|
Plane plane = new Plane(normal, c[0]);
|
|
bool hit = plane.Raycast(ray, out float dist);
|
|
if (!hit) { Debug.Log($" MISS(Plane.Raycast=false) {sel.gameObject.name} | rtPos={rt.position:F2} normal={normal:F2}"); continue; }
|
|
|
|
bool inRect = IsPointInRect(ray.GetPoint(dist), c);
|
|
Debug.Log($" {(inRect ? "HIT" : "MISS(InRect=false)")} {sel.gameObject.name} | dist={dist:F2} inRect={inRect}");
|
|
}
|
|
}
|
|
|
|
private static Selectable FindSelectableUnderRay(Ray ray, ref float maxDist)
|
|
{
|
|
Selectable closest = null;
|
|
float closestDist = maxDist;
|
|
|
|
foreach (Selectable sel in Selectable.allSelectablesArray)
|
|
{
|
|
if (!sel.gameObject.activeInHierarchy || !sel.interactable) continue;
|
|
|
|
var rt = sel.GetComponent<RectTransform>();
|
|
if (rt == null) continue;
|
|
|
|
Vector3[] corners = new Vector3[4];
|
|
rt.GetWorldCorners(corners);
|
|
|
|
// Plane.Raycast은 normal 쪽(앞면)에서 레이가 출발해야 true 반환.
|
|
// rt.forward가 카메라 반대를 향할 수 있으므로 레이 원점 방향으로 노말 보정.
|
|
Vector3 normal = rt.forward;
|
|
if (Vector3.Dot(normal, ray.direction) >= 0f)
|
|
normal = -normal;
|
|
|
|
Plane plane = new Plane(normal, corners[0]);
|
|
if (!plane.Raycast(ray, out float dist)) continue;
|
|
if (dist >= closestDist || dist <= 0f) continue;
|
|
|
|
if (!IsPointInRect(ray.GetPoint(dist), corners)) continue;
|
|
|
|
closestDist = dist;
|
|
closest = sel;
|
|
}
|
|
|
|
maxDist = closestDist;
|
|
return closest;
|
|
}
|
|
|
|
private static bool IsPointInRect(Vector3 p, Vector3[] c)
|
|
{
|
|
Vector3 toP = p - c[0];
|
|
Vector3 right = c[3] - c[0];
|
|
Vector3 up = c[1] - c[0];
|
|
|
|
float r = Vector3.Dot(toP, right) / right.sqrMagnitude;
|
|
float u = Vector3.Dot(toP, up) / up.sqrMagnitude;
|
|
return r >= 0f && r <= 1f && u >= 0f && u <= 1f;
|
|
}
|
|
|
|
private bool GetButton(InputFeatureUsage<bool> usage)
|
|
{
|
|
var chars = InputDeviceCharacteristics.Controller |
|
|
(isRightHand
|
|
? InputDeviceCharacteristics.Right
|
|
: InputDeviceCharacteristics.Left);
|
|
|
|
var devices = new List<InputDevice>();
|
|
InputDevices.GetDevicesWithCharacteristics(chars, devices);
|
|
if (devices.Count == 0) return false;
|
|
|
|
devices[0].TryGetFeatureValue(usage, out bool pressed);
|
|
return pressed;
|
|
}
|
|
}
|
|
}
|