成就系统 巨大修改 新的ui 黑白效果 等一堆

This commit is contained in:
FloatGaming
2026-01-23 18:43:23 +08:00
parent f3ece026ca
commit 0e47864569
256 changed files with 134131 additions and 31192 deletions
@@ -1,12 +1,15 @@
using System.Collections.Generic;
using UnityEngine;
using System.Collections;
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>>();
// Format: trackIndex -> Queue of (noteId, noteType) tuples
private Dictionary<int, Queue<(string noteId, string noteType)>> trackKeyMappings =
new Dictionary<int, Queue<(string, string)>>();
// Track notes currently being judged to prevent double-judging the same note
// Format: trackIndex -> HashSet of note IDs being judged
@@ -14,11 +17,28 @@ public class TrackKeyManager : MonoBehaviour
// 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;
// --- Anti-deadlock housekeeping ---
// Some notes can be pooled/disabled without firing OnTriggerExit2D, leaving stale ids in the queue.
// To guarantee the track never blocks forever, each queued id gets a TTL.
// When TTL expires, we prune it as stale.
private readonly Dictionary<string, float> noteIdExpiry = new Dictionary<string, float>();
[Header("TrackKeyManager housekeeping")]
[Tooltip("Seconds a queued note id is allowed to stay without being removed. Prevents permanent deadlocks when OnTriggerExit2D is missed.")]
[Range(0.5f, 10f)]
public float queuedIdTtlSeconds = 3f;
[Tooltip("Maximum number of stale head entries to prune per track per call.")]
[Range(1, 20)]
public int pruneBatchSize = 5;
private Coroutine cleanupCoroutine;
private void Awake()
{
if (Instance == null)
@@ -29,14 +49,129 @@ public class TrackKeyManager : MonoBehaviour
{
Destroy(gameObject);
}
// start periodic cleanup to defend against stale locks that can survive missed Unregister/Unlock calls
StartCleanupRoutine();
}
private void OnDestroy()
{
StopCleanupRoutine();
}
private void StartCleanupRoutine()
{
if (cleanupCoroutine == null)
cleanupCoroutine = StartCoroutine(CleanupStaleLocksRoutine());
}
private void StopCleanupRoutine()
{
if (cleanupCoroutine != null)
{
StopCoroutine(cleanupCoroutine);
cleanupCoroutine = null;
}
}
// Periodically scan locks and queued ids to remove stale entries that can cause deadlocks.
private IEnumerator CleanupStaleLocksRoutine()
{
while (true)
{
// run once per second (unscaled so it runs during pause)
yield return new WaitForSecondsRealtime(1f);
float now = Time.unscaledTime;
// 1) Prune head entries for all tracks (defensive)
var trackKeys = new List<int>(trackKeyMappings.Keys);
foreach (var ti in trackKeys)
{
PruneStaleHeads(ti);
}
// 2) Inspect locks and release any that reference ids that are expired or not present in any queue
var trackLockKeys = new List<int>(trackNotesBeingJudged.Keys);
foreach (var t in trackLockKeys)
{
var locks = trackNotesBeingJudged[t];
if (locks == null || locks.Count == 0) continue;
var toRemove = new List<string>();
foreach (var noteId in locks)
{
bool hasExpiry = noteIdExpiry.TryGetValue(noteId, out var expiry);
bool expired = hasExpiry && now > expiry;
// If expiry missing or expired, consider the id stale and remove lock
if (!hasExpiry || expired)
{
toRemove.Add(noteId);
continue;
}
// Also if the queues do not contain this id anywhere, it is likely stale
bool foundInQueues = false;
foreach (var qpair in trackKeyMappings)
{
foreach (var item in qpair.Value)
{
if (!string.IsNullOrEmpty(item.noteId) && item.noteId == noteId)
{
foundInQueues = true;
break;
}
}
if (foundInQueues) break;
}
if (!foundInQueues)
{
toRemove.Add(noteId);
}
}
foreach (var id in toRemove)
{
locks.Remove(id);
noteIdExpiry.Remove(id);
if (GameConfig.verboseLogs) Debug.LogWarning($"[TrackKeyManager] Cleanup: removed stale lock id={id} on track {t}");
}
// If locks become empty, remove dictionary entry to keep structure clean
if (locks.Count == 0)
{
trackNotesBeingJudged.Remove(t);
}
}
// 3) Defensive: if a track has no queued ids but has consumed-frame or locks, clear them
var allTracks = new HashSet<int>();
foreach (var k in trackKeyMappings.Keys) allTracks.Add(k);
foreach (var k in trackNotesBeingJudged.Keys) allTracks.Add(k);
foreach (var k in trackConsumedFrame.Keys) allTracks.Add(k);
foreach (var track in allTracks)
{
bool hasQueue = trackKeyMappings.ContainsKey(track) && trackKeyMappings[track].Count > 0;
bool hasLocks = trackNotesBeingJudged.ContainsKey(track) && trackNotesBeingJudged[track].Count > 0;
if (!hasQueue && hasLocks)
{
// clear locks for this track
trackNotesBeingJudged.Remove(track);
if (GameConfig.verboseLogs) Debug.LogWarning($"[TrackKeyManager] Cleanup: cleared locks for empty track {track}");
}
}
}
}
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))
{
@@ -47,20 +182,65 @@ public class TrackKeyManager : MonoBehaviour
if (GameConfig.verboseLogs) Debug.Log("[TrackKeyManager] Focus regained, cleared track consumption");
}
}
lastCheckedFrame = currentFrame;
lastFocusedState = isFocused;
}
private void PruneStaleHeads(int trackIndex)
{
if (!trackKeyMappings.TryGetValue(trackIndex, out var q) || q == null || q.Count == 0)
return;
int pruned = 0;
float now = Time.unscaledTime;
while (q.Count > 0 && pruned < Mathf.Max(1, pruneBatchSize))
{
var head = q.Peek();
if (string.IsNullOrEmpty(head.noteId))
{
q.Dequeue();
pruned++;
continue;
}
// never prune if this id is currently locked for judgement
if (trackNotesBeingJudged.TryGetValue(trackIndex, out var locks) && locks != null && locks.Contains(head.noteId))
break;
// if an expiry exists and has passed, prune
if (noteIdExpiry.TryGetValue(head.noteId, out var expiry) && now > expiry)
{
q.Dequeue();
noteIdExpiry.Remove(head.noteId);
pruned++;
if (GameConfig.verboseLogs)
Debug.LogWarning($"[TrackKeyManager] Pruned stale head on track {trackIndex}: id={head.noteId} type={head.noteType} now={now:F2} expiry={expiry:F2}");
continue;
}
// head is still within TTL
break;
}
}
/// <summary>
/// 当音符进入判定区域时,添加音符 id 到队列
/// 当音符进入判定区域时,添加音符 id 到队列(包含note类型信息)
/// noteType: "tap" 或 "hold"
/// </summary>
public void RegisterKey(int trackIndex, string noteInstanceId)
public void RegisterKey(int trackIndex, string noteInstanceId, string noteType = "tap")
{
if (!trackKeyMappings.ContainsKey(trackIndex))
trackKeyMappings[trackIndex] = new Queue<string>();
trackKeyMappings[trackIndex] = new Queue<(string, string)>();
trackKeyMappings[trackIndex].Enqueue(noteInstanceId);
trackKeyMappings[trackIndex].Enqueue((noteInstanceId, noteType));
// record/refresh TTL so the id can't block forever if exit/unregister is missed
if (!string.IsNullOrEmpty(noteInstanceId))
{
noteIdExpiry[noteInstanceId] = Time.unscaledTime + Mathf.Max(0.25f, queuedIdTtlSeconds);
}
}
/// <summary>
@@ -69,24 +249,34 @@ public class TrackKeyManager : MonoBehaviour
public void UnregisterKey(int trackIndex, string noteInstanceId)
{
if (!trackKeyMappings.ContainsKey(trackIndex) || trackKeyMappings[trackIndex].Count == 0)
{
// still clear TTL record
if (!string.IsNullOrEmpty(noteInstanceId)) noteIdExpiry.Remove(noteInstanceId);
return;
}
// If the head matches, dequeue. Otherwise try to remove from queue by rebuilding it.
if (trackKeyMappings[trackIndex].Peek() == noteInstanceId)
if (trackKeyMappings[trackIndex].Count > 0 && trackKeyMappings[trackIndex].Peek().noteId == noteInstanceId)
{
trackKeyMappings[trackIndex].Dequeue();
if (!string.IsNullOrEmpty(noteInstanceId)) noteIdExpiry.Remove(noteInstanceId);
return;
}
// Otherwise remove matching id if present (preserve order of others)
var q = trackKeyMappings[trackIndex];
var temp = new Queue<string>();
var temp = new Queue<(string, string)>();
while (q.Count > 0)
{
var v = q.Dequeue();
if (v != noteInstanceId) temp.Enqueue(v);
var item = q.Dequeue();
if (item.noteId != noteInstanceId)
{
temp.Enqueue(item);
}
}
trackKeyMappings[trackIndex] = temp;
if (!string.IsNullOrEmpty(noteInstanceId)) noteIdExpiry.Remove(noteInstanceId);
}
/// <summary>
@@ -94,13 +284,29 @@ public class TrackKeyManager : MonoBehaviour
/// </summary>
public string GetCurrentNoteId(int trackIndex)
{
PruneStaleHeads(trackIndex);
if (trackKeyMappings.ContainsKey(trackIndex) && trackKeyMappings[trackIndex].Count > 0)
{
return trackKeyMappings[trackIndex].Peek();
return trackKeyMappings[trackIndex].Peek().noteId;
}
return null; // 无音符
}
/// <summary>
/// 获取当前队头note的类型("tap" 或 "hold"
/// </summary>
public string GetCurrentNoteType(int trackIndex)
{
PruneStaleHeads(trackIndex);
if (trackKeyMappings.ContainsKey(trackIndex) && trackKeyMappings[trackIndex].Count > 0)
{
return trackKeyMappings[trackIndex].Peek().noteType;
}
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.
@@ -108,7 +314,10 @@ public class TrackKeyManager : MonoBehaviour
public bool TryLockTrackForJudge(int trackIndex, string noteId)
{
if (trackIndex < 0) return false;
// Before locking, prune any expired heads so they can't block judgement.
PruneStaleHeads(trackIndex);
if (!trackNotesBeingJudged.ContainsKey(trackIndex))
{
trackNotesBeingJudged[trackIndex] = new HashSet<string>();
@@ -131,11 +340,14 @@ public class TrackKeyManager : MonoBehaviour
public void UnlockTrackForJudge(int trackIndex, string noteId)
{
if (trackIndex < 0) return;
if (trackNotesBeingJudged.ContainsKey(trackIndex))
{
trackNotesBeingJudged[trackIndex].Remove(noteId);
}
// also clear any expiry record for this note id (it has finished its lifecycle)
if (!string.IsNullOrEmpty(noteId)) noteIdExpiry.Remove(noteId);
}
/// <summary>
@@ -146,14 +358,14 @@ public class TrackKeyManager : MonoBehaviour
{
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)
@@ -161,11 +373,11 @@ public class TrackKeyManager : MonoBehaviour
trackConsumedFrame[trackIndex] = currentFrame;
return true;
}
// Same frame, already consumed
// Same frame, already consumed - reject
return false;
}
/// <summary>
/// Force reset the consumption state (useful for testing or specific scenarios)
/// </summary>
@@ -173,4 +385,16 @@ public class TrackKeyManager : MonoBehaviour
{
trackConsumedFrame.Clear();
}
/// <summary>
/// Force clear all track locks and per-frame consumption state.
/// Used to simulate a global input release (e.g. when ending playback).
/// </summary>
public void ClearAllLocks()
{
trackNotesBeingJudged.Clear();
trackConsumedFrame.Clear();
noteIdExpiry.Clear();
if (GameConfig.verboseLogs) Debug.Log("[TrackKeyManager] ClearAllLocks called: cleared locks and consumption state");
}
}