diff --git a/Assets/Scenes/gamePlay_gamePlay.unity b/Assets/Scenes/gamePlay_gamePlay.unity index 4e710198..c03ff770 100644 --- a/Assets/Scenes/gamePlay_gamePlay.unity +++ b/Assets/Scenes/gamePlay_gamePlay.unity @@ -602,7 +602,7 @@ Transform: m_GameObject: {fileID: 16261624} serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} - m_LocalPosition: {x: -4.96, y: -3.25, z: 0} + m_LocalPosition: {x: -4.96, y: -3.5, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] @@ -2646,7 +2646,7 @@ Transform: m_GameObject: {fileID: 165168687} serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} - m_LocalPosition: {x: 2.56, y: -3.25, z: 0} + m_LocalPosition: {x: 2.56, y: -3.5, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] @@ -12607,6 +12607,7 @@ MonoBehaviour: calibrateTolerance: 0.02 calibrateChecks: 2 calibrateInterval: 0.01 + enableYOffsetCompensation: 0 judgeConfig: {fileID: 0} --- !u!114 &397604453 MonoBehaviour: @@ -12797,6 +12798,7 @@ MonoBehaviour: calibrateTolerance: 0.02 calibrateChecks: 2 calibrateInterval: 0.01 + enableYOffsetCompensation: 0 judgeConfig: {fileID: 11400000, guid: f870aa419242e72438b57ac9659f61e9, type: 2} --- !u!1 &400656413 GameObject: @@ -13175,6 +13177,7 @@ MonoBehaviour: calibrateTolerance: 0.02 calibrateChecks: 2 calibrateInterval: 0.01 + enableYOffsetCompensation: 0 judgeConfig: {fileID: 0} --- !u!61 &411632596 BoxCollider2D: @@ -23786,7 +23789,7 @@ Transform: m_GameObject: {fileID: 801049735} serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} - m_LocalPosition: {x: 5.04, y: -3.25, z: 0} + m_LocalPosition: {x: 5.04, y: -3.5, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] @@ -25424,12 +25427,13 @@ MonoBehaviour: m_EditorClassIdentifier: bmm: {fileID: 1261342020} sm: {fileID: 1261342024} + thisSong_backPic: {fileID: 0} exit_toSelectSongs: {fileID: 1328532448} replay_thisGame: {fileID: 841673124} display_rankList: {fileID: 1461351370} share_toSocialMedia: {fileID: 807575965} songName_Text: {fileID: 1561321613} - thisLevel_currentPercentage_Text: {fileID: 0} + thisLevel_currentPercentage_Text: {fileID: 719951169} finalScore_Text: {fileID: 1090215144} pmScoreSum_Text: {fileID: 55210107} idolScoreSum_Text: {fileID: 81893895} @@ -28817,6 +28821,7 @@ MonoBehaviour: calibrateTolerance: 0.02 calibrateChecks: 2 calibrateInterval: 0.01 + enableYOffsetCompensation: 0 judgeConfig: {fileID: 0} --- !u!61 &1090508772 BoxCollider2D: @@ -34160,7 +34165,7 @@ Transform: m_GameObject: {fileID: 1369160196} serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} - m_LocalPosition: {x: 0.04, y: -3.25, z: 0} + m_LocalPosition: {x: 0.04, y: -3.5, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] @@ -34367,6 +34372,7 @@ MonoBehaviour: calibrateTolerance: 0.02 calibrateChecks: 2 calibrateInterval: 0.01 + enableYOffsetCompensation: 0 judgeConfig: {fileID: 0} --- !u!61 &1370459979 BoxCollider2D: @@ -35739,7 +35745,7 @@ Transform: m_GameObject: {fileID: 1475663029} serializedVersion: 2 m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} - m_LocalPosition: {x: -2.46, y: -3.25, z: 0} + m_LocalPosition: {x: -2.46, y: -3.5, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] @@ -36075,6 +36081,7 @@ MonoBehaviour: calibrateTolerance: 0.02 calibrateChecks: 2 calibrateInterval: 0.01 + enableYOffsetCompensation: 0 judgeConfig: {fileID: 0} --- !u!61 &1486355885 BoxCollider2D: @@ -37034,6 +37041,7 @@ MonoBehaviour: calibrateTolerance: 0.02 calibrateChecks: 2 calibrateInterval: 0.01 + enableYOffsetCompensation: 0 judgeConfig: {fileID: 0} --- !u!61 &1572820069 BoxCollider2D: @@ -37145,6 +37153,8 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: e726455158177b3499eb8f0910c54e30, type: 3} m_Name: m_EditorClassIdentifier: + settlement_go: {fileID: 874962971} + sc: {fileID: 874962973} --- !u!4 &1577793742 Transform: m_ObjectHideFlags: 0 @@ -41954,6 +41964,7 @@ MonoBehaviour: calibrateTolerance: 0.02 calibrateChecks: 2 calibrateInterval: 0.01 + enableYOffsetCompensation: 0 judgeConfig: {fileID: 0} --- !u!61 &1938112567 BoxCollider2D: @@ -44382,6 +44393,7 @@ MonoBehaviour: calibrateTolerance: 0.02 calibrateChecks: 2 calibrateInterval: 0.01 + enableYOffsetCompensation: 0 judgeConfig: {fileID: 0} --- !u!61 &2072610047 BoxCollider2D: @@ -45168,6 +45180,7 @@ MonoBehaviour: calibrateTolerance: 0.02 calibrateChecks: 2 calibrateInterval: 0.01 + enableYOffsetCompensation: 0 judgeConfig: {fileID: 0} --- !u!61 &2112693515 BoxCollider2D: @@ -46323,6 +46336,7 @@ MonoBehaviour: calibrateTolerance: 0.02 calibrateChecks: 2 calibrateInterval: 0.01 + enableYOffsetCompensation: 0 judgeConfig: {fileID: 0} --- !u!114 &176508373887267660 MonoBehaviour: @@ -46728,6 +46742,7 @@ MonoBehaviour: calibrateTolerance: 0.02 calibrateChecks: 2 calibrateInterval: 0.01 + enableYOffsetCompensation: 0 judgeConfig: {fileID: 0} --- !u!61 &2411783544221426195 BoxCollider2D: @@ -47205,6 +47220,7 @@ MonoBehaviour: calibrateTolerance: 0.02 calibrateChecks: 2 calibrateInterval: 0.01 + enableYOffsetCompensation: 0 judgeConfig: {fileID: 0} --- !u!114 &3721014538685850233 MonoBehaviour: @@ -47620,6 +47636,7 @@ MonoBehaviour: calibrateTolerance: 0.02 calibrateChecks: 2 calibrateInterval: 0.01 + enableYOffsetCompensation: 0 judgeConfig: {fileID: 0} --- !u!114 &4363530835118640843 MonoBehaviour: @@ -47881,6 +47898,7 @@ MonoBehaviour: calibrateTolerance: 0.02 calibrateChecks: 2 calibrateInterval: 0.01 + enableYOffsetCompensation: 0 judgeConfig: {fileID: 0} --- !u!114 &4387330747821330999 MonoBehaviour: @@ -48467,6 +48485,7 @@ MonoBehaviour: calibrateTolerance: 0.02 calibrateChecks: 2 calibrateInterval: 0.01 + enableYOffsetCompensation: 0 judgeConfig: {fileID: 0} --- !u!114 &9221553951272782130 MonoBehaviour: diff --git a/Assets/scripts/gamePlay_gameplay/Animation_GenerateJudgementSituationPrefab.cs b/Assets/scripts/gamePlay_gameplay/Animation_GenerateJudgementSituationPrefab.cs index 4f7d480a..18c9b9e9 100644 --- a/Assets/scripts/gamePlay_gameplay/Animation_GenerateJudgementSituationPrefab.cs +++ b/Assets/scripts/gamePlay_gameplay/Animation_GenerateJudgementSituationPrefab.cs @@ -42,8 +42,12 @@ public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour private void Awake() { - if (Instance == null) Instance = this; - else + if (Instance == null) + { + Instance = this; + DontDestroyOnLoad(gameObject); + } + else if (Instance != this) { Destroy(gameObject); return; @@ -52,10 +56,27 @@ public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour CacheSpawnPoints(); } + private void Start() + { + // Ensure spawn points are cached after all scene objects are initialized + if (redSpawn == null && redEffect != null) + { + CacheSpawnPoints(); + } + } + private void OnEnable() { // In case this object survives scene changes / references are assigned late. - if (redSpawn == null && redEffect != null) CacheSpawnPoints(); + if (Instance == null) + { + Instance = this; + } + + if (redSpawn == null && redEffect != null) + { + CacheSpawnPoints(); + } } private void CacheSpawnPoints() @@ -66,15 +87,42 @@ public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour yellowSpawn = yellowEffect != null ? yellowEffect.transform : null; purpleSpawn = purpleEffect != null ? purpleEffect.transform : null; blueSpawn = blueEffect != null ? blueEffect.transform : null; + + if (GameConfig.verboseLogs) + { + Debug.Log($"[Animation_Generate] Cached spawn points: red={redSpawn!=null}, green={greenSpawn!=null}, yellow={yellowSpawn!=null}, purple={purpleSpawn!=null}, blue={blueSpawn!=null}"); + } } public void SpawnJudgePrefab(string color, string judgeResult) { + if (string.IsNullOrEmpty(color)) + { + Debug.LogError("[Animation_Generate] SpawnJudgePrefab called with empty color!"); + return; + } + + if (string.IsNullOrEmpty(judgeResult)) + { + Debug.LogError("[Animation_Generate] SpawnJudgePrefab called with empty judgeResult!"); + return; + } + Transform spawnPoint = GetSpawnPoint(color); - if (spawnPoint == null) return; + if (spawnPoint == null) + { + Debug.LogError($"[Animation_Generate] Could not find spawn point for color: {color}"); + return; + } GameObject prefabToUse = GetJudgePrefab(judgeResult); - if (prefabToUse == null) return; + if (prefabToUse == null) + { + Debug.LogError($"[Animation_Generate] Could not find judge prefab for result: {judgeResult}. Available: perfect={perfect_judge_prefab != null}, great={great_judge_prefab != null}, good={good_judge_prefab != null}, miss={miss_judge_prefab != null}"); + return; + } + + if (GameConfig.verboseLogs) Debug.Log($"[Animation_Generate] Spawning judge prefab: color={color}, result={judgeResult}, prefab={prefabToUse.name}"); GameObject instance = Instantiate(prefabToUse, spawnPoint); instance.transform.localPosition = Vector3.zero; @@ -159,7 +207,7 @@ public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour sr.color = c; } - // ȴȾɣֹ + // ȴȾɣֹɾ yield return new WaitForEndOfFrame(); if (obj != null) Destroy(obj); @@ -172,30 +220,52 @@ public class Animation_GenerateJudgementSituationPrefab : MonoBehaviour // If references were destroyed (Unity fake null) try to rebuild cache once. if (redSpawn == null && greenSpawn == null && yellowSpawn == null && purpleSpawn == null && blueSpawn == null) { + if (GameConfig.verboseLogs) Debug.Log("[Animation_Generate] Rebuilding spawn point cache"); CacheSpawnPoints(); } + Transform point = null; switch (color.ToLower()) { - case "red": return redSpawn; - case "green": return greenSpawn; - case "yellow": return yellowSpawn; - case "purple": return purpleSpawn; - case "blue": return blueSpawn; - default: return null; + case "red": point = redSpawn; break; + case "green": point = greenSpawn; break; + case "yellow": point = yellowSpawn; break; + case "purple": point = purpleSpawn; break; + case "blue": point = blueSpawn; break; + default: + Debug.LogWarning($"[Animation_Generate] Unknown color: {color}"); + return null; } + + if (point == null) + { + Debug.LogError($"[Animation_Generate] Spawn point for color {color} is null. Check if effects are assigned in inspector. Make sure the effect object is active in the scene."); + } + + return point; } private GameObject GetJudgePrefab(string result) { if (string.IsNullOrEmpty(result)) return null; + + GameObject prefab = null; switch (result.ToLower()) { - case "perfect": return perfect_judge_prefab; - case "great": return great_judge_prefab; - case "good": return good_judge_prefab; - case "miss": return miss_judge_prefab; - default: return null; + case "perfect": prefab = perfect_judge_prefab; break; + case "great": prefab = great_judge_prefab; break; + case "good": prefab = good_judge_prefab; break; + case "miss": prefab = miss_judge_prefab; break; + default: + Debug.LogWarning($"[Animation_Generate] Unknown judge result: {result}"); + return null; } + + if (prefab == null) + { + Debug.LogError($"[Animation_Generate] Judge prefab for '{result}' is not assigned in inspector! Check Animation_GenerateJudgementSituationPrefab component."); + } + + return prefab; } } \ No newline at end of file diff --git a/Assets/scripts/gamePlay_gameplay/HoldNote.cs b/Assets/scripts/gamePlay_gameplay/HoldNote.cs index 9e622dba..31bd1b5c 100644 --- a/Assets/scripts/gamePlay_gameplay/HoldNote.cs +++ b/Assets/scripts/gamePlay_gameplay/HoldNote.cs @@ -321,11 +321,24 @@ public class HoldNote : BaseNote if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] KeyDown detected for {noteID} key={keyToPress} time={Time.time:F3} hitTime={hitTime:F3} hasEnteredLine={hasEnteredLine}"); float pressTimeLocal = Time.time; float maxWindow = (judgeConfig?.missRange ?? 0.5f) * holdWindowMultiplier; + + // prevent multiple notes on same track being judged by a single press + if (TrackKeyManager.Instance != null && !TrackKeyManager.Instance.TryLockTrackForJudge(trackIndex, noteID)) + return; + if (Mathf.Abs(pressTimeLocal - hitTime) <= maxWindow) { HandleStart(false); // do not mark isJudged here; Start remains active for hold } + else + { + // Press is outside window, release the lock + if (TrackKeyManager.Instance != null) + { + TrackKeyManager.Instance.UnlockTrackForJudge(trackIndex, noteID); + } + } } } else if (segment == NoteSegment.Start && hasEnteredLine) @@ -360,6 +373,10 @@ public class HoldNote : BaseNote { if (Input.GetKeyUp(keyToPress)) { + // protect end handling with track lock so a single release doesn't trigger multiple ends + if (TrackKeyManager.Instance != null && !TrackKeyManager.Instance.TryLockTrackForJudge(trackIndex, noteID)) + return; + HandleEnd(); isJudged = true; } @@ -806,6 +823,8 @@ public class HoldNote : BaseNote AnimationController.Global?.StopHoldParticles(); isJudged = true; + // notify global judge manager that one final note (hold end) has been judged + JudgeManager.Instance?.NotifyNoteJudged(); ScheduleReturnToPool(0.2f); } @@ -846,6 +865,13 @@ public class HoldNote : BaseNote } if (GameConfig.verboseLogs) Debug.Log($"[HoldNote] ReturnToPool called for {noteID} segment={segment} time={Time.time:F3}"); + + // Release lock if still held + if (TrackKeyManager.Instance != null) + { + TrackKeyManager.Instance.UnlockTrackForJudge(trackIndex, noteID); + } + gameObject.SetActive(false); if (segment == NoteSegment.Start) diff --git a/Assets/scripts/gamePlay_gameplay/Judge/NoteJudgeConfig.asset b/Assets/scripts/gamePlay_gameplay/Judge/NoteJudgeConfig.asset index d9b8b5ad..ed565c85 100644 --- a/Assets/scripts/gamePlay_gameplay/Judge/NoteJudgeConfig.asset +++ b/Assets/scripts/gamePlay_gameplay/Judge/NoteJudgeConfig.asset @@ -12,7 +12,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 088ed1ed6b6731f43ad3166781e3e931, type: 3} m_Name: NoteJudgeConfig m_EditorClassIdentifier: - perfectRange: 0.1 - greatRange: 0.2 - goodRange: 0.3 - missRange: 0.4 + perfectRange: 0.05 + greatRange: 0.1 + goodRange: 0.15 + missRange: 0.2 diff --git a/Assets/scripts/gamePlay_gameplay/JudgeManager.cs b/Assets/scripts/gamePlay_gameplay/JudgeManager.cs index 4b321159..bb6706bd 100644 --- a/Assets/scripts/gamePlay_gameplay/JudgeManager.cs +++ b/Assets/scripts/gamePlay_gameplay/JudgeManager.cs @@ -1,10 +1,19 @@ +using System; using System.Collections.Generic; using UnityEngine; public class JudgeManager : MonoBehaviour { + [Header("settlement go")] + public GameObject settlement_go; + public settlementController sc; + public static JudgeManager Instance { get; private set; } + // total/remaining notes tracking for end-of-song detection + private int totalNotes = 0; + private int remainingNotes = 0; + // ===== ԭ������ ===== private Dictionary> judgeQueues = new Dictionary>(); private Dictionary startJudgedNotes = new Dictionary(); @@ -29,13 +38,95 @@ public class JudgeManager : MonoBehaviour return holdStates[noteID]; } + // Public event and hook invoked when the entire beatmap's last note has been judged. + // The method is intentionally empty so you can override or call it via the singleton. + public event Action AllNotesJudged; + + /// + /// Public empty hook called when all notes have been judged. Implement your end-of-song logic here + /// or subscribe to the AllNotesJudged event. This method is intentionally left empty. + /// + public void OnAllNotesJudged() + { + // safe null checks and invocation + if (settlement_go != null) + { + try + { + sc?.startSettlement_uiUpdate(); + } + catch (Exception ex) + { + Debug.LogWarning($"[JudgeManager] Exception calling startSettlement_uiUpdate: {ex}"); + } + settlement_go.SetActive(true); + } + else + { + Debug.LogError("结算页面不存在!"); + } + } + + /// + /// Helper to invoke the AllNotesJudged event and call the public hook. + /// Call this when you detect the last note was judged. + /// + public void TriggerAllNotesJudged() + { + try + { + AllNotesJudged?.Invoke(); + } + catch (Exception ex) + { + Debug.LogWarning($"[JudgeManager] Exception while invoking AllNotesJudged event: {ex}"); + } + try + { + OnAllNotesJudged(); + } + catch (Exception ex) + { + Debug.LogWarning($"[JudgeManager] Exception in OnAllNotesJudged hook: {ex}"); + } + } + private void Awake() { if (Instance == null) Instance = this; else Destroy(gameObject); + + if (settlement_go != null) settlement_go.SetActive(false); } - // ===== �������ж�����բ�� ===== + /// + /// Set the total number of notes in the current beatmap. Remaining notes will be initialized + /// to this value. Call before playback/spawning starts. + /// + public void SetTotalNotes(int total) + { + totalNotes = Mathf.Max(0, total); + remainingNotes = totalNotes; + Debug.Log($"[JudgeManager] SetTotalNotes: total={totalNotes}"); + } + + /// + /// Notify that one note has been finally judged (hit or miss). When remaining reaches zero, + /// TriggerAllNotesJudged will be invoked. + /// + public void NotifyNoteJudged() + { + if (remainingNotes <= 0) return; + remainingNotes = Mathf.Max(0, remainingNotes - 1); + Debug.Log($"[JudgeManager] NotifyNoteJudged: remaining={remainingNotes}"); + if (remainingNotes == 0) + { + Debug.Log("[JudgeManager] All notes judged -> Triggering AllNotesJudged"); + TriggerAllNotesJudged(); + } + } + + // ===== �������ж�����խ�� ===== public bool TryResolveStart(string noteID) { var s = GetHoldState(noteID); diff --git a/Assets/scripts/gamePlay_gameplay/Note.cs b/Assets/scripts/gamePlay_gameplay/Note.cs index d38e5151..ff554678 100644 --- a/Assets/scripts/gamePlay_gameplay/Note.cs +++ b/Assets/scripts/gamePlay_gameplay/Note.cs @@ -7,14 +7,18 @@ public class Note : BaseNote private AnimationController anim; private NoteController controller; private bool isJudged = false; + private bool hasLock = false; // track whether we hold the track lock private NoteData noteData; - [Header("�ж���������")] - public NoteJudgeConfig judgeConfig; // �ж��������ã�����Ԥ���������ʱ��ֵ + [Header("判定配置")] + public NoteJudgeConfig judgeConfig; // 判定窗口配置,包含判定时间范围 // Cache allies per track to avoid GameObject.Find on every judge. private static AllyCombatant[] allyCache; + + // Timeout handling - force miss if note isn't judged by this time + private float missDeadlineTime = -1f; private static AllyCombatant GetAllyForTrackCached(int trackIndex) { @@ -44,8 +48,13 @@ public class Note : BaseNote Speed = speed; this.hitTime = hitTime; isJudged = false; + hasLock = false; this.judgeConfig = judgeConfig; this.noteData = data; + + // Set miss deadline: hitTime + missRange (the latest time to judge before auto-miss) + float missRange = (judgeConfig?.missRange ?? 0.5f); + this.missDeadlineTime = hitTime + missRange; if (controller != null) { @@ -54,10 +63,10 @@ public class Note : BaseNote InputManager.OnKeyPressed += HandlePress; - // �ж��������ü�� + // 判定配置检查 if (judgeConfig == null) { - if (GameConfig.verboseLogs) Debug.LogError($"NoteJudgeConfig is null! �ж���������δ���룡track={trackIndex}"); + if (GameConfig.verboseLogs) Debug.LogError($"NoteJudgeConfig is null! 判定配置未赋值!track={trackIndex}"); } else { @@ -68,17 +77,77 @@ public class Note : BaseNote private void OnDestroy() { InputManager.OnKeyPressed -= HandlePress; + // Release lock if we still hold it + if (hasLock) + { + TrackKeyManager.Instance?.UnlockTrackForJudge(TrackIndex, gameObject.GetInstanceID().ToString()); + hasLock = false; + } + } + + private void Update() + { + // Force miss if we've passed the deadline without being judged + if (!isJudged && Time.time > missDeadlineTime && gameObject.activeSelf) + { + if (GameConfig.verboseLogs) Debug.Log($"[Note] {noteColor} on track {TrackIndex} exceeded miss deadline at time {Time.time:F3}, forcing Miss"); + JudgeMiss(); + } } private void HandlePress(KeyCode key) { - // ֻ�ж���ȷ��������ж��� - if (isJudged || key != keyToPress || (controller != null && !controller.IsInJudgeZone())) + // only proceed for matching key and not already judged + if (isJudged || key != keyToPress) return; - float timeDifference = Mathf.Abs(Time.time - hitTime); - // ��������ƫ���ֵ��ǰ����ֵ��� - float rawOffsetMs = (hitTime - Time.time) * 1000f; + string myId = gameObject.GetInstanceID().ToString(); + + // Ensure only one note per track consumes this physical press per frame + if (TrackKeyManager.Instance != null && !TrackKeyManager.Instance.TryConsumeTrackForFrame(TrackIndex)) + { + if (GameConfig.verboseLogs) Debug.Log($"[Note] Press ignored due to frame consumption on track {TrackIndex}"); + return; + } + + // Ensure we are the front-most note on this track. If not, ignore this press so later press can hit the next note. + var headId = TrackKeyManager.Instance?.GetCurrentNoteId(TrackIndex); + if (headId != null && headId != myId) + { + if (GameConfig.verboseLogs) Debug.Log($"[Note] Press ignored because another note is ahead on track {TrackIndex}: head={headId} me={myId}"); + return; + } + + // Try to acquire lock for this note (prevents multiple notes on same track being judged at once) + if (TrackKeyManager.Instance != null && !TrackKeyManager.Instance.TryLockTrackForJudge(TrackIndex, myId)) + return; + + hasLock = true; + + float pressTime = Time.time; + float maxWindow = (judgeConfig?.missRange ?? 0.5f); + + // If there is a controller and the note is not inside judge zone, only allow judgment + // if the press time is within the allowed window. Otherwise ignore the press. + if (controller != null && !controller.IsInJudgeZone()) + { + if (Mathf.Abs(pressTime - hitTime) > maxWindow) + { + // outside allowed window and not in judge zone -> release lock and ignore the press + if (hasLock) + { + TrackKeyManager.Instance?.UnlockTrackForJudge(TrackIndex, myId); + hasLock = false; + } + if (GameConfig.verboseLogs) Debug.Log($"[Note] {noteColor} press rejected: outside timing window and not in judge zone"); + return; + } + // otherwise within window: fall through to normal judging + } + + float timeDifference = Mathf.Abs(pressTime - hitTime); + // raw offset ms: positive = note was early (hitTime > pressTime) + float rawOffsetMs = (hitTime - pressTime) * 1000f; string judgeResult = null; if (judgeConfig != null) @@ -106,7 +175,7 @@ public class Note : BaseNote } else { - if (GameConfig.verboseLogs) Debug.Log($"{keyToPress}: Miss"); + if (GameConfig.verboseLogs) Debug.Log($"{keyToPress}: Miss (timeout)"); judgeResult = "Miss"; } } @@ -125,7 +194,7 @@ public class Note : BaseNote } else { - if (GameConfig.verboseLogs) Debug.Log($"{keyToPress}: Miss"); + if (GameConfig.verboseLogs) Debug.Log($"{keyToPress}: Miss (timeout)"); judgeResult = "Miss"; } } @@ -135,6 +204,9 @@ public class Note : BaseNote InputManager.Instance?.ShowJudgeResult(TrackIndex, judgeResult); JudgeSoundManager.Instance?.PlayJudgeSound(judgeResult); teamUIController.Instance?.OnJudgeResult(judgeResult); + + // Spawn judgment prefab - ensure noteColor is passed correctly + if (GameConfig.verboseLogs) Debug.Log($"[Note] Spawning judge prefab: color={noteColor}, result={judgeResult}"); Animation_GenerateJudgementSituationPrefab.Instance?.SpawnJudgePrefab(noteColor, judgeResult); try @@ -174,12 +246,26 @@ public class Note : BaseNote controller.StopMovement(); controller.PlayHitEffect(); } + + // Remove from per-track queue immediately so following notes become head + string myId = gameObject.GetInstanceID().ToString(); + TrackKeyManager.Instance?.UnregisterKey(TrackIndex, myId); + + // Release lock if we held it + if (hasLock) + { + TrackKeyManager.Instance?.UnlockTrackForJudge(TrackIndex, myId); + hasLock = false; + } + ReturnToPool(); if (anim != null) { anim.PlayDestroyAnimation(noteColor); } + // notify global judge manager that this short note has been finally judged (hit) + JudgeManager.Instance?.NotifyNoteJudged(); } public void JudgeMiss() @@ -194,6 +280,9 @@ public class Note : BaseNote InputManager.Instance?.ShowJudgeResult(TrackIndex, "Miss"); JudgeSoundManager.Instance?.PlayJudgeSound("Miss"); teamUIController.Instance?.OnJudgeResult("Miss"); + + // Spawn judgment prefab for Miss - ensure noteColor is passed correctly + if (GameConfig.verboseLogs) Debug.Log($"[Note.JudgeMiss] Spawning miss judge prefab: color={noteColor}"); Animation_GenerateJudgementSituationPrefab.Instance?.SpawnJudgePrefab(noteColor, "Miss"); try @@ -213,16 +302,38 @@ public class Note : BaseNote try { SkillBuilder.Instance?.NotifyNoteHit(TrackIndex, "Miss", SkillDefinition.NoteTypeTrigger.Tap); } catch { } + // notify global judge manager that this note has been finally judged (miss) + JudgeManager.Instance?.NotifyNoteJudged(); + + // Remove from per-track queue and release lock immediately so next note can be judged + string myId = gameObject.GetInstanceID().ToString(); + TrackKeyManager.Instance?.UnregisterKey(TrackIndex, myId); + if (hasLock) + { + TrackKeyManager.Instance?.UnlockTrackForJudge(TrackIndex, myId); + hasLock = false; + } + ReturnToPool(); } private void ReturnToPool() { InputManager.OnKeyPressed -= HandlePress; + // Release lock if we still hold it + if (hasLock) + { + TrackKeyManager.Instance?.UnlockTrackForJudge(TrackIndex, gameObject.GetInstanceID().ToString()); + hasLock = false; + } if (controller != null) { controller.ResetState(); } + + // Ensure we are removed from per-track queue to avoid stale entries + TrackKeyManager.Instance?.UnregisterKey(TrackIndex, gameObject.GetInstanceID().ToString()); + gameObject.SetActive(false); NotePool.Instance.ReturnNote(gameObject, noteColor); } @@ -238,15 +349,24 @@ public class Note : BaseNote } /// - /// �� Controller ֪ͨ Note �Ƿ�����ж��� + /// From Controller notify Note whether it's inside judge zone /// public void SetJudgeZone(bool inZone) { - // leaving judge zone without being judged -> Miss + // leaving judge zone if (!inZone) { - // JudgeMiss already handles UI/sound and prevents double execution. - JudgeMiss(); + // Only process if we haven't been judged yet + if (!isJudged) + { + // IMPORTANT: Don't call JudgeMiss directly here because SetJudgeZone is called + // from OnTriggerExit2D, which is in a physics callback. + // Calling ReturnToPool (which deactivates the object) inside a physics callback + // causes "GameObject is already being activated or deactivated" errors. + // Instead, mark for miss and let Update handle it next frame. + if (GameConfig.verboseLogs) Debug.Log($"[Note.SetJudgeZone] Note {noteColor} left judge zone on track {TrackIndex}, will force Miss next frame"); + missDeadlineTime = Time.time; // Force deadline to now so Update will handle it + } } } } \ No newline at end of file diff --git a/Assets/scripts/gamePlay_gameplay/NoteController.cs b/Assets/scripts/gamePlay_gameplay/NoteController.cs index 76bfbf55..b3a93165 100644 --- a/Assets/scripts/gamePlay_gameplay/NoteController.cs +++ b/Assets/scripts/gamePlay_gameplay/NoteController.cs @@ -5,23 +5,35 @@ public class NoteController : MonoBehaviour { private float speed; private bool isMoving = true; - private bool isInJudgeZone = false; // �ж������� + private bool isInJudgeZone = false; // 判定范围内 private ParticleSystem hitEffect; private Note linkedNote; + + // For absolute position tracking instead of relative translate + private Vector3 spawnPosition; + private float activationTime = -1f; // When the note should start moving (hitTime - travelTime) + private float baseSpawnYOffset = 0f; // Initial Y offset from compensation at spawn time private void Awake() { - hitEffect = GetComponentInChildren(); linkedNote = GetComponent(); + spawnPosition = transform.position; } private void Update() { - if (isMoving) + if (isMoving && activationTime >= 0f) { - transform.Translate(Vector3.down * speed * Time.deltaTime); + // Use absolute positioning based on elapsed time since activation + float elapsedSinceActivation = Time.time - activationTime; + float travelDistance = speed * Mathf.Max(0f, elapsedSinceActivation); + + // Position = spawnPoint + initial offset + downward travel + Vector3 newPos = spawnPosition; + newPos.y -= (baseSpawnYOffset + travelDistance); + transform.position = newPos; } } @@ -43,14 +55,28 @@ public class NoteController : MonoBehaviour } } - - + /// + /// Configure timing for absolute position tracking. + /// Call this from NoteSpawner after the note is spawned. + /// + public void ConfigureAbsolutePositioning(Vector3 initialSpawnPos, float hitTime, float travelTime, float initialYOffset) + { + spawnPosition = initialSpawnPos; + activationTime = hitTime - travelTime; + baseSpawnYOffset = initialYOffset; + + if (GameConfig.verboseLogs) + Debug.Log($"[NoteController] Configured: spawnPos={spawnPosition}, activationTime={activationTime:F3}, initialYOffset={initialYOffset:F4}"); + } public void ResetState() { speed = 0f; isMoving = true; isInJudgeZone = false; + activationTime = -1f; + baseSpawnYOffset = 0f; + spawnPosition = transform.position; if (hitEffect != null) { @@ -59,8 +85,6 @@ public class NoteController : MonoBehaviour } } - // �� OnTriggerEnter2D �� OnTriggerExit2D ������ Controller �� - private void OnTriggerEnter2D(Collider2D collision) { if (collision.CompareTag("JudgmentLine")) @@ -68,8 +92,8 @@ public class NoteController : MonoBehaviour if (linkedNote != null) { isInJudgeZone = true; - TrackKeyManager.Instance.RegisterKey(linkedNote.GetTrackIndex(), linkedNote.GetKey()); - + // register note by instance id so TrackKeyManager can prioritize + TrackKeyManager.Instance.RegisterKey(linkedNote.GetTrackIndex(), linkedNote.gameObject.GetInstanceID().ToString()); } } } @@ -81,18 +105,14 @@ public class NoteController : MonoBehaviour if (linkedNote != null) { isInJudgeZone = false; - TrackKeyManager.Instance.UnregisterKey(linkedNote.GetTrackIndex(), linkedNote.GetKey()); + TrackKeyManager.Instance.UnregisterKey(linkedNote.GetTrackIndex(), linkedNote.gameObject.GetInstanceID().ToString()); linkedNote.SetJudgeZone(isInJudgeZone); } } } - - - - /// - /// �� Note ��ѯ�Ƿ����ж����� + /// Query Note whether it's in judge zone /// public bool IsInJudgeZone() { diff --git a/Assets/scripts/gamePlay_gameplay/NoteSpawner.cs b/Assets/scripts/gamePlay_gameplay/NoteSpawner.cs index 60b48336..4feeb158 100644 --- a/Assets/scripts/gamePlay_gameplay/NoteSpawner.cs +++ b/Assets/scripts/gamePlay_gameplay/NoteSpawner.cs @@ -37,6 +37,11 @@ public class NoteSpawner : MonoBehaviour [Tooltip("Interval (seconds realtime) between calibration checks.")] public float calibrateInterval = 0.01f; + // 控制是否启用“追赶偏移量(y-offset compensation)”计算,默认关闭 + [Header("Early compensation (experimental)")] + [Tooltip("When enabled, spawn positioning will compensate each segment's position based on its own activation time so newly spawned segments appear at their expected traveled position. Default: OFF.")] + public bool enableYOffsetCompensation = false; + // optional: constants for runtime clamping (kept for internal use) private const float SpeedMultiplierMin = 0.8f; private const float SpeedMultiplierMax = 1.25f; @@ -72,6 +77,15 @@ public class NoteSpawner : MonoBehaviour return; } + // initialize JudgeManager total note count (count each NoteData as one logical note; + // long notes are counted once as a single logical note) + int total = 0; + if (beatmap.notes != null) + { + total = beatmap.notes.Length; + } + JudgeManager.Instance?.SetTotalNotes(total); + bpm = beatmap.bpm; startTime = Time.time; Debug.Log($"歌曲开始时间: {startTime}"); @@ -150,25 +164,52 @@ public class NoteSpawner : MonoBehaviour return; } - note.transform.position = spawnPoints[noteData.trackIndex].position; + Transform spawnPoint = spawnPoints[noteData.trackIndex]; + + // Calculate timing parameters + float smLocal = Mathf.Clamp(speedMultiplier, SpeedMultiplierMin, SpeedMultiplierMax); + float baseTravelTime = (60f / bpm) * 4f; + float effectiveTravelTime = baseTravelTime / Mathf.Max(0.0001f, smLocal); + float noteSpeed = CalculateSpeed(effectiveTravelTime); + + // Calculate hit time (realtime when note should be judged) + float rawHit = startTime + noteData.time + globalHitDelay; + float realtimeHit = Mathf.Max(0f, rawHit); + + // Calculate activation time (when note should start moving) + float activationTime = realtimeHit - effectiveTravelTime; + + // Compute early compensation for short notes if enabled + float noteYOffset = 0f; + if (enableYOffsetCompensation) + { + float timeSinceNoteActivation = Time.time - activationTime; + noteYOffset = Mathf.Max(0f, timeSinceNoteActivation * noteSpeed); + } + + // Set initial position with offset compensation + Vector3 initialPosition = spawnPoint.position + Vector3.down * noteYOffset; + note.transform.position = initialPosition; note.transform.rotation = Quaternion.identity; Note noteScript = note.GetComponent(); + NoteController noteController = note.GetComponent(); + if (noteScript != null) { - float noteSpawnTime = Time.time; - // pass realtime hit time to Note.Setup, include globalHitDelay - float rawHit = startTime + noteData.time + globalHitDelay; - float realtimeHit = Mathf.Max(0f, rawHit); // clamp to non-negative - - // clamp multiplier locally and calculate travel time and speed consistent with speedMultiplier - float smLocal = Mathf.Clamp(speedMultiplier, SpeedMultiplierMin, SpeedMultiplierMax); - float baseTravelTime = (60f / bpm) * 4f; - float effectiveTravelTime = baseTravelTime / Mathf.Max(0.0001f, smLocal); - float speed = CalculateSpeed(effectiveTravelTime); - - noteScript.Setup(key, noteData.trackIndex, speed, realtimeHit, noteData.color, judgeConfig, noteData); - if (GameConfig.verboseLogs) Debug.Log($"到达时间:{noteSpawnTime + baseTravelTime}"); + if (GameConfig.verboseLogs) Debug.Log($"生成短音符: color={noteData.color}, realtimeHit={realtimeHit:F3}, activationTime={activationTime:F3}, yOffset={noteYOffset:F4}"); + + // Setup note script with timing parameters + noteScript.Setup(key, noteData.trackIndex, noteSpeed, realtimeHit, noteData.color, judgeConfig, noteData); + + // Configure controller for absolute positioning (replaces relative Translate) + if (noteController != null) + { + noteController.ConfigureAbsolutePositioning(initialPosition, realtimeHit, effectiveTravelTime, noteYOffset); + } + + // Schedule calibration for short note to correct any drift + StartCoroutine(CalibrateNoteAfterSpawn(noteScript.GetComponent(), initialPosition)); } else { @@ -221,6 +262,10 @@ public class NoteSpawner : MonoBehaviour float effectiveTravelTimeHold = baseTravelTimeHold / Mathf.Max(0.0001f, smLocalHold); float holdSpeed = CalculateSpeed(effectiveTravelTimeHold); + // base realtime for hits + float rawBase = startTime + noteData.time + globalHitDelay; + float baseHit = Mathf.Max(0f, rawBase); + // 生成 Start 段 GameObject startObj = notePool.GetStartNote(noteData.color); if (startObj == null) @@ -228,16 +273,24 @@ public class NoteSpawner : MonoBehaviour Debug.LogError("对象池返回空 hold note start!"); return; } - startObj.transform.position = spawnPoint.position; + + // compute per-segment compensation only if enabled + float startYOffset = 0f; + if (enableYOffsetCompensation) + { + float startActivation = baseHit - effectiveTravelTimeHold; + float timeSinceActivation = Time.time - startActivation; + startYOffset = Mathf.Max(0f, timeSinceActivation * holdSpeed); + } + + startObj.transform.position = spawnPoint.position + Vector3.down * startYOffset; startObj.transform.rotation = Quaternion.identity; HoldNote holdNote = startObj.GetComponent(); if (holdNote != null) { // pass realtime hit time (startTime + note.time) and delay 0, include globalHitDelay - float rawStartHit = startTime + noteData.time + globalHitDelay; - float startHit = Mathf.Max(0f, rawStartHit); - holdNote.Setup(holdNoteId, noteData.trackIndex, holdSpeed, startHit, 0f, false, scheduledEndTime, noteData.color, key, "start", judgeConfig, noteData); + holdNote.Setup(holdNoteId, noteData.trackIndex, holdSpeed, baseHit, 0f, false, scheduledEndTime, noteData.color, key, "start", judgeConfig, noteData); // compute visual scale for hold segments so pieces visually connect across speedMultiplier changes float visualScale = 0.9f * smLocalHold + 0.1f; // linear fit: f(1)=1, f(2)=1.9 @@ -264,15 +317,22 @@ public class NoteSpawner : MonoBehaviour continue; } - segObj.transform.position = spawnPoint.position; + float segYOffset = 0f; + if (enableYOffsetCompensation) + { + float segHit = baseHit + segmentDelay; + float segActivation = segHit - effectiveTravelTimeHold; + float timeSinceActivationSeg = Time.time - segActivation; + segYOffset = Mathf.Max(0f, timeSinceActivationSeg * holdSpeed); + } + + segObj.transform.position = spawnPoint.position + Vector3.down * segYOffset; segObj.transform.rotation = Quaternion.identity; HoldNote holdSeg = segObj.GetComponent(); if (holdSeg != null) { // 中段都标记为 middle,pass realtime base time and segmentDelay - float rawBase = startTime + noteData.time + globalHitDelay; - float baseHit = Mathf.Max(0f, rawBase); holdSeg.Setup(holdNoteId, noteData.trackIndex, holdSpeed, baseHit, segmentDelay, false, scheduledEndTime, noteData.color, key, "middle", judgeConfig, noteData); // apply same visual scale so middle pieces visually connect @@ -281,8 +341,9 @@ public class NoteSpawner : MonoBehaviour holdSeg.visualSpeedMultiplier = smLocalHold; // Immediately calibrate position and schedule additional checks to correct any offset - holdSeg.CalibratePosition(spawnPoint.position, calibrateTolerance); - StartCoroutine(CalibrateAfterSpawn(holdSeg, spawnPoint.position)); + Vector3 calibSpawnPos = spawnPoint.position + Vector3.down * segYOffset; + holdSeg.CalibratePosition(calibSpawnPos, calibrateTolerance); + StartCoroutine(CalibrateAfterSpawn(holdSeg, calibSpawnPos)); } else { @@ -297,7 +358,19 @@ public class NoteSpawner : MonoBehaviour Debug.LogError("对象池返回空 hold note 片段(end)!"); return; } - endObj.transform.position = spawnPoint.position; + + float endDelay = segmentCount * actualSegmentInterval; // 不按 speedMultiplier 缩放,使用谱面长度保证尾段紧随中段 + + float endYOffset = 0f; + if (enableYOffsetCompensation) + { + float endHit = baseHit + endDelay; + float endActivation = endHit - effectiveTravelTimeHold; + float timeSinceActivationEnd = Time.time - endActivation; + endYOffset = Mathf.Max(0f, timeSinceActivationEnd * holdSpeed); + } + + endObj.transform.position = spawnPoint.position + Vector3.down * endYOffset; endObj.transform.rotation = Quaternion.identity; // 为便于识别,将实例名追加后缀 @@ -308,10 +381,6 @@ public class NoteSpawner : MonoBehaviour HoldNote holdEnd = endObj.GetComponent(); if (holdEnd != null) { - // 将 delay 设置为整个 hold 长度 (或 segmentCount * actualSegmentInterval),确保尾部在最后中段之后 - float endDelay = segmentCount * actualSegmentInterval; // 不按 speedMultiplier 缩放,使用谱面长度保证尾段紧随中段 - float rawBase = startTime + noteData.time + globalHitDelay; - float baseHit = Mathf.Max(0f, rawBase); holdEnd.Setup(holdNoteId, noteData.trackIndex, holdSpeed, baseHit, endDelay, true, scheduledEndTime, noteData.color, key, "end", judgeConfig, noteData); // apply visual scale to end piece as well @@ -320,8 +389,9 @@ public class NoteSpawner : MonoBehaviour holdEnd.visualSpeedMultiplier = smLocalHold; // schedule calibration for end as well to be safe - holdEnd.CalibratePosition(spawnPoint.position, calibrateTolerance); - StartCoroutine(CalibrateAfterSpawn(holdEnd, spawnPoint.position)); + Vector3 calibEndPos = spawnPoint.position + Vector3.down * endYOffset; + holdEnd.CalibratePosition(calibEndPos, calibrateTolerance); + StartCoroutine(CalibrateAfterSpawn(holdEnd, calibEndPos)); } else { @@ -340,6 +410,31 @@ public class NoteSpawner : MonoBehaviour } } + /// + /// 校验短音符的位置,防止其因计时偏差而位移 + /// + private IEnumerator CalibrateNoteAfterSpawn(NoteController noteController, Vector3 expectedSpawnPos) + { + if (noteController == null) yield break; + + GameObject noteObj = noteController.gameObject; + for (int i = 0; i < Mathf.Max(1, calibrateChecks); i++) + { + yield return new WaitForSecondsRealtime(calibrateInterval); + if (noteObj == null || !noteObj.activeSelf) yield break; + + // Check if note position deviates significantly from expected spawn position + float distanceDeviation = Vector3.Distance(noteObj.transform.position, expectedSpawnPos); + if (distanceDeviation > calibrateTolerance) + { + // Snap to expected position while allowing vertical movement + Vector3 correctedPos = expectedSpawnPos; + noteObj.transform.position = correctedPos; + if (GameConfig.verboseLogs) Debug.Log($"[NoteSpawner] Calibrated short note position, deviation was {distanceDeviation:F4}"); + } + } + } + private float CalculateSpeed(float noteTravelTime) { return 10.75f / noteTravelTime; diff --git a/Assets/scripts/gamePlay_gameplay/TrackKeyManager.cs b/Assets/scripts/gamePlay_gameplay/TrackKeyManager.cs index e75571ca..212f193e 100644 --- a/Assets/scripts/gamePlay_gameplay/TrackKeyManager.cs +++ b/Assets/scripts/gamePlay_gameplay/TrackKeyManager.cs @@ -5,7 +5,19 @@ public class TrackKeyManager : MonoBehaviour { public static TrackKeyManager Instance { get; private set; } - private Dictionary> trackKeyMappings = new Dictionary>(); + // 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() { @@ -18,41 +30,147 @@ public class TrackKeyManager : MonoBehaviour Destroy(gameObject); } } - - /// - /// жʱӰӳ - /// - public void RegisterKey(int trackIndex, KeyCode key) + + private void Update() { - if (!trackKeyMappings.ContainsKey(trackIndex)) - trackKeyMappings[trackIndex] = new Queue(); - - trackKeyMappings[trackIndex].Enqueue(key); + // 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 UnregisterKey(int trackIndex, KeyCode key) + 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 (trackKeyMappings[trackIndex].Peek() == key) + // 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 KeyCode GetCurrentKey(int trackIndex) + public string GetCurrentNoteId(int trackIndex) { if (trackKeyMappings.ContainsKey(trackIndex) && trackKeyMappings[trackIndex].Count > 0) { return trackKeyMappings[trackIndex].Peek(); } - return KeyCode.None; // ް + 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(); } } diff --git a/Assets/scripts/gamePlay_gameplay/settlementController.cs b/Assets/scripts/gamePlay_gameplay/settlementController.cs index fc1e58c4..3ae6540b 100644 --- a/Assets/scripts/gamePlay_gameplay/settlementController.cs +++ b/Assets/scripts/gamePlay_gameplay/settlementController.cs @@ -10,6 +10,7 @@ public class settlementController : MonoBehaviour [SerializeField] private ScoreManager sm; private SongData thisSong_so; private int maxScore_sum = 2000000; + [SerializeField] private Image thisSong_backPic; [Header("ת֮ť")] public Button exit_toSelectSongs; public Button replay_thisGame; @@ -65,6 +66,7 @@ public class settlementController : MonoBehaviour if(!sm) { songName_Text.text = bmm.parsedTitle; + thisSong_backPic.sprite = bmm.assignedSongData.illustration; finalScore_Text.text = (sm.allSum_pmScore + sm.allSum_idolScore).ToString(); pmScoreSum_Text.text = sm.allSum_pmScore.ToString();