Files
bansonic_beta_main/Assets/scripts/gamePlay_gameplay/TrackTouchInput.cs
T

183 lines
6.8 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;
/// <summary>
/// 手游触屏输入分发器:每帧遍历所有触摸点(及编辑器鼠标),用 Physics2D.OverlapPoint
/// 命中带 TrackTouchZone 的透明 Collider2D,把按下/抬起转发给 InputManager。
///
/// 为什么用这种方式而不是 UI 的 IPointerDownHandler
/// - 轨道会平移+旋转晃动,点击区必须跟随晃动的 ColliderOverlapPoint 支持旋转过的碰撞体)。
/// - 需要多点触控:每根手指独立按住各自的轨道(靠 fingerId 配对按下/抬起)。
///
/// 判定逻辑完全不变:这里只调用现成的 PressTrack/ReleaseTrack
/// 判定时间戳仍由 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;
// fingerId -> 当前按住的轨道索引。用于在抬起/取消时精确释放对应轨道。
private readonly Dictionary<int, int> _activeTouches = new Dictionary<int, int>();
// 鼠标模拟用的"手指 id",取一个不会和真实 fingerId 冲突的值。
private const int MouseFingerId = -100;
private int _mouseHeldTrack = -1;
private Camera Cam
{
get
{
if (gameplayCamera == null) gameplayCamera = Camera.main;
return gameplayCamera;
}
}
private void Update()
{
var im = InputManager.Instance;
if (im == null) return;
Camera cam = Cam;
if (cam == null) return;
ProcessTouches(im, cam);
if (enableMouseFallback && Input.touchCount == 0)
{
ProcessMouse(im, cam);
}
}
private void ProcessTouches(InputManager im, Camera cam)
{
for (int i = 0; i < Input.touchCount; i++)
{
Touch t = Input.GetTouch(i);
switch (t.phase)
{
case TouchPhase.Began:
{
int track = ResolveTrack(cam, t.position);
if (track >= 0)
{
_activeTouches[t.fingerId] = track;
im.PressTrack(track);
}
break;
}
case TouchPhase.Ended:
case TouchPhase.Canceled:
{
if (_activeTouches.TryGetValue(t.fingerId, out int track))
{
_activeTouches.Remove(t.fingerId);
im.ReleaseTrack(track);
}
break;
}
// Moved / Stationary:手指在轨道内保持按住即可,不重新判定归属,
// 避免手指轻微滑动跨到相邻轨道时反复 Press/Release 造成误判。
}
}
}
private void ProcessMouse(InputManager im, Camera cam)
{
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;
}
}
}
/// <summary>
/// 把屏幕坐标转换为轨道平面上的世界点,用 OverlapPoint 命中轨道点击区,返回轨道索引;无命中返回 -1。
///
/// 相机移动/晃动/推拉时,每帧都用当前相机状态转换,所以命中自动跟随相机。
/// - 正交相机:ScreenToWorldPoint 忽略深度,直接取 x/y。
/// - 透视相机:从相机发射线,求与轨道平面(Z = trackPlaneWorldZ)的交点,
/// 避免因相机 Z 变化(如 cameraDash)导致命中点偏移。
/// </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
{
// 透视:射线与轨道平面(法线 +Z,过 z = trackPlaneWorldZ)求交。
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;
}
}