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 TMPro;
using UnityEngine.InputSystem.Controls;
using UnityEngine.InputSystem.EnhancedTouch;
using UnityEngine.InputSystem.LowLevel;
using UnityEngine.UI;
using Touch = UnityEngine.InputSystem.EnhancedTouch.Touch;
using TouchPhase = UnityEngine.InputSystem.TouchPhase;
namespace Ichni.RhythmGame
{
///
/// 为节奏游戏设计的输入管理器,处理多点触控并分发三种主要事件。
/// 【重要】此版本内置了编辑器内的鼠标模拟功能,无需手机即可测试。
///
public class GameInputManager : MonoBehaviour
{
// =====================================================================
// 可配置参数 (Configurable Parameters)
// =====================================================================
[Header("划动设置 (Swipe Settings)")] [Tooltip("识别为划动的最小移动距离(像素)")] [SerializeField]
private float minSwipeDistance = 50f;
[SerializeField] private float swipeAngleThreshold = 1f;
public GameInput gameInput;
public PlayerInput playerInput;
private string rebindsFilePath => Application.persistentDataPath + "/GameData/Rebindings.json";
// =====================================================================
// 内部状态 (Internal State)
// =====================================================================
private class TouchState
{
public int TouchId;
public Vector2 StartPosition;
public Vector2 LastPosition;
public Vector2 LastSwipeDirection = Vector2.zero;
public bool isFirstSwipe = true;
}
private readonly Dictionary _activeTouches = new Dictionary(10);
private readonly Stack _touchStatePool = new Stack(10);
// 为鼠标模拟专门设置一个固定的Touch ID
private const int MOUSE_TOUCH_ID = 999;
// =====================================================================
// MonoBehaviour 生命周期方法 (Lifecycle Methods)
// =====================================================================
private void Awake()
{
#if UNITY_EDITOR || UNITY_STANDALONE
DOTween.SetTweensCapacity(200, 200);
gameInput = new GameInput();
gameInput.Game.Enable();
if (ES3.FileExists(rebindsFilePath) && ES3.KeyExists("Rebinds", rebindsFilePath))
{
gameInput.LoadBindingOverridesFromJson(ES3.Load("Rebinds", rebindsFilePath));
Debug.Log("已加载自定义按键绑定");
}
RegisterActionsInputs();
#else
Debug.Log("已启用真实触摸输入");
#endif
}
private void OnEnable()
{
// GameInputManager owns its EnhancedTouch dependency instead of relying on CwInput initialization.
if (!EnhancedTouchSupport.enabled)
{
EnhancedTouchSupport.Enable();
}
#if UNITY_EDITOR || UNITY_STANDALONE
SetDesktopInputActionsEnabled(Touchscreen.current == null);
#endif
}
private void Update()
{
if (!GameManager.Instance.songPlayer.isUpdating)
{
return;
}
// 使用预处理指令区分平台
#if UNITY_EDITOR || UNITY_STANDALONE
// Device Simulator and physical desktop touchscreens expose a Touchscreen device.
// In that case use the same EnhancedTouch path as mobile builds.
bool useTouchInput = Touchscreen.current != null;
SetDesktopInputActionsEnabled(!useTouchInput);
if (useTouchInput)
{
ProcessRealTouchInput();
}
else
{
HandleHolding();
}
#else
ProcessRealTouchInput();
#endif
}
private void OnDisable()
{
foreach (TouchState state in _activeTouches.Values)
{
RecycleTouchState(state);
}
_activeTouches.Clear();
#if UNITY_EDITOR || UNITY_STANDALONE
SetDesktopInputActionsEnabled(false);
#else
#endif
}
private void OnTap(int id, Vector2 position, double inputEventTime)
{
if (SettingsManager.instance.settingsSaveData.debugMode)
{
GenerateTapMark(id, position);
}
GameManager.Instance.noteJudgeManager.SetNewInputUnitTap(id, position, inputEventTime);
}
private void OnTouch(int id, Vector2 position, double inputEventTime)
{
if (SettingsManager.instance.settingsSaveData.debugMode)
{
GenerateTouchMark(id, position);
}
GameManager.Instance.noteJudgeManager.SetNewInputUnitTouch(id, position, inputEventTime);
}
private void OnSwipe(int id, Vector2 position, bool isGeneric, bool isFirst, Vector2 direction,
double inputEventTime, bool showDebugFeedback = true)
{
// 此处只负责把已经识别出的 Swipe 记录为输入样本。白色/红色箭头是采集层反馈,
// 是否命中 Flick 要等 LateUpdate 中 Note 完成空间采样后再由 NoteJudgeManager 决定。
if (showDebugFeedback && SettingsManager.instance.settingsSaveData.debugMode)
{
GenerateSwipeMark(id, position, isGeneric, isFirst, direction);
if (isFirst) Debug.Log($"划动开始 - ID: {id}, 位置: {position}, 方向: {direction}");
else Debug.Log($"划动更新 - ID: {id}, 位置: {position}, 方向: {direction}");
}
GameManager.Instance.noteJudgeManager.SetNewInputUnitSwipe(id, position, isGeneric, isFirst, direction, inputEventTime);
}
#if UNITY_EDITOR || UNITY_STANDALONE
///
/// 【仅在编辑器中运行】处理鼠标输入并模拟触摸事件。
///
private void ProcessMouseInput()
{
if (Mouse.current == null) return;
Vector2 position = Mouse.current.position.ReadValue();
double inputEventTime = InputState.currentTime;
if (Mouse.current.leftButton.wasPressedThisFrame)
{
ProcessInputEvent(MOUSE_TOUCH_ID, TouchPhase.Began, position, inputEventTime);
}
else if (Mouse.current.leftButton.isPressed)
{
// 如果鼠标位置有变化,则为Moved,否则为Stationary
if (Mouse.current.delta.ReadValue().sqrMagnitude > 0.1f)
{
ProcessInputEvent(MOUSE_TOUCH_ID, TouchPhase.Moved, position, inputEventTime);
}
else
{
ProcessInputEvent(MOUSE_TOUCH_ID, TouchPhase.Stationary, position, inputEventTime);
}
}
else if (Mouse.current.leftButton.wasReleasedThisFrame)
{
ProcessInputEvent(MOUSE_TOUCH_ID, TouchPhase.Ended, position, inputEventTime);
}
if (Mouse.current.rightButton.wasPressedThisFrame)
{
ProcessInputEvent(MOUSE_TOUCH_ID + 1, TouchPhase.Began, position, inputEventTime);
}
else if (Mouse.current.rightButton.isPressed)
{
// 如果鼠标位置有变化,则为Moved,否则为Stationary
if (Mouse.current.delta.ReadValue().sqrMagnitude > 0.1f)
{
ProcessInputEvent(MOUSE_TOUCH_ID + 1, TouchPhase.Moved, position, inputEventTime);
}
else
{
ProcessInputEvent(MOUSE_TOUCH_ID + 1, TouchPhase.Stationary, position, inputEventTime);
}
}
else if (Mouse.current.rightButton.wasReleasedThisFrame)
{
ProcessInputEvent(MOUSE_TOUCH_ID + 1, TouchPhase.Ended, position, inputEventTime);
}
}
#endif
#if UNITY_EDITOR || UNITY_STANDALONE
public bool holdingTouch0;
public bool holdingTouch1;
public bool holdingTouch2;
public bool holdingTouch3;
public bool holdingSwipe0;
public bool holdingSwipe1;
private void RegisterActionsInputs()
{
gameInput.Game.Tap0.performed += ctx =>
{
if (ctx.performed)
{
Vector2 inputPosition = new Vector2(-600 + Screen.width * 0.5f, 200f);
OnTap(0, inputPosition, ctx.time);
holdingTouch0 = true;
}
};
gameInput.Game.Tap1.performed += ctx =>
{
if (ctx.performed)
{
Vector2 inputPosition = new Vector2(-200 + Screen.width * 0.5f, 200f);
OnTap(1, inputPosition, ctx.time);
holdingTouch1 = true;
}
};
gameInput.Game.Tap2.performed += ctx =>
{
if (ctx.performed)
{
Vector2 inputPosition = new Vector2(200 + Screen.width * 0.5f, 200f);
OnTap(2, inputPosition, ctx.time);
holdingTouch2 = true;
}
};
gameInput.Game.Tap3.performed += ctx =>
{
if (ctx.performed)
{
Vector2 inputPosition = new Vector2(600 + Screen.width * 0.5f, 200f);
OnTap(3, inputPosition, ctx.time);
holdingTouch3 = true;
}
};
gameInput.Game.Tap0.canceled += ctx =>
{
if (ctx.canceled)
{
holdingTouch0 = false;
}
};
gameInput.Game.Tap1.canceled += ctx =>
{
if (ctx.canceled)
{
holdingTouch1 = false;
}
};
gameInput.Game.Tap2.canceled += ctx =>
{
if (ctx.canceled)
{
holdingTouch2 = false;
}
};
gameInput.Game.Tap3.canceled += ctx =>
{
if (ctx.canceled)
{
holdingTouch3 = false;
}
};
gameInput.Game.Swipe0.performed += ctx =>
{
if (ctx.performed)
{
holdingSwipe0 = true;
}
};
gameInput.Game.Swipe0.canceled += ctx =>
{
if (ctx.canceled)
{
holdingSwipe0 = false;
}
};
}
private void SetDesktopInputActionsEnabled(bool shouldEnable)
{
if (gameInput == null || gameInput.Game.enabled == shouldEnable) return;
if (shouldEnable)
{
gameInput.Game.Enable();
}
else
{
gameInput.Game.Disable();
}
}
private void HandleHolding()
{
double inputEventTime = InputState.currentTime;
if (holdingTouch0)
{
Vector2 inputPosition = new Vector2(-600 + Screen.width * 0.5f, 200f);
OnTouch(0, inputPosition, inputEventTime);
}
if (holdingTouch1)
{
Vector2 inputPosition = new Vector2(-200 + Screen.width * 0.5f, 200f);
OnTouch(1, inputPosition, inputEventTime);
}
if (holdingTouch2)
{
Vector2 inputPosition = new Vector2(200 + Screen.width * 0.5f, 200f);
OnTouch(2, inputPosition, inputEventTime);
}
if (holdingTouch3)
{
Vector2 inputPosition = new Vector2(600 + Screen.width * 0.5f, 200f);
OnTouch(3, inputPosition, inputEventTime);
}
if (holdingSwipe0)
{
Vector2 inputPosition = new Vector2(Screen.width * 0.5f, 200f);
OnSwipe(0, inputPosition, true, false, Vector2.zero, inputEventTime);
}
}
#endif
///
/// 【仅在真机上运行】处理真实的触摸屏输入。
///
private void ProcessRealTouchInput()
{
if (Touchscreen.current == null) return;
foreach (Touch touch in Touch.activeTouches)
{
double inputEventTime = touch.phase == TouchPhase.Stationary
? InputState.currentTime
: touch.time;
ProcessInputEvent(
touch.touchId,
touch.phase,
touch.screenPosition,
inputEventTime
);
}
}
///
/// 所有输入事件的核心处理函数,无论是真实触摸还是鼠标模拟都会调用它。
///
private void ProcessInputEvent(int touchId, TouchPhase phase, Vector2 position, double inputEventTime)
{
switch (phase)
{
case TouchPhase.Began:
if (_activeTouches.TryGetValue(touchId, out TouchState existingState))
{
RecycleTouchState(existingState);
}
TouchState newState = GetTouchState(touchId, position);
_activeTouches[touchId] = newState;
OnTap(touchId, position, inputEventTime);
OnTouch(touchId, position, inputEventTime);
break;
case TouchPhase.Moved:
if (_activeTouches.TryGetValue(touchId, out TouchState movedState))
{
OnTouch(touchId, position, inputEventTime);
bool emittedNewSwipeSegment = DetectSwipe(movedState, position, inputEventTime);
if (!emittedNewSwipeSegment && !movedState.isFirstSwipe)
{
EmitContinuingSwipe(movedState, position, inputEventTime, true);
}
}
break;
case TouchPhase.Stationary:
if (_activeTouches.TryGetValue(touchId, out TouchState stationaryState))
{
OnTouch(touchId, position, inputEventTime);
if (!stationaryState.isFirstSwipe)
{
// Stationary keeps the Swipe available for judgment, but it is not a new movement sample.
EmitContinuingSwipe(stationaryState, position, inputEventTime, false);
}
}
break;
case TouchPhase.Ended:
if (_activeTouches.TryGetValue(touchId, out TouchState endedState))
{
_activeTouches.Remove(touchId);
RecycleTouchState(endedState);
if (SettingsManager.instance.settingsSaveData.debugMode)
{
GenerateEndMark(position);
}
}
break;
case TouchPhase.Canceled:
if (_activeTouches.TryGetValue(touchId, out TouchState canceledState))
{
_activeTouches.Remove(touchId);
RecycleTouchState(canceledState);
if (SettingsManager.instance.settingsSaveData.debugMode)
{
GenerateCanceledMark(position);
}
}
break;
}
}
///
/// 检测新的划动段。首次超过阈值后,该触点会保持 Swipe 状态直到 Ended/Canceled。
///
private bool DetectSwipe(TouchState state, Vector2 currentPosition, double inputEventTime)
{
Vector2 movementDelta = currentPosition - state.LastPosition;
state.LastPosition = currentPosition;
if (state.isFirstSwipe)
{
Vector2 initialSwipeVector = currentPosition - state.StartPosition;
if (initialSwipeVector.sqrMagnitude < minSwipeDistance * minSwipeDistance) return false;
Vector2 direction = initialSwipeVector.normalized;
OnSwipe(state.TouchId, currentPosition, false, true, direction, inputEventTime);
state.LastSwipeDirection = direction;
state.isFirstSwipe = false;
return true;
}
if (movementDelta.sqrMagnitude > Mathf.Epsilon)
{
Vector2 direction = movementDelta.normalized;
if (Vector2.Dot(direction, state.LastSwipeDirection) <= swipeAngleThreshold)
{
state.LastSwipeDirection = direction;
}
}
// Swipe 激活后,每个移动采样都使用当前触点位置;不再等待新的距离阈值。
EmitContinuingSwipe(state, currentPosition, inputEventTime, true);
return true;
}
private void EmitContinuingSwipe(TouchState state, Vector2 currentPosition, double inputEventTime,
bool showDebugFeedback)
{
OnSwipe(state.TouchId, currentPosition, false, false, state.LastSwipeDirection, inputEventTime,
showDebugFeedback);
}
private TouchState GetTouchState(int touchId, Vector2 startPosition)
{
TouchState state = _touchStatePool.Count > 0 ? _touchStatePool.Pop() : new TouchState();
state.TouchId = touchId;
state.StartPosition = startPosition;
state.LastPosition = startPosition;
state.LastSwipeDirection = Vector2.zero;
state.isFirstSwipe = true;
return state;
}
private void RecycleTouchState(TouchState state)
{
if (state == null) return;
state.TouchId = 0;
state.StartPosition = Vector2.zero;
state.LastPosition = Vector2.zero;
state.LastSwipeDirection = Vector2.zero;
state.isFirstSwipe = true;
_touchStatePool.Push(state);
}
private void GenerateTapMark(int id, Vector2 pos)
{
RectTransform mark = LeanPool.Spawn(GameManager.Instance.basePrefabs.tapInputMark,
GameManager.Instance.judgeHintCanvas.transform).GetComponent();
RectTransform canvasRect = GameManager.Instance.judgeHintCanvas.GetComponent();
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect, pos, null, out Vector2 uiPosition))
{
mark.anchoredPosition = uiPosition;
mark.GetComponentInChildren().text = GameManager.Instance.noteJudgeManager.checkingTapList.Count.ToString();
}
Sequence ss = DOTween.Sequence();
ss.OnStart(() =>
{
mark.GetComponent().color = Color.white;
mark.localScale = Vector3.zero;
});
ss.Join(mark.GetComponent().DOFade(0, 0.25f));
ss.Join(mark.DOScale(5, 0.25f));
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 canvasRect = GameManager.Instance.judgeHintCanvas.GetComponent();
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect, pos, null, out Vector2 uiPosition))
{
mark.anchoredPosition = uiPosition;
}
Sequence ss = DOTween.Sequence();
ss.OnStart(() => { mark.GetComponent().color = Color.white; });
ss.Join(mark.GetComponent().DOFade(0, 0.1f));
ss.OnComplete(() => LeanPool.Despawn(mark.gameObject));
ss.SetUpdate(true);
ss.Play();
}
private void GenerateSwipeMark(int id, Vector2 pos, bool isGeneric, bool isFirst, Vector2 direction)
{
GameObject markPrefab = isGeneric
? GameManager.Instance.basePrefabs.genericSwipeInputMark
: GameManager.Instance.basePrefabs.directionalSwipeInputMark;
RectTransform mark = LeanPool.Spawn(markPrefab, GameManager.Instance.judgeHintCanvas.transform).GetComponent();
RectTransform canvasRect = GameManager.Instance.judgeHintCanvas.GetComponent();
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().color = isFirst ? Color.red : Color.white;
mark.localScale = Vector3.zero;
});
ss.Join(mark.GetComponent().DOFade(0, 0.25f));
ss.Join(mark.DOScale(5, 0.25f));
ss.OnComplete(() => LeanPool.Despawn(mark.gameObject));
ss.SetUpdate(true);
ss.Play();
}
private void GenerateEndMark(Vector2 pos)
{
RectTransform canvasRect = GameManager.Instance.judgeHintCanvas.GetComponent();
RectTransform mark = LeanPool.Spawn(GameManager.Instance.basePrefabs.inputEndMark, canvasRect).GetComponent();
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect, pos, null, out Vector2 uiPosition))
{
mark.anchoredPosition = uiPosition;
}
Sequence ss = DOTween.Sequence();
ss.OnStart(() =>
{
mark.GetComponent().color = Color.white;
mark.localScale = Vector3.one * 5f;
});
ss.Join(mark.GetComponent().DOFade(0, 0.25f));
ss.Join(mark.DOScale(0, 0.25f));
ss.OnComplete(() => LeanPool.Despawn(mark.gameObject));
ss.SetUpdate(true);
ss.Play();
}
private void GenerateCanceledMark(Vector2 pos)
{
RectTransform canvasRect = GameManager.Instance.judgeHintCanvas.GetComponent();
RectTransform mark = LeanPool.Spawn(GameManager.Instance.basePrefabs.inputCanceledMark, canvasRect).GetComponent();
if (RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRect, pos, null, out Vector2 uiPosition))
{
mark.anchoredPosition = uiPosition;
}
Sequence ss = DOTween.Sequence();
ss.OnStart(() =>
{
mark.GetComponent().color = Color.white;
mark.localScale = Vector3.one * 5f;
});
ss.Join(mark.GetComponent().DOFade(0, 0.25f));
ss.Join(mark.DOScale(0, 0.25f));
ss.OnComplete(() => LeanPool.Despawn(mark.gameObject));
ss.SetUpdate(true);
ss.Play();
}
}
}