253 lines
8.9 KiB
C#
253 lines
8.9 KiB
C#
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,同时按多指时
|
||
/// 常常只上报一个触点 → 只能触发一个轨道。所以这里优先用新输入系统的
|
||
/// EnhancedTouch(Touch.activeTouches),它能可靠上报所有并发手指;
|
||
/// 仅在没有新输入系统时回退到旧版 Input.touches。判定逻辑完全不变。
|
||
///
|
||
/// 为什么用物理命中而不是 UI 的 IPointerDownHandler:
|
||
/// - 轨道会平移+旋转晃动,点击区必须跟随晃动的 Collider(OverlapPoint 支持旋转过的碰撞体)。
|
||
/// - 需要多点触控:每根手指独立按住各自的轨道(靠 touchId 配对按下/抬起)。
|
||
///
|
||
/// 判定时间戳仍由 Note.HandlePress 读取 GameplayClock.NowSongTime(dspTime)。
|
||
///
|
||
/// 用法:场景里放一个空物体挂本脚本,把主相机拖到 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()
|
||
{
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// <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)
|
||
{
|
||
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);
|
||
}
|
||
|
||
Collider2D hit = Physics2D.OverlapPoint(point, hitLayers);
|
||
if (hit == null) return -1;
|
||
|
||
TrackTouchZone zone = hit.GetComponent<TrackTouchZone>();
|
||
if (zone == null) zone = hit.GetComponentInParent<TrackTouchZone>();
|
||
return zone != null ? zone.trackIndex : -1;
|
||
}
|
||
|
||
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
|
||
}
|
||
}
|