177 lines
5.8 KiB
C#
177 lines
5.8 KiB
C#
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<int, Queue<string>> trackKeyMappings = new Dictionary<int, Queue<string>>();
|
|
|
|
// Track notes currently being judged to prevent double-judging the same note
|
|
// Format: trackIndex -> HashSet of note IDs being judged
|
|
private Dictionary<int, HashSet<string>> trackNotesBeingJudged = new Dictionary<int, HashSet<string>>();
|
|
|
|
// transient per-frame consumption: ensures one physical press only affects one note per track per frame
|
|
private Dictionary<int, int> trackConsumedFrame = new Dictionary<int, int>();
|
|
|
|
// 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 当音符进入判定区域时,添加音符 id 到队列
|
|
/// </summary>
|
|
public void RegisterKey(int trackIndex, string noteInstanceId)
|
|
{
|
|
if (!trackKeyMappings.ContainsKey(trackIndex))
|
|
trackKeyMappings[trackIndex] = new Queue<string>();
|
|
|
|
trackKeyMappings[trackIndex].Enqueue(noteInstanceId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 当音符离开判定区域时,移除音符 id
|
|
/// </summary>
|
|
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<string>();
|
|
while (q.Count > 0)
|
|
{
|
|
var v = q.Dequeue();
|
|
if (v != noteInstanceId) temp.Enqueue(v);
|
|
}
|
|
trackKeyMappings[trackIndex] = temp;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取轨道当前排在队首的音符实例 id(队列头)
|
|
/// </summary>
|
|
public string GetCurrentNoteId(int trackIndex)
|
|
{
|
|
if (trackKeyMappings.ContainsKey(trackIndex) && trackKeyMappings[trackIndex].Count > 0)
|
|
{
|
|
return trackKeyMappings[trackIndex].Peek();
|
|
}
|
|
return null; // 无音符
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public bool TryLockTrackForJudge(int trackIndex, string noteId)
|
|
{
|
|
if (trackIndex < 0) return false;
|
|
|
|
if (!trackNotesBeingJudged.ContainsKey(trackIndex))
|
|
{
|
|
trackNotesBeingJudged[trackIndex] = new HashSet<string>();
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Release the lock for a note on a track. Call this after judgment is complete.
|
|
/// </summary>
|
|
public void UnlockTrackForJudge(int trackIndex, string noteId)
|
|
{
|
|
if (trackIndex < 0) return;
|
|
|
|
if (trackNotesBeingJudged.ContainsKey(trackIndex))
|
|
{
|
|
trackNotesBeingJudged[trackIndex].Remove(noteId);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Force reset the consumption state (useful for testing or specific scenarios)
|
|
/// </summary>
|
|
public void ResetConsumptionState()
|
|
{
|
|
trackConsumedFrame.Clear();
|
|
}
|
|
}
|