feat: polish VR gameplay and sync tools
This commit is contained in:
@@ -11,10 +11,20 @@ namespace VRBeats
|
||||
{
|
||||
[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);
|
||||
@@ -40,7 +50,8 @@ namespace VRBeats
|
||||
private void Start()
|
||||
{
|
||||
// Awake 이후 reflection으로 isRightHand가 설정되므로 Start에서 로그
|
||||
Debug.Log($"[VRPointer] Start — {gameObject.name} / isRightHand={isRightHand}");
|
||||
if (debugLogging)
|
||||
Debug.Log($"[VRPointer] Start — {gameObject.name} / isRightHand={isRightHand}");
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
@@ -56,11 +67,14 @@ namespace VRBeats
|
||||
private void Update()
|
||||
{
|
||||
// 3초마다 연결된 디바이스 목록 출력
|
||||
_deviceLogTimer += Time.deltaTime;
|
||||
if (_deviceLogTimer >= 3f)
|
||||
if (debugLogging)
|
||||
{
|
||||
_deviceLogTimer = 0f;
|
||||
LogConnectedDevices();
|
||||
_deviceLogTimer += Time.deltaTime;
|
||||
if (_deviceLogTimer >= 3f)
|
||||
{
|
||||
_deviceLogTimer = 0f;
|
||||
LogConnectedDevices();
|
||||
}
|
||||
}
|
||||
|
||||
bool trigger = GetButton(CommonUsages.triggerButton);
|
||||
@@ -70,31 +84,41 @@ namespace VRBeats
|
||||
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;
|
||||
|
||||
_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}] 조이스틱 클릭 눌림");
|
||||
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 hitDist = maxDistance;
|
||||
float selectableHitDist = maxDistance;
|
||||
float scrollHitDist = maxDistance;
|
||||
|
||||
Selectable hit = FindSelectableUnderRay(ray, ref hitDist);
|
||||
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 (hit != _currentHover)
|
||||
if (debugLogging && hit != _currentHover)
|
||||
{
|
||||
Debug.Log(hit != null
|
||||
? $"[VRPointer] HOVER → {hit.gameObject.name}"
|
||||
@@ -103,16 +127,21 @@ namespace VRBeats
|
||||
|
||||
UpdateHoverState(hit);
|
||||
|
||||
// 검지 트리거 또는 A/X 버튼으로 클릭
|
||||
if (triggerDown || primaryDown)
|
||||
if (triggerUp && _dragScrollRect != null)
|
||||
EndScrollDrag(hand, ray);
|
||||
|
||||
// 검지 트리거 또는 A/X 버튼으로 클릭.
|
||||
// ScrollRect 위의 검지 트리거는 드래그/클릭 판별을 위해 release 시점에 처리한다.
|
||||
if ((triggerDown && !beganScrollDrag) || primaryDown)
|
||||
{
|
||||
if (_currentHover != null)
|
||||
{
|
||||
string btn = triggerDown ? "검지 트리거" : (isRightHand ? "A" : "X");
|
||||
Debug.Log($"[VRPointer:{hand}] CLICK [{btn}] → {_currentHover.gameObject.name}");
|
||||
string btn = triggerDown && !beganScrollDrag ? "검지 트리거" : (isRightHand ? "A" : "X");
|
||||
if (debugLogging)
|
||||
Debug.Log($"[VRPointer:{hand}] CLICK [{btn}] → {_currentHover.gameObject.name}");
|
||||
Click(_currentHover);
|
||||
}
|
||||
else
|
||||
else if (debugLogging)
|
||||
{
|
||||
Debug.Log($"[VRPointer:{hand}] CLICK — 레이 아래 버튼 없음 " +
|
||||
$"pos={transform.position:F2} fwd={transform.forward:F2}");
|
||||
@@ -121,6 +150,12 @@ namespace VRBeats
|
||||
}
|
||||
|
||||
DrawLine(hitDist);
|
||||
|
||||
_prevTrigger = trigger;
|
||||
_prevGrip = grip;
|
||||
_prevPrimary = primary;
|
||||
_prevSecondary = secondary;
|
||||
_prevThumbstick = thumbstick;
|
||||
}
|
||||
|
||||
private void LogConnectedDevices()
|
||||
@@ -222,6 +257,7 @@ namespace VRBeats
|
||||
foreach (Selectable sel in Selectable.allSelectablesArray)
|
||||
{
|
||||
if (!sel.gameObject.activeInHierarchy || !sel.interactable) continue;
|
||||
if (!IsOnEnabledCanvas(sel)) continue;
|
||||
|
||||
var rt = sel.GetComponent<RectTransform>();
|
||||
if (rt == null) continue;
|
||||
@@ -260,6 +296,189 @@ namespace VRBeats
|
||||
return r >= 0f && r <= 1f && u >= 0f && u <= 1f;
|
||||
}
|
||||
|
||||
private static ScrollRect FindScrollRectUnderRay(Ray ray, ref float maxDist)
|
||||
{
|
||||
ScrollRect closest = null;
|
||||
float closestDist = maxDist;
|
||||
var all = Object.FindObjectsByType<ScrollRect>(FindObjectsSortMode.None);
|
||||
|
||||
foreach (ScrollRect scroll in all)
|
||||
{
|
||||
if (!scroll.isActiveAndEnabled) continue;
|
||||
if (!IsOnEnabledCanvas(scroll)) continue;
|
||||
|
||||
RectTransform rt = scroll.viewport != null
|
||||
? scroll.viewport
|
||||
: scroll.GetComponent<RectTransform>();
|
||||
if (rt == null) continue;
|
||||
|
||||
Vector3[] corners = new Vector3[4];
|
||||
rt.GetWorldCorners(corners);
|
||||
|
||||
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 = scroll;
|
||||
}
|
||||
|
||||
if (closest != null)
|
||||
maxDist = closestDist;
|
||||
|
||||
return closest;
|
||||
}
|
||||
|
||||
private static bool IsOnEnabledCanvas(Component component)
|
||||
{
|
||||
Canvas[] canvases = component.GetComponentsInParent<Canvas>(true);
|
||||
if (canvases.Length == 0)
|
||||
return true;
|
||||
|
||||
for (int i = 0; i < canvases.Length; i++)
|
||||
{
|
||||
Canvas canvas = canvases[i];
|
||||
if (canvas == null)
|
||||
continue;
|
||||
|
||||
if (!canvas.enabled || !canvas.gameObject.activeInHierarchy)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void HandleScroll(ScrollRect scrollRect)
|
||||
{
|
||||
if (!CanScrollVertically(scrollRect))
|
||||
return;
|
||||
|
||||
Vector2 axis = GetAxis(CommonUsages.primary2DAxis);
|
||||
if (Mathf.Abs(axis.y) < scrollDeadZone)
|
||||
return;
|
||||
|
||||
scrollRect.verticalNormalizedPosition = Mathf.Clamp01(
|
||||
scrollRect.verticalNormalizedPosition + axis.y * scrollSpeed * Time.deltaTime);
|
||||
}
|
||||
|
||||
private bool TryBeginScrollDrag(ScrollRect scrollRect, Selectable pressSelectable, Ray ray)
|
||||
{
|
||||
if (!CanScrollVertically(scrollRect))
|
||||
return false;
|
||||
|
||||
if (!TryGetScrollLocalPoint(scrollRect, ray, out Vector2 localPoint, out _))
|
||||
return false;
|
||||
|
||||
_dragScrollRect = scrollRect;
|
||||
_triggerPressSelectable = pressSelectable;
|
||||
_dragStartLocalPoint = localPoint;
|
||||
_dragStartNormalizedPosition = scrollRect.verticalNormalizedPosition;
|
||||
_dragMaxNormalizedDelta = 0f;
|
||||
return true;
|
||||
}
|
||||
|
||||
private void UpdateScrollDrag(Ray ray)
|
||||
{
|
||||
if (_dragScrollRect == null)
|
||||
return;
|
||||
|
||||
if (!TryGetScrollLocalPoint(_dragScrollRect, ray, out Vector2 localPoint, out float viewportHeight))
|
||||
return;
|
||||
|
||||
float deltaY = localPoint.y - _dragStartLocalPoint.y;
|
||||
float normalizedDelta = deltaY / viewportHeight * dragScrollSpeed;
|
||||
_dragMaxNormalizedDelta = Mathf.Max(_dragMaxNormalizedDelta, Mathf.Abs(normalizedDelta));
|
||||
|
||||
_dragScrollRect.verticalNormalizedPosition = Mathf.Clamp01(
|
||||
_dragStartNormalizedPosition - normalizedDelta);
|
||||
}
|
||||
|
||||
private void EndScrollDrag(string hand, Ray ray)
|
||||
{
|
||||
bool shouldClick = _dragMaxNormalizedDelta < dragClickThreshold;
|
||||
ScrollRect scrollRect = _dragScrollRect;
|
||||
Selectable pressSelectable = _triggerPressSelectable;
|
||||
float startNormalizedPosition = _dragStartNormalizedPosition;
|
||||
|
||||
ClearScrollDrag();
|
||||
|
||||
if (!shouldClick)
|
||||
return;
|
||||
|
||||
if (scrollRect != null)
|
||||
scrollRect.verticalNormalizedPosition = startNormalizedPosition;
|
||||
|
||||
if (pressSelectable != null && pressSelectable.isActiveAndEnabled && pressSelectable.interactable)
|
||||
{
|
||||
if (debugLogging)
|
||||
Debug.Log($"[VRPointer:{hand}] CLICK [검지 트리거] → {pressSelectable.gameObject.name}");
|
||||
Click(pressSelectable);
|
||||
}
|
||||
else if (debugLogging)
|
||||
{
|
||||
Debug.Log($"[VRPointer:{hand}] CLICK — 레이 아래 버튼 없음 " +
|
||||
$"pos={transform.position:F2} fwd={transform.forward:F2}");
|
||||
DebugRaycastAttempt(ray);
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearScrollDrag()
|
||||
{
|
||||
_dragScrollRect = null;
|
||||
_triggerPressSelectable = null;
|
||||
_dragStartLocalPoint = Vector2.zero;
|
||||
_dragStartNormalizedPosition = 0f;
|
||||
_dragMaxNormalizedDelta = 0f;
|
||||
}
|
||||
|
||||
private static bool CanScrollVertically(ScrollRect scrollRect)
|
||||
{
|
||||
if (scrollRect == null || !scrollRect.vertical)
|
||||
return false;
|
||||
|
||||
RectTransform viewport = scrollRect.viewport != null
|
||||
? scrollRect.viewport
|
||||
: scrollRect.GetComponent<RectTransform>();
|
||||
|
||||
if (viewport == null || scrollRect.content == null)
|
||||
return true;
|
||||
|
||||
return scrollRect.content.rect.height > viewport.rect.height + 1f;
|
||||
}
|
||||
|
||||
private static bool TryGetScrollLocalPoint(ScrollRect scrollRect, Ray ray, out Vector2 localPoint, out float viewportHeight)
|
||||
{
|
||||
localPoint = Vector2.zero;
|
||||
viewportHeight = 1f;
|
||||
|
||||
RectTransform rt = scrollRect.viewport != null
|
||||
? scrollRect.viewport
|
||||
: scrollRect.GetComponent<RectTransform>();
|
||||
if (rt == null)
|
||||
return false;
|
||||
|
||||
Vector3[] corners = new Vector3[4];
|
||||
rt.GetWorldCorners(corners);
|
||||
|
||||
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) || dist <= 0f)
|
||||
return false;
|
||||
|
||||
Vector3 local = rt.InverseTransformPoint(ray.GetPoint(dist));
|
||||
localPoint = new Vector2(local.x, local.y);
|
||||
viewportHeight = Mathf.Max(1f, rt.rect.height);
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool GetButton(InputFeatureUsage<bool> usage)
|
||||
{
|
||||
var chars = InputDeviceCharacteristics.Controller |
|
||||
@@ -274,5 +493,20 @@ namespace VRBeats
|
||||
devices[0].TryGetFeatureValue(usage, out bool pressed);
|
||||
return pressed;
|
||||
}
|
||||
|
||||
private Vector2 GetAxis(InputFeatureUsage<Vector2> usage)
|
||||
{
|
||||
var chars = InputDeviceCharacteristics.Controller |
|
||||
(isRightHand
|
||||
? InputDeviceCharacteristics.Right
|
||||
: InputDeviceCharacteristics.Left);
|
||||
|
||||
var devices = new List<InputDevice>();
|
||||
InputDevices.GetDevicesWithCharacteristics(chars, devices);
|
||||
if (devices.Count == 0) return Vector2.zero;
|
||||
|
||||
devices[0].TryGetFeatureValue(usage, out Vector2 axis);
|
||||
return axis;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user