Files
2026-07-30 23:15:58 +08:00

373 lines
13 KiB
C#

using UnityEngine;
using TMPro;
using System;
using UnityEngine.UI;
// Runs before default-order scripts so the per-track held-state table (trackHeld) is
// updated from keyboard/touch this frame *before* hold notes read it. This preserves the
// frame-accurate timing the old direct Input.GetKey polling had in HoldNote.
[DefaultExecutionOrder(-100)]
public class InputManager : MonoBehaviour
{
public static InputManager Instance { get; private set; }
// Legacy KeyCode-based events (maintained for backward compatibility with existing Note/HoldNote code).
// These are now fired by OnTrackInput handlers after receiving dspTime from GameplayInputActions.
public static event Action<KeyCode> OnKeyPressed;
public static event Action<KeyCode> OnKeyReleased;
// New dspTime-carrying events: track index + dspTime at the moment Input System captured the event.
// Allows sub-frame-accurate judgement by converting dspTime → song time via GameplayClock.SongTimeFromDsp.
public static event Action<int, double> OnTrackPressedWithDspTime;
public static event Action<int, double> OnTrackReleasedWithDspTime;
private static readonly string[] TrackColors = { "red", "green", "yellow", "purple", "blue" };
[Header("Inspector")]
public TextMeshProUGUI[] trackJudgeTexts = new TextMeshProUGUI[5];
[Header("Inspector")]
public Text[] trackKeyTexts = new Text[5];
[Header("Lane Background Settings")]
[Tooltip("The sprites for the 5 lanes (Red, Green, Yellow, Purple, Blue)")]
public SpriteRenderer[] laneSprites = new SpriteRenderer[5];
[Tooltip("Alpha value when pressed (0-255)")]
public float pressedAlpha = 30f;
[Header("Inspector")]
public bool showJudgeText = true;
// Documentation text normalized.
[Header("Inspector")]
[Tooltip("Documentation text normalized.")]
public Color keyActiveColor = Color.yellow;
[Tooltip("Documentation text normalized.")]
public Color keyInactiveColor = Color.black;
// Documentation text normalized.
public Color perfectColor = Color.yellow;
public Color greatColor = Color.green;
public Color goodColor = Color.cyan;
public Color missColor = Color.red;
private KeyCode[] cachedKeys = new KeyCode[5];
private KeyCode cachedSecondaryTrack3Key = KeyCode.V;
private bool pauseBlockedLastFrame = false;
// Per-track held count. Written by both keyboard (Update) and touch (PressTrack/
// ReleaseTrack) so hold-note logic can query one platform-agnostic source instead
// of polling Input.GetKey directly (which cannot be driven by touch on Android).
// Count-based tracking fixes multi-touch on the same lane: one finger lifting no
// longer releases the lane while another finger is still holding it.
private int[] trackHeldCount = new int[5];
/// <summary>
/// True while the given track (0-4) is currently held, regardless of whether the
/// source is keyboard or touch. Replaces direct Input.GetKey polling in hold notes.
/// </summary>
public bool IsTrackHeld(int trackIndex)
{
if (trackIndex < 0 || trackIndex >= trackHeldCount.Length) return false;
return trackHeldCount[trackIndex] > 0;
}
private void Awake()
{
if (Instance == null)
Instance = this;
else
Destroy(gameObject);
// Documentation text normalized.
RefreshKeyCache();
// Documentation text normalized.
if (trackKeyTexts != null)
{
foreach (var t in trackKeyTexts)
{
if (t != null) t.color = keyInactiveColor;
}
}
// Ensure GameplayInputActions exists (auto-create if missing).
if (GameplayInputActions.Instance == null)
{
GameObject inputActionsGo = new GameObject("GameplayInputActions");
inputActionsGo.AddComponent<GameplayInputActions>();
DontDestroyOnLoad(inputActionsGo);
}
// Subscribe to Input System events from GameplayInputActions (dspTime-carrying press/release).
GameplayInputActions.OnTrackPressed += OnTrackInput_Press;
GameplayInputActions.OnTrackReleased += OnTrackInput_Release;
}
private void OnDestroy()
{
if (GameplayInputActions.Instance != null)
{
GameplayInputActions.OnTrackPressed -= OnTrackInput_Press;
GameplayInputActions.OnTrackReleased -= OnTrackInput_Release;
}
}
/// <summary>
/// Handle track press from GameplayInputActions (Input System). Updates held state,
/// fires both legacy KeyCode event and new dspTime event, and updates visuals.
/// </summary>
private void OnTrackInput_Press(int trackIndex, double pressDspTime)
{
if (IsBlockedByPause()) return;
KeyCode sourceKey = trackIndex >= 0 && trackIndex < cachedKeys.Length ? cachedKeys[trackIndex] : KeyCode.None;
PressTrack(trackIndex, sourceKey, pressDspTime);
}
/// <summary>
/// Handle track release from GameplayInputActions (Input System). Updates held state,
/// fires both legacy KeyCode event and new dspTime event, and updates visuals.
/// </summary>
private void OnTrackInput_Release(int trackIndex, double releaseDspTime)
{
if (IsBlockedByPause()) return;
ReleaseTrack(trackIndex, releaseDspTime);
}
private void RefreshKeyCache()
{
for (int i = 0; i < TrackColors.Length; i++)
{
cachedKeys[i] = KeyBindingManager.GetKeyForColor(TrackColors[i]);
}
cachedSecondaryTrack3Key = KeyBindingManager.GetSecondaryKeyForTrack3();
}
private void Start()
{
// Ensure runtime input and UI labels both use current bindings.
RefreshKeyCache();
RefreshKeyLabels();
// Initialize lane sprites to 0 alpha
if (laneSprites != null)
{
foreach (var sprite in laneSprites)
{
if (sprite != null) SetSpriteAlpha(sprite, 0f);
}
}
}
private void SetSpriteAlpha(SpriteRenderer sprite, float alpha255)
{
if (sprite == null) return;
Color c = sprite.color;
c.a = Mathf.Clamp01(alpha255 / 255f);
sprite.color = c;
}
private void Update()
{
bool pauseBlocked = IsBlockedByPause();
if (pauseBlocked)
{
if (!pauseBlockedLastFrame)
ForceReleaseAllKeys();
pauseBlockedLastFrame = true;
return;
}
pauseBlockedLastFrame = false;
// Legacy Input.GetKeyDown polling removed: keyboard input now comes from
// GameplayInputActions (Input System) via OnTrackInput_Press/Release handlers.
// Touch input still drives PressTrack/ReleaseTrack directly via TrackTouchInput.
}
/// <summary>
/// Begin holding a track (0-4). Called by keyboard Update on key-down and by touch
/// regions on pointer-down. Sets the held-state table, raises OnKeyPressed with the
/// track's bound key so event-driven tap notes keep working, and updates lane visuals.
/// Always fires OnKeyPressed to allow rapid same-track taps even when held.
/// </summary>
public void PressTrack(int index)
{
KeyCode sourceKey = index >= 0 && index < cachedKeys.Length ? cachedKeys[index] : KeyCode.None;
// Touch/other callers without an event timestamp use the current dspTime.
PressTrack(index, sourceKey, AudioSettings.dspTime);
}
public void PressTrack(int index, KeyCode sourceKey)
{
PressTrack(index, sourceKey, AudioSettings.dspTime);
}
public void PressTrack(int index, KeyCode sourceKey, double pressDspTime)
{
if (index < 0 || index >= TrackColors.Length) return;
bool wasHeld = trackHeldCount[index] > 0;
trackHeldCount[index]++;
// Always fire tap judgment pulse, even if already held (fixes rapid same-track taps).
// Fire both the legacy KeyCode event and the new dspTime event so both consumers work.
KeyCode key = cachedKeys[index];
if (key != KeyCode.None)
OnKeyPressed?.Invoke(key);
OnTrackPressedWithDspTime?.Invoke(index, pressDspTime);
if (trackKeyTexts != null && index < trackKeyTexts.Length)
{
var txt = trackKeyTexts[index];
if (txt != null)
{
if (sourceKey != KeyCode.None)
txt.text = KeyBindingManager.GetDisplayName(sourceKey);
txt.color = keyActiveColor;
}
}
// Only update visuals on the first press (0→1 transition)
if (wasHeld) return;
if (laneSprites != null && index < laneSprites.Length)
SetSpriteAlpha(laneSprites[index], pressedAlpha);
if (trackKeyTexts != null && index < trackKeyTexts.Length)
{
var txt = trackKeyTexts[index];
if (txt != null) txt.color = keyActiveColor;
}
}
/// <summary>
/// Release a track (0-4). Called by keyboard Update on key-up and by touch regions
/// on pointer-up. Clears the held-state table, raises OnKeyReleased, and resets visuals.
/// </summary>
public void ReleaseTrack(int index)
{
ReleaseTrack(index, AudioSettings.dspTime);
}
public void ReleaseTrack(int index, double releaseDspTime)
{
if (index < 0 || index >= TrackColors.Length) return;
if (trackHeldCount[index] <= 0) return; // not held; nothing to release
trackHeldCount[index]--;
if (trackHeldCount[index] > 0) return; // another finger/key is still holding this lane
KeyCode key = cachedKeys[index];
if (key != KeyCode.None)
OnKeyReleased?.Invoke(key);
OnTrackReleasedWithDspTime?.Invoke(index, releaseDspTime);
if (laneSprites != null && index < laneSprites.Length)
SetSpriteAlpha(laneSprites[index], 0f);
if (trackKeyTexts != null && index < trackKeyTexts.Length)
{
var txt = trackKeyTexts[index];
if (txt != null) txt.color = keyInactiveColor;
}
}
private bool IsBlockedByPause()
{
var pm = PauseManager.Instance;
return pm != null && pm.IsPaused;
}
/// <summary>
/// Force a release event for all bound keys and reset UI indicators.
/// Useful for external "clear input" operations where the system should treat
/// all keys as released regardless of physical state.
/// </summary>
public void ForceReleaseAllKeys()
{
for (int i = 0; i < TrackColors.Length; i++)
{
string color = TrackColors[i];
bool wasHeld = trackHeldCount[i] > 0;
trackHeldCount[i] = 0; // clear held state so hold notes don't see a stale press after pause/clear
KeyCode key = KeyBindingManager.GetKeyForColor(color);
if (key == KeyCode.None) continue;
if (wasHeld)
{
try { OnKeyReleased?.Invoke(key); } catch { }
}
int index = GetIndexForColor(color);
if (laneSprites != null && index >= 0 && index < laneSprites.Length)
{
SetSpriteAlpha(laneSprites[index], 0f);
}
if (trackKeyTexts != null && index >= 0 && index < trackKeyTexts.Length)
{
var txt = trackKeyTexts[index];
if (txt != null) txt.color = keyInactiveColor;
}
}
}
// Refresh displayed labels for key bindings (called after rebind)
public void RefreshKeyLabels()
{
if (trackKeyTexts != null)
{
for (int i = 0; i < TrackColors.Length && i < trackKeyTexts.Length; i++)
{
var txt = trackKeyTexts[i];
if (txt == null) continue;
KeyCode k = KeyBindingManager.GetKeyForColor(TrackColors[i]);
txt.text = KeyBindingManager.GetDisplayName(k);
}
}
// Keep runtime input cache in sync with latest bindings even when label UI is missing.
RefreshKeyCache();
}
private int GetIndexForColor(string color)
{
// Documentation text normalized.
switch (color)
{
case "red": return 0;
case "green": return 1;
case "yellow": return 2;
case "purple": return 3;
case "blue": return 4;
default: return -1;
}
}
/// <summary>
/// Documentation text normalized.
public void ShowJudgeResult(int trackIndex, string result)
{
if (!showJudgeText) return;
if (trackJudgeTexts == null || trackIndex < 0 || trackIndex >= trackJudgeTexts.Length) return;
var textObj = trackJudgeTexts[trackIndex];
if (textObj == null) return;
textObj.text = result;
switch (result)
{
case "Perfect":
textObj.color = perfectColor;
break;
case "Great":
textObj.color = greatColor;
break;
case "Good":
textObj.color = goodColor;
break;
case "Miss":
textObj.color = missColor;
break;
default:
textObj.color = Color.white;
break;
}
}
}