修复短音符延迟

This commit is contained in:
FloatGaming
2026-01-15 02:46:42 +08:00
parent 271a576289
commit f3ece026ca
10 changed files with 666 additions and 105 deletions
+25 -6
View File
@@ -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);
+135 -15
View File
@@ -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()
{
+125 -30
View File
@@ -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)
{
// 中段都标记为 middlepass 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();