using System.Collections.Generic; using UnityEngine; public class TrackKeyManager : MonoBehaviour { public static TrackKeyManager Instance { get; private set; } // store note instance IDs (as strings) in queue order per track private Dictionary> trackKeyMappings = new Dictionary>(); // Track notes currently being judged to prevent double-judging the same note // Format: trackIndex -> HashSet of note IDs being judged private Dictionary> trackNotesBeingJudged = new Dictionary>(); // transient per-frame consumption: ensures one physical press only affects one note per track per frame private Dictionary trackConsumedFrame = new Dictionary(); // track last frame we checked for focus loss private int lastCheckedFrame = -1; private bool lastFocusedState = true; private void Awake() { if (Instance == null) { Instance = this; } else { Destroy(gameObject); } } private void Update() { // Clear per-frame consumption dictionary when focus is regained or at frame boundaries int currentFrame = Time.frameCount; bool isFocused = Application.isFocused; // If we've moved to a new frame or focus state changed, clear consumption tracking if (lastCheckedFrame != currentFrame || (isFocused && !lastFocusedState)) { // On focus regain, clear the consumed frame dictionary to allow input again if (isFocused && !lastFocusedState) { trackConsumedFrame.Clear(); if (GameConfig.verboseLogs) Debug.Log("[TrackKeyManager] Focus regained, cleared track consumption"); } } lastCheckedFrame = currentFrame; lastFocusedState = isFocused; } /// /// 当音符进入判定区域时,添加音符 id 到队列 /// public void RegisterKey(int trackIndex, string noteInstanceId) { if (!trackKeyMappings.ContainsKey(trackIndex)) trackKeyMappings[trackIndex] = new Queue(); trackKeyMappings[trackIndex].Enqueue(noteInstanceId); } /// /// 当音符离开判定区域时,移除音符 id /// public void UnregisterKey(int trackIndex, string noteInstanceId) { if (!trackKeyMappings.ContainsKey(trackIndex) || trackKeyMappings[trackIndex].Count == 0) return; // If the head matches, dequeue. Otherwise try to remove from queue by rebuilding it. if (trackKeyMappings[trackIndex].Peek() == noteInstanceId) { trackKeyMappings[trackIndex].Dequeue(); return; } // Otherwise remove matching id if present (preserve order of others) var q = trackKeyMappings[trackIndex]; var temp = new Queue(); while (q.Count > 0) { var v = q.Dequeue(); if (v != noteInstanceId) temp.Enqueue(v); } trackKeyMappings[trackIndex] = temp; } /// /// 获取轨道当前排在队首的音符实例 id(队列头) /// public string GetCurrentNoteId(int trackIndex) { if (trackKeyMappings.ContainsKey(trackIndex) && trackKeyMappings[trackIndex].Count > 0) { return trackKeyMappings[trackIndex].Peek(); } return null; // 无音符 } /// /// Try to register a note as being judged on a track. Returns true if successful (no other note is currently being judged). /// This prevents multiple notes on the same track from being judged by a single key press. /// public bool TryLockTrackForJudge(int trackIndex, string noteId) { if (trackIndex < 0) return false; if (!trackNotesBeingJudged.ContainsKey(trackIndex)) { trackNotesBeingJudged[trackIndex] = new HashSet(); } // If there are already notes being judged on this track, reject if (trackNotesBeingJudged[trackIndex].Count > 0) { return false; } // Lock this note trackNotesBeingJudged[trackIndex].Add(noteId); return true; } /// /// Release the lock for a note on a track. Call this after judgment is complete. /// public void UnlockTrackForJudge(int trackIndex, string noteId) { if (trackIndex < 0) return; if (trackNotesBeingJudged.ContainsKey(trackIndex)) { trackNotesBeingJudged[trackIndex].Remove(noteId); } } /// /// Try to mark this track as consumed for the current frame. Returns true if this call /// successfully consumes the track (no other note on the same track has consumed this frame). /// public bool TryConsumeTrackForFrame(int trackIndex) { if (trackIndex < 0) return false; int currentFrame = Time.frameCount; // If the recorded frame is old or invalid, allow consumption if (!trackConsumedFrame.ContainsKey(trackIndex)) { trackConsumedFrame[trackIndex] = currentFrame; return true; } // If we're in a different frame, reset and allow int recordedFrame = trackConsumedFrame[trackIndex]; if (recordedFrame != currentFrame) { trackConsumedFrame[trackIndex] = currentFrame; return true; } // Same frame, already consumed return false; } /// /// Force reset the consumption state (useful for testing or specific scenarios) /// public void ResetConsumptionState() { trackConsumedFrame.Clear(); } }