some updates

This commit is contained in:
FloatGaming
2026-07-25 08:31:10 +08:00
parent af8a0b6810
commit 86351dbd2a
225 changed files with 46757 additions and 590 deletions
@@ -7,124 +7,111 @@ 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。
/// Mobile touch dispatcher for the five gameplay tracks.
/// PC builds disable this component immediately and use keyboard input only.
/// </summary>
public class TrackTouchInput : MonoBehaviour
{
[Tooltip("渲染 gameplay 的相机。留空则自动使用 Camera.main。")]
[SerializeField] private Camera gameplayCamera;
[Tooltip("是否在编辑器/PC 上用鼠标模拟单点触摸(方便调试)。")]
[SerializeField] private bool enableMouseFallback = true;
[Tooltip("命中检测使用的层。默认 Everything;如需只检测点击区层可在此限制。")]
[SerializeField] private bool enableMouseFallback = false;
[Tooltip("推荐:给 5 个 TrackTouchZone 物体单独设一个 layer(比如 layer 10 TouchZone),这里只勾该 layer,避免音符/背景碰撞体挤爆缓冲导致间歇漏点")]
[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 readonly Dictionary<int, int> activeTouches = new Dictionary<int, int>();
private int mouseHeldTrack = -1;
// 缓冲从 16 扩到 64:密集音符流量下防止 TrackTouchZone 被挤出 OverlapPoint 结果
private static readonly Collider2D[] overlapResults = new Collider2D[64];
private ContactFilter2D overlapFilter;
private bool filterReady;
private Camera Cam
{
get
{
if (gameplayCamera == null) gameplayCamera = Camera.main;
if (gameplayCamera == null)
gameplayCamera = Camera.main;
return gameplayCamera;
}
}
private static bool ShouldProcessTouchInput()
{
#if UNITY_ANDROID || UNITY_IOS
return true;
#else
return false;
#endif
}
private void Awake()
{
if (!ShouldProcessTouchInput())
enabled = false;
}
#if ENABLE_INPUT_SYSTEM
private void OnEnable()
{
// 开启增强触摸,Touch.activeTouches 才会被填充。
if (!ShouldProcessTouchInput())
return;
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;
if (!ShouldProcessTouchInput())
return;
InputManager inputManager = InputManager.Instance;
Camera cam = Cam;
if (cam == null) return;
int touchCount = ProcessTouches(im, cam);
if (inputManager == null || cam == null)
return;
int touchCount = ProcessTouches(inputManager, cam);
if (enableMouseFallback && touchCount == 0)
{
ProcessMouse(im, cam);
ProcessMouse(inputManager, cam);
}
#endif
}
/// <summary>处理所有并发触点,返回本帧触点数量。</summary>
private int ProcessTouches(InputManager im, Camera cam)
private int ProcessTouches(InputManager inputManager, 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)
ETouch touch = touches[i];
switch (touch.phase)
{
case UnityEngine.InputSystem.TouchPhase.Began:
HandleTouchBegan(im, cam, t.touchId, t.screenPosition);
HandleTouchBegan(inputManager, cam, touch.touchId, touch.screenPosition);
break;
case UnityEngine.InputSystem.TouchPhase.Ended:
case UnityEngine.InputSystem.TouchPhase.Canceled:
HandleTouchEnded(im, t.touchId);
HandleTouchEnded(inputManager, touch.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)
Touch touch = Input.GetTouch(i);
switch (touch.phase)
{
case TouchPhase.Began:
HandleTouchBegan(im, cam, t.fingerId, t.position);
HandleTouchBegan(inputManager, cam, touch.fingerId, touch.position);
break;
case TouchPhase.Ended:
case TouchPhase.Canceled:
HandleTouchEnded(im, t.fingerId);
HandleTouchEnded(inputManager, touch.fingerId);
break;
}
}
@@ -132,47 +119,45 @@ public class TrackTouchInput : MonoBehaviour
#endif
}
private void HandleTouchBegan(InputManager im, Camera cam, int touchId, Vector2 screenPos)
private void HandleTouchBegan(InputManager inputManager, Camera cam, int touchId, Vector2 screenPosition)
{
int track = ResolveTrack(cam, screenPos);
if (track >= 0)
{
_activeTouches[touchId] = track;
im.PressTrack(track);
}
int track = ResolveTrack(cam, screenPosition);
if (track < 0)
return;
activeTouches[touchId] = track;
inputManager.PressTrack(track);
}
private void HandleTouchEnded(InputManager im, int touchId)
private void HandleTouchEnded(InputManager inputManager, int touchId)
{
if (_activeTouches.TryGetValue(touchId, out int track))
{
_activeTouches.Remove(touchId);
im.ReleaseTrack(track);
}
if (!activeTouches.TryGetValue(touchId, out int track))
return;
activeTouches.Remove(touchId);
inputManager.ReleaseTrack(track);
}
private void ProcessMouse(InputManager im, Camera cam)
private void ProcessMouse(InputManager inputManager, Camera cam)
{
#if ENABLE_INPUT_SYSTEM
var mouse = Mouse.current;
if (mouse == null) return;
Mouse 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);
mouseHeldTrack = track;
inputManager.PressTrack(track);
}
}
else if (mouse.leftButton.wasReleasedThisFrame)
else if (mouse.leftButton.wasReleasedThisFrame && mouseHeldTrack >= 0)
{
if (_mouseHeldTrack >= 0)
{
im.ReleaseTrack(_mouseHeldTrack);
_mouseHeldTrack = -1;
}
inputManager.ReleaseTrack(mouseHeldTrack);
mouseHeldTrack = -1;
}
#else
if (Input.GetMouseButtonDown(0))
@@ -180,123 +165,118 @@ public class TrackTouchInput : MonoBehaviour
int track = ResolveTrack(cam, Input.mousePosition);
if (track >= 0)
{
_mouseHeldTrack = track;
im.PressTrack(track);
mouseHeldTrack = track;
inputManager.PressTrack(track);
}
}
else if (Input.GetMouseButtonUp(0))
else if (Input.GetMouseButtonUp(0) && mouseHeldTrack >= 0)
{
if (_mouseHeldTrack >= 0)
{
im.ReleaseTrack(_mouseHeldTrack);
_mouseHeldTrack = -1;
}
inputManager.ReleaseTrack(mouseHeldTrack);
mouseHeldTrack = -1;
}
#endif
}
/// <summary>
/// 把屏幕坐标转换为轨道平面上的世界点,用 OverlapPoint 命中轨道点击区,返回轨道索引;无命中返回 -1。
///
/// 相机移动/晃动/推拉时,每帧都用当前相机状态转换,所以命中自动跟随相机。
/// - 正交相机:ScreenToWorldPoint 忽略深度,直接取 x/y。
/// - 透视相机:从相机发射线,求与轨道平面(Z = trackPlaneWorldZ)的交点。
/// </summary>
private int ResolveTrack(Camera cam, Vector2 screenPos)
private int ResolveTrack(Camera cam, Vector2 screenPosition)
{
EnsureOverlapFilter();
Vector2 point;
Vector2 worldPoint;
if (forceOrthographicMode || cam.orthographic)
{
Vector3 world = cam.ScreenToWorldPoint(screenPos);
point = new Vector2(world.x, world.y);
Vector3 world = cam.ScreenToWorldPoint(screenPosition);
worldPoint = new Vector2(world.x, world.y);
}
else
{
Ray ray = cam.ScreenPointToRay(screenPos);
Ray ray = cam.ScreenPointToRay(screenPosition);
float denom = ray.direction.z;
if (Mathf.Abs(denom) < 1e-6f)
{
return -1; // 射线与平面近乎平行,无有效交点
}
return -1;
float t = (trackPlaneWorldZ - ray.origin.z) / denom;
if (t < 0f)
{
return -1; // 交点在相机背后
}
return -1;
Vector3 world = ray.origin + ray.direction * t;
point = new Vector2(world.x, world.y);
worldPoint = new Vector2(world.x, world.y);
}
// 用 OverlapPointAll 遍历所有命中的碰撞体,挑出带 TrackTouchZone 的那个。
// 关键修复:判定线附近同一屏幕点常密集着 note 的 Collider2D 与 judgeline 触发器,
// 单值版 OverlapPoint 只返回其中一个,若不是 tap zone 就丢判定(=点了轨道却没反应)。
// 遍历全部命中即可穿透这些遮挡,可靠找到轨道点击区。
int count = Physics2D.OverlapPoint(point, _overlapFilter, _overlapResults);
int count = Physics2D.OverlapPoint(worldPoint, overlapFilter, overlapResults);
for (int i = 0; i < count; i++)
{
Collider2D hit = _overlapResults[i];
if (hit == null) continue;
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;
if (zone == null)
zone = hit.GetComponentInParent<TrackTouchZone>();
if (zone != null)
return zone.trackIndex;
}
// 兜底:没有精确命中任何 zone(点落在轨道间缝隙或紧邻边缘)。
// 取横向最近的 zone,只要触点在其 Collider 的纵向范围内即算命中——
// 等效于把相邻轨道点击区在横向补满、消除缝隙,且不误触远离轨道区的点击。
return ResolveNearestZone(point);
return ResolveNearestZone(worldPoint);
}
// 横向最近轨道兜底:在所有 zone 中找触点纵向落在其包围盒内、且横向距离最近的一个。
private int ResolveNearestZone(Vector2 point)
private int ResolveNearestZone(Vector2 worldPoint)
{
var zones = TrackTouchZone.All;
int best = -1;
float bestDist = float.MaxValue;
IReadOnlyList<TrackTouchZone> zones = TrackTouchZone.All;
int bestTrack = -1;
float bestDistance = 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;
}
TrackTouchZone zone = zones[i];
if (zone == null || zone.Collider == null)
continue;
// OverlapPoint 的复用缓冲与过滤器(避免每次触摸分配 GC)。
private static readonly Collider2D[] _overlapResults = new Collider2D[16];
private ContactFilter2D _overlapFilter;
private bool _filterReady;
Bounds bounds = zone.Collider.bounds;
if (worldPoint.y < bounds.min.y - 0.1f || worldPoint.y > bounds.max.y + 0.1f)
continue;
float distance = Mathf.Abs(worldPoint.x - bounds.center.x);
if (distance < bestDistance)
{
bestDistance = distance;
bestTrack = zone.trackIndex;
}
}
return bestTrack;
}
private void EnsureOverlapFilter()
{
if (_filterReady) return;
_overlapFilter = new ContactFilter2D();
_overlapFilter.useLayerMask = true;
_overlapFilter.SetLayerMask(hitLayers);
_overlapFilter.useTriggers = true; // tap zone 可能是触发器,需包含
_filterReady = true;
if (filterReady)
return;
overlapFilter = new ContactFilter2D();
overlapFilter.useLayerMask = true;
overlapFilter.SetLayerMask(hitLayers);
overlapFilter.useTriggers = true;
filterReady = true;
}
private void OnDisable()
{
// 组件被关闭/切场景时,释放所有还按住的轨道,避免音符卡在 held 状态。
var im = InputManager.Instance;
if (im != null)
if (!ShouldProcessTouchInput())
return;
InputManager inputManager = InputManager.Instance;
if (inputManager != null)
{
foreach (var kv in _activeTouches)
foreach (KeyValuePair<int, int> touch in activeTouches)
{
im.ReleaseTrack(kv.Value);
inputManager.ReleaseTrack(touch.Value);
}
if (_mouseHeldTrack >= 0) im.ReleaseTrack(_mouseHeldTrack);
if (mouseHeldTrack >= 0)
inputManager.ReleaseTrack(mouseHeldTrack);
}
_activeTouches.Clear();
_mouseHeldTrack = -1;
activeTouches.Clear();
mouseHeldTrack = -1;
#if ENABLE_INPUT_SYSTEM
EnhancedTouchSupport.Disable();