Files
bansonic_beta_main/Assets/scripts/gamePlay_gameplay/TrackTouchInput.cs
T
2026-07-22 05:35:53 +08:00

306 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using UnityEngine;
#if ENABLE_INPUT_SYSTEM
using UnityEngine.InputSystem;
using UnityEngine.InputSystem.EnhancedTouch;
using ETouch = UnityEngine.InputSystem.EnhancedTouch.Touch;
#endif
/// <summary>
/// 手游触屏输入分发器:每帧遍历所有触摸点(及编辑器鼠标),用 Physics2D.OverlapPoint
/// 命中带 TrackTouchZone 的透明 Collider2D,把按下/抬起转发给 InputManager。
///
/// 【多点触控根因】项目 activeInputHandler = Both(同时启用新旧输入系统)。
/// 在这种配置下,旧版 UnityEngine.Input.touches 走的是兼容 shim,同时按多指时
/// 常常只上报一个触点 → 只能触发一个轨道。所以这里优先用新输入系统的
/// EnhancedTouchTouch.activeTouches),它能可靠上报所有并发手指;
/// 仅在没有新输入系统时回退到旧版 Input.touches。判定逻辑完全不变。
///
/// 为什么用物理命中而不是 UI 的 IPointerDownHandler
/// - 轨道会平移+旋转晃动,点击区必须跟随晃动的 ColliderOverlapPoint 支持旋转过的碰撞体)。
/// - 需要多点触控:每根手指独立按住各自的轨道(靠 touchId 配对按下/抬起)。
///
/// 判定时间戳仍由 Note.HandlePress 读取 GameplayClock.NowSongTimedspTime)。
///
/// 用法:场景里放一个空物体挂本脚本,把主相机拖到 gameplayCamera(留空则自动取 Camera.main)。
/// 5 块透明轨道点击区各挂 TrackTouchZone 并设置 trackIndex。
/// </summary>
public class TrackTouchInput : MonoBehaviour
{
[Tooltip("渲染 gameplay 的相机。留空则自动使用 Camera.main。")]
[SerializeField] private Camera gameplayCamera;
[Tooltip("是否在编辑器/PC 上用鼠标模拟单点触摸(方便调试)。")]
[SerializeField] private bool enableMouseFallback = true;
[Tooltip("命中检测使用的层。默认 Everything;如需只检测点击区层可在此限制。")]
[SerializeField] private LayerMask hitLayers = ~0;
[Tooltip("透视相机必填:轨道点击区所在平面的世界 Z 坐标(轨道 Collider 的 Z)。\n" +
"透视相机下屏幕点转世界点依赖深度,用它把射线交到轨道平面,相机怎么晃/推拉都精确。\n" +
"正交相机可忽略此项。")]
[SerializeField] private float trackPlaneWorldZ = 0f;
[Tooltip("相机是否为正交投影。正交相机忽略深度,命中转换更简单。\n" +
"留空自动按相机 orthographic 判断;一般不用手动改。")]
[SerializeField] private bool forceOrthographicMode = false;
// 触点 id -> 当前按住的轨道索引。用于在抬起/取消时精确释放对应轨道。
private readonly Dictionary<int, int> _activeTouches = new Dictionary<int, int>();
private int _mouseHeldTrack = -1;
private Camera Cam
{
get
{
if (gameplayCamera == null) gameplayCamera = Camera.main;
return gameplayCamera;
}
}
#if ENABLE_INPUT_SYSTEM
private void OnEnable()
{
// 开启增强触摸,Touch.activeTouches 才会被填充。
EnhancedTouchSupport.Enable();
}
#endif
private void Update()
{
#if UNITY_STANDALONE_WIN || UNITY_EDITOR_WIN
// Windows 平台:禁用轨道点击/触摸轮询,只依赖按键输入。
// 编辑器(Win)也一并禁用,便于在 PC 上以键盘方式测试真实 Windows 行为。
return;
#else
var im = InputManager.Instance;
if (im == null) return;
Camera cam = Cam;
if (cam == null) return;
int touchCount = ProcessTouches(im, cam);
if (enableMouseFallback && touchCount == 0)
{
ProcessMouse(im, cam);
}
#endif
}
/// <summary>处理所有并发触点,返回本帧触点数量。</summary>
private int ProcessTouches(InputManager im, Camera cam)
{
#if ENABLE_INPUT_SYSTEM
// 优先走新输入系统的增强触摸:可靠上报所有并发手指。
var touches = ETouch.activeTouches;
int count = touches.Count;
for (int i = 0; i < count; i++)
{
ETouch t = touches[i];
switch (t.phase)
{
case UnityEngine.InputSystem.TouchPhase.Began:
HandleTouchBegan(im, cam, t.touchId, t.screenPosition);
break;
case UnityEngine.InputSystem.TouchPhase.Ended:
case UnityEngine.InputSystem.TouchPhase.Canceled:
HandleTouchEnded(im, t.touchId);
break;
// Moved / Stationary:手指在轨道内保持按住即可,不重新判定归属。
}
}
return count;
#else
// 回退:旧版 Input.touches(仅在未启用新输入系统时使用)。
int count = Input.touchCount;
for (int i = 0; i < count; i++)
{
Touch t = Input.GetTouch(i);
switch (t.phase)
{
case TouchPhase.Began:
HandleTouchBegan(im, cam, t.fingerId, t.position);
break;
case TouchPhase.Ended:
case TouchPhase.Canceled:
HandleTouchEnded(im, t.fingerId);
break;
}
}
return count;
#endif
}
private void HandleTouchBegan(InputManager im, Camera cam, int touchId, Vector2 screenPos)
{
int track = ResolveTrack(cam, screenPos);
if (track >= 0)
{
_activeTouches[touchId] = track;
im.PressTrack(track);
}
}
private void HandleTouchEnded(InputManager im, int touchId)
{
if (_activeTouches.TryGetValue(touchId, out int track))
{
_activeTouches.Remove(touchId);
im.ReleaseTrack(track);
}
}
private void ProcessMouse(InputManager im, Camera cam)
{
#if ENABLE_INPUT_SYSTEM
var mouse = Mouse.current;
if (mouse == null) return;
if (mouse.leftButton.wasPressedThisFrame)
{
int track = ResolveTrack(cam, mouse.position.ReadValue());
if (track >= 0)
{
_mouseHeldTrack = track;
im.PressTrack(track);
}
}
else if (mouse.leftButton.wasReleasedThisFrame)
{
if (_mouseHeldTrack >= 0)
{
im.ReleaseTrack(_mouseHeldTrack);
_mouseHeldTrack = -1;
}
}
#else
if (Input.GetMouseButtonDown(0))
{
int track = ResolveTrack(cam, Input.mousePosition);
if (track >= 0)
{
_mouseHeldTrack = track;
im.PressTrack(track);
}
}
else if (Input.GetMouseButtonUp(0))
{
if (_mouseHeldTrack >= 0)
{
im.ReleaseTrack(_mouseHeldTrack);
_mouseHeldTrack = -1;
}
}
#endif
}
/// <summary>
/// 把屏幕坐标转换为轨道平面上的世界点,用 OverlapPoint 命中轨道点击区,返回轨道索引;无命中返回 -1。
///
/// 相机移动/晃动/推拉时,每帧都用当前相机状态转换,所以命中自动跟随相机。
/// - 正交相机:ScreenToWorldPoint 忽略深度,直接取 x/y。
/// - 透视相机:从相机发射线,求与轨道平面(Z = trackPlaneWorldZ)的交点。
/// </summary>
private int ResolveTrack(Camera cam, Vector2 screenPos)
{
EnsureOverlapFilter();
Vector2 point;
if (forceOrthographicMode || cam.orthographic)
{
Vector3 world = cam.ScreenToWorldPoint(screenPos);
point = new Vector2(world.x, world.y);
}
else
{
Ray ray = cam.ScreenPointToRay(screenPos);
float denom = ray.direction.z;
if (Mathf.Abs(denom) < 1e-6f)
{
return -1; // 射线与平面近乎平行,无有效交点
}
float t = (trackPlaneWorldZ - ray.origin.z) / denom;
if (t < 0f)
{
return -1; // 交点在相机背后
}
Vector3 world = ray.origin + ray.direction * t;
point = new Vector2(world.x, world.y);
}
// 用 OverlapPointAll 遍历所有命中的碰撞体,挑出带 TrackTouchZone 的那个。
// 关键修复:判定线附近同一屏幕点常密集着 note 的 Collider2D 与 judgeline 触发器,
// 单值版 OverlapPoint 只返回其中一个,若不是 tap zone 就丢判定(=点了轨道却没反应)。
// 遍历全部命中即可穿透这些遮挡,可靠找到轨道点击区。
int count = Physics2D.OverlapPoint(point, _overlapFilter, _overlapResults);
for (int i = 0; i < count; i++)
{
Collider2D hit = _overlapResults[i];
if (hit == null) continue;
TrackTouchZone zone = hit.GetComponent<TrackTouchZone>();
if (zone == null) zone = hit.GetComponentInParent<TrackTouchZone>();
if (zone != null) return zone.trackIndex;
}
// 兜底:没有精确命中任何 zone(点落在轨道间缝隙或紧邻边缘)。
// 取横向最近的 zone,只要触点在其 Collider 的纵向范围内即算命中——
// 等效于把相邻轨道点击区在横向补满、消除缝隙,且不误触远离轨道区的点击。
return ResolveNearestZone(point);
}
// 横向最近轨道兜底:在所有 zone 中找触点纵向落在其包围盒内、且横向距离最近的一个。
private int ResolveNearestZone(Vector2 point)
{
var zones = TrackTouchZone.All;
int best = -1;
float bestDist = float.MaxValue;
for (int i = 0; i < zones.Count; i++)
{
TrackTouchZone z = zones[i];
if (z == null || z.Collider == null) continue;
Bounds b = z.Collider.bounds;
// 纵向必须落在该点击区范围内(留一点容差),避免点屏幕别处也触发轨道。
if (point.y < b.min.y - 0.1f || point.y > b.max.y + 0.1f) continue;
float dx = Mathf.Abs(point.x - b.center.x);
if (dx < bestDist) { bestDist = dx; best = z.trackIndex; }
}
return best;
}
// OverlapPoint 的复用缓冲与过滤器(避免每次触摸分配 GC)。
private static readonly Collider2D[] _overlapResults = new Collider2D[16];
private ContactFilter2D _overlapFilter;
private bool _filterReady;
private void EnsureOverlapFilter()
{
if (_filterReady) return;
_overlapFilter = new ContactFilter2D();
_overlapFilter.useLayerMask = true;
_overlapFilter.SetLayerMask(hitLayers);
_overlapFilter.useTriggers = true; // tap zone 可能是触发器,需包含
_filterReady = true;
}
private void OnDisable()
{
// 组件被关闭/切场景时,释放所有还按住的轨道,避免音符卡在 held 状态。
var im = InputManager.Instance;
if (im != null)
{
foreach (var kv in _activeTouches)
{
im.ReleaseTrack(kv.Value);
}
if (_mouseHeldTrack >= 0) im.ReleaseTrack(_mouseHeldTrack);
}
_activeTouches.Clear();
_mouseHeldTrack = -1;
#if ENABLE_INPUT_SYSTEM
EnhancedTouchSupport.Disable();
#endif
}
}