339 lines
12 KiB
C#
339 lines
12 KiB
C#
using UnityEngine;
|
||
using UnityEngine.InputSystem;
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using DG.Tweening;
|
||
using Ichni;
|
||
using Lean.Common;
|
||
using Lean.Pool;
|
||
using Sirenix.OdinInspector;
|
||
using UnityEngine.InputSystem.Controls;
|
||
using UnityEngine.UI;
|
||
using Touch = UnityEngine.InputSystem.EnhancedTouch.Touch;
|
||
using TouchPhase = UnityEngine.InputSystem.TouchPhase;
|
||
|
||
/// <summary>
|
||
/// 为节奏游戏设计的输入管理器,处理多点触控并分发三种主要事件。
|
||
/// 【重要】此版本内置了编辑器内的鼠标模拟功能,无需手机即可测试。
|
||
/// </summary>
|
||
public class RhythmInputManager : SerializedMonoBehaviour
|
||
{
|
||
// =====================================================================
|
||
// 公共事件 (Public Events)
|
||
// =====================================================================
|
||
public static event Action<int, Vector2> OnTap;
|
||
public static event Action<int, Vector2> OnTouch;
|
||
public static event Action<int, Vector2, bool, Vector2> OnSwipe;
|
||
|
||
// =====================================================================
|
||
// 可配置参数 (Configurable Parameters)
|
||
// =====================================================================
|
||
|
||
[Header("划动设置 (Swipe Settings)")]
|
||
[Tooltip("识别为划动的最小移动距离(像素)")]
|
||
[SerializeField] private float minSwipeDistance = 200.0f;
|
||
[SerializeField] private float swipeAngleThreshold = 1f;
|
||
|
||
// =====================================================================
|
||
// 内部状态 (Internal State)
|
||
// =====================================================================
|
||
private class TouchState
|
||
{
|
||
public int TouchId;
|
||
public Vector2 StartPosition;
|
||
public float StartTime;
|
||
public Vector2 LastSwipeDirection = Vector2.zero;
|
||
public bool IsTapCandidate = true;
|
||
}
|
||
|
||
private readonly Dictionary<int, TouchState> _activeTouches = new Dictionary<int, TouchState>();
|
||
|
||
// 为鼠标模拟专门设置一个固定的Touch ID
|
||
private const int MOUSE_TOUCH_ID = 999;
|
||
|
||
// =====================================================================
|
||
// MonoBehaviour 生命周期方法 (Lifecycle Methods)
|
||
// =====================================================================
|
||
|
||
private void Awake()
|
||
{
|
||
//OnTap += (id, pos)=> Debug.Log($"Tap Detected: Touch ID {id}, Position {pos}");
|
||
//OnTouch += (id, pos) => Debug.Log($"Touch Detected: Touch ID {id}, Position {pos}");
|
||
//OnSwipe += (id, pos, dir) => Debug.Log($"Swipe Detected: Touch ID {id}, Position {pos}, Direction {dir}");
|
||
|
||
OnTap += GenerateTapMark;
|
||
OnTouch += GenerateTouchMark;
|
||
OnSwipe += GenerateSwipeMark;
|
||
}
|
||
|
||
private void Update()
|
||
{
|
||
// 使用预处理指令区分平台
|
||
#if UNITY_EDITOR || UNITY_STANDALONE
|
||
// --- 在Unity编辑器中,使用鼠标模拟触摸 ---
|
||
//ProcessMouseInput();
|
||
ProcessKeyboardInput();
|
||
#else
|
||
// --- 在真机设备上,使用真实的触摸输入 ---
|
||
ProcessRealTouchInput();
|
||
#endif
|
||
}
|
||
|
||
// =====================================================================
|
||
// 核心处理逻辑 (Core Processing Logic)
|
||
// =====================================================================
|
||
|
||
#if UNITY_EDITOR
|
||
/// <summary>
|
||
/// 【仅在编辑器中运行】处理鼠标输入并模拟触摸事件。
|
||
/// </summary>
|
||
private void ProcessMouseInput()
|
||
{
|
||
if (Mouse.current == null) return;
|
||
|
||
Vector2 position = Mouse.current.position.ReadValue();
|
||
|
||
if (Mouse.current.leftButton.wasPressedThisFrame)
|
||
{
|
||
ProcessInputEvent(MOUSE_TOUCH_ID, TouchPhase.Began, position);
|
||
}
|
||
else if (Mouse.current.leftButton.isPressed)
|
||
{
|
||
// 如果鼠标位置有变化,则为Moved,否则为Stationary
|
||
if (Mouse.current.delta.ReadValue().sqrMagnitude > 0.1f)
|
||
{
|
||
ProcessInputEvent(MOUSE_TOUCH_ID, TouchPhase.Moved, position);
|
||
}
|
||
else
|
||
{
|
||
ProcessInputEvent(MOUSE_TOUCH_ID, TouchPhase.Stationary, position);
|
||
}
|
||
}
|
||
else if (Mouse.current.leftButton.wasReleasedThisFrame)
|
||
{
|
||
ProcessInputEvent(MOUSE_TOUCH_ID, TouchPhase.Ended, position);
|
||
}
|
||
|
||
if (Mouse.current.rightButton.wasPressedThisFrame)
|
||
{
|
||
ProcessInputEvent(MOUSE_TOUCH_ID + 1, TouchPhase.Began, position);
|
||
}
|
||
else if (Mouse.current.rightButton.isPressed)
|
||
{
|
||
// 如果鼠标位置有变化,则为Moved,否则为Stationary
|
||
if (Mouse.current.delta.ReadValue().sqrMagnitude > 0.1f)
|
||
{
|
||
ProcessInputEvent(MOUSE_TOUCH_ID + 1, TouchPhase.Moved, position);
|
||
}
|
||
else
|
||
{
|
||
ProcessInputEvent(MOUSE_TOUCH_ID + 1, TouchPhase.Stationary, position);
|
||
}
|
||
}
|
||
else if (Mouse.current.rightButton.wasReleasedThisFrame)
|
||
{
|
||
ProcessInputEvent(MOUSE_TOUCH_ID + 1, TouchPhase.Ended, position);
|
||
}
|
||
}
|
||
#endif
|
||
|
||
#if UNITY_EDITOR || UNITY_STANDALONE
|
||
public static List<KeyControl> availableKeys = new List<KeyControl>()
|
||
{
|
||
Keyboard.current.dKey,
|
||
Keyboard.current.fKey,
|
||
Keyboard.current.jKey,
|
||
Keyboard.current.kKey,
|
||
};
|
||
public static List<KeyControl> flickKeys = new List<KeyControl>()
|
||
{
|
||
Keyboard.current.sKey,
|
||
Keyboard.current.lKey,
|
||
};
|
||
private void ProcessKeyboardInput()
|
||
{
|
||
for (int index = 0; index < availableKeys.Count; index++)
|
||
{
|
||
Vector2 inputPosition = new Vector2(index * 400 - 600 + Screen.width * 0.5f, 200f); // 假设每个按键的虚拟位置
|
||
|
||
if (availableKeys[index].wasPressedThisFrame)
|
||
{
|
||
OnTap?.Invoke(index, inputPosition);
|
||
}
|
||
|
||
if (availableKeys[index].isPressed)
|
||
{
|
||
OnTouch?.Invoke(index, inputPosition);
|
||
}
|
||
}
|
||
|
||
for (int index = 0; index < flickKeys.Count; index++)
|
||
{
|
||
Vector2 inputPosition = new Vector2(index * 1600 - 800 + Screen.width * 0.5f, 200f); // 假设每个划动按键的虚拟位置
|
||
|
||
if (flickKeys[index].wasPressedThisFrame)
|
||
{
|
||
OnSwipe?.Invoke(index, inputPosition, true, Vector2.zero);
|
||
}
|
||
}
|
||
}
|
||
#endif
|
||
|
||
/// <summary>
|
||
/// 【仅在真机上运行】处理真实的触摸屏输入。
|
||
/// </summary>
|
||
private void ProcessRealTouchInput()
|
||
{
|
||
if (Touchscreen.current == null) return;
|
||
|
||
foreach (Touch touch in Touch.activeTouches)
|
||
{
|
||
ProcessInputEvent(
|
||
touch.touchId,
|
||
touch.phase,
|
||
touch.screenPosition
|
||
);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 所有输入事件的核心处理函数,无论是真实触摸还是鼠标模拟都会调用它。
|
||
/// </summary>
|
||
private void ProcessInputEvent(int touchId, TouchPhase phase, Vector2 position)
|
||
{
|
||
switch (phase)
|
||
{
|
||
case TouchPhase.Began:
|
||
var newState = new TouchState
|
||
{
|
||
TouchId = touchId,
|
||
StartPosition = position,
|
||
StartTime = Time.time,
|
||
};
|
||
_activeTouches[touchId] = newState;
|
||
OnTap?.Invoke(touchId, position);
|
||
OnTouch?.Invoke(touchId, position);
|
||
break;
|
||
|
||
case TouchPhase.Moved:
|
||
if (_activeTouches.TryGetValue(touchId, out TouchState movedState))
|
||
{
|
||
OnTouch?.Invoke(touchId, position);
|
||
DetectSwipe(movedState, position);
|
||
}
|
||
|
||
break;
|
||
|
||
case TouchPhase.Stationary:
|
||
if (_activeTouches.TryGetValue(touchId, out TouchState stationaryState))
|
||
{
|
||
OnTouch?.Invoke(touchId, position);
|
||
}
|
||
|
||
break;
|
||
|
||
case TouchPhase.Canceled:
|
||
if (_activeTouches.ContainsKey(touchId))
|
||
{
|
||
_activeTouches.Remove(touchId);
|
||
}
|
||
|
||
break;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检测划动逻辑 (无需修改)
|
||
/// </summary>
|
||
private void DetectSwipe(TouchState state, Vector2 currentPosition)
|
||
{
|
||
Vector2 swipeVector = currentPosition - state.StartPosition;
|
||
if (swipeVector.magnitude < minSwipeDistance) return;
|
||
|
||
Vector2 direction = swipeVector.normalized;
|
||
|
||
// 检查是否是新的划动方向
|
||
if (Vector2.Dot(direction, state.LastSwipeDirection) <= swipeAngleThreshold)
|
||
{
|
||
OnSwipe?.Invoke(state.TouchId, state.StartPosition, false, direction);
|
||
state.LastSwipeDirection = direction;
|
||
state.StartPosition = currentPosition;
|
||
state.StartTime = Time.time;
|
||
}
|
||
}
|
||
|
||
private void GenerateTapMark(int id, Vector2 pos)
|
||
{
|
||
RectTransform mark = LeanPool.Spawn(GameManager.instance.basePrefabs.tapInputMark,
|
||
GameManager.instance.judgeHintCanvas.transform). GetComponent<RectTransform>();
|
||
|
||
RectTransform canvasRect = GameManager.instance.judgeHintCanvas.GetComponent<RectTransform>();
|
||
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect, pos, null, out Vector2 uiPosition))
|
||
{
|
||
mark.anchoredPosition = uiPosition;
|
||
}
|
||
|
||
Sequence ss = DOTween.Sequence();
|
||
ss.OnStart(() =>
|
||
{
|
||
mark.GetComponent<Image>().color = Color.white;
|
||
mark.localScale = Vector3.zero;
|
||
});
|
||
ss.Join(mark.GetComponent<Image>().DOFade(0, 0.5f));
|
||
ss.Join(mark.DOScale(5, 0.5f));
|
||
ss.OnComplete(() => LeanPool.Despawn(mark.gameObject));
|
||
ss.SetUpdate(true);
|
||
ss.Play();
|
||
}
|
||
|
||
private void GenerateTouchMark(int id, Vector2 pos)
|
||
{
|
||
RectTransform mark = LeanPool.Spawn(GameManager.instance.basePrefabs.touchInputMark,
|
||
GameManager.instance.judgeHintCanvas.transform). GetComponent<RectTransform>();
|
||
|
||
RectTransform canvasRect = GameManager.instance.judgeHintCanvas.GetComponent<RectTransform>();
|
||
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect, pos, null, out Vector2 uiPosition))
|
||
{
|
||
mark.anchoredPosition = uiPosition;
|
||
}
|
||
|
||
Sequence ss = DOTween.Sequence();
|
||
ss.OnStart(() =>
|
||
{
|
||
mark.GetComponent<Image>().color = Color.white;
|
||
});
|
||
ss.Join(mark.GetComponent<Image>().DOFade(0, 0.1f));
|
||
ss.OnComplete(() => LeanPool.Despawn(mark.gameObject));
|
||
ss.SetUpdate(true);
|
||
ss.Play();
|
||
}
|
||
|
||
private void GenerateSwipeMark(int id, Vector2 pos, bool isGeneric, Vector2 direction)
|
||
{
|
||
GameObject markPrefab = isGeneric
|
||
? GameManager.instance.basePrefabs.genericSwipeInputMark
|
||
: GameManager.instance.basePrefabs.directionalSwipeInputMark;
|
||
|
||
RectTransform mark = LeanPool.Spawn(markPrefab, GameManager.instance.judgeHintCanvas.transform). GetComponent<RectTransform>();
|
||
|
||
RectTransform canvasRect = GameManager.instance.judgeHintCanvas.GetComponent<RectTransform>();
|
||
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect, pos, null, out Vector2 uiPosition))
|
||
{
|
||
mark.anchoredPosition = uiPosition;
|
||
}
|
||
mark.localEulerAngles = new Vector3(0, 0, Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg - 90f);
|
||
|
||
Sequence ss = DOTween.Sequence();
|
||
ss.OnStart(() =>
|
||
{
|
||
mark.GetComponent<Image>().color = Color.white;
|
||
mark.localScale = Vector3.zero;
|
||
});
|
||
ss.Join(mark.GetComponent<Image>().DOFade(0, 0.5f));
|
||
ss.Join(mark.DOScale(5, 0.5f));
|
||
ss.OnComplete(() => LeanPool.Despawn(mark.gameObject));
|
||
ss.SetUpdate(true);
|
||
ss.Play();
|
||
}
|
||
}
|