Files
bansonic_beta_main/Assets/scripts/gamePlay_gameplay/TrackTouchInput.cs
T
2026-07-16 23:04:59 +08:00

253 lines
8.9 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()
{
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
}
}