修复短音符延迟
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<KeyCode, Queue<Note>> judgeQueues = new Dictionary<KeyCode, Queue<Note>>();
|
||||
private Dictionary<string, bool> startJudgedNotes = new Dictionary<string, bool>();
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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("结算页面不存在!");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Helper to invoke the AllNotesJudged event and call the public hook.
|
||||
/// Call this when you detect the last note was judged.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
// ===== �������ж�����բ�� =====
|
||||
/// <summary>
|
||||
/// Set the total number of notes in the current beatmap. Remaining notes will be initialized
|
||||
/// to this value. Call before playback/spawning starts.
|
||||
/// </summary>
|
||||
public void SetTotalNotes(int total)
|
||||
{
|
||||
totalNotes = Mathf.Max(0, total);
|
||||
remainingNotes = totalNotes;
|
||||
Debug.Log($"[JudgeManager] SetTotalNotes: total={totalNotes}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notify that one note has been finally judged (hit or miss). When remaining reaches zero,
|
||||
/// TriggerAllNotesJudged will be invoked.
|
||||
/// </summary>
|
||||
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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// �� Controller ֪ͨ Note �Ƿ�����ж���
|
||||
/// From Controller notify Note whether it's inside judge zone
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ParticleSystem>();
|
||||
linkedNote = GetComponent<Note>();
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Configure timing for absolute position tracking.
|
||||
/// Call this from NoteSpawner after the note is spawned.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// �� Note ��ѯ�Ƿ����ж�����
|
||||
/// Query Note whether it's in judge zone
|
||||
/// </summary>
|
||||
public bool IsInJudgeZone()
|
||||
{
|
||||
|
||||
@@ -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<Note>();
|
||||
NoteController noteController = note.GetComponent<NoteController>();
|
||||
|
||||
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<NoteController>(), 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<HoldNote>();
|
||||
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<HoldNote>();
|
||||
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<HoldNote>();
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验短音符的位置,防止其因计时偏差而位移
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
@@ -5,7 +5,19 @@ public class TrackKeyManager : MonoBehaviour
|
||||
{
|
||||
public static TrackKeyManager Instance { get; private set; }
|
||||
|
||||
private Dictionary<int, Queue<KeyCode>> trackKeyMappings = new Dictionary<int, Queue<KeyCode>>();
|
||||
// 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()
|
||||
{
|
||||
@@ -18,41 +30,147 @@ public class TrackKeyManager : MonoBehaviour
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当音符进入判定区域时,添加按键映射
|
||||
/// </summary>
|
||||
public void RegisterKey(int trackIndex, KeyCode key)
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!trackKeyMappings.ContainsKey(trackIndex))
|
||||
trackKeyMappings[trackIndex] = new Queue<KeyCode>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 当音符离开判定区域时,移除按键映射
|
||||
/// 当音符进入判定区域时,添加音符 id 到队列
|
||||
/// </summary>
|
||||
public void UnregisterKey(int trackIndex, KeyCode key)
|
||||
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 (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<string>();
|
||||
while (q.Count > 0)
|
||||
{
|
||||
var v = q.Dequeue();
|
||||
if (v != noteInstanceId) temp.Enqueue(v);
|
||||
}
|
||||
trackKeyMappings[trackIndex] = temp;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取轨道当前的按键(队列头)
|
||||
/// 获取轨道当前排在队首的音符实例 id(队列头)
|
||||
/// </summary>
|
||||
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; // 无音符
|
||||
}
|
||||
|
||||
/// <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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user