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

This commit is contained in:
2026-01-23 18:43:23 +08:00
parent f3ece026ca
commit 0e47864569
256 changed files with 134131 additions and 31192 deletions
+437 -42
View File
@@ -1,9 +1,14 @@
using System;
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine.UI;
public class NoteSpawner : MonoBehaviour
{
public event Action AllNotesSpawned; // invoked when all notes have been spawned
public NotePool notePool; // 引用 NotePool
public Transform[] spawnPoints; // 对应轨道的生成点
public GameObject[] notePrefabs; // 短音符预制体
@@ -42,6 +47,19 @@ public class NoteSpawner : MonoBehaviour
[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;
[Header("Immediate Settlement")]
[Tooltip("Optional. If set, this will be used to trigger settlement UI (JudgeManager.TriggerAllNotesJudged). If null, will fall back to JudgeManager.Instance.")]
public JudgeManager judgeManager;
[Tooltip("Optional UI Button. When clicked, will immediately stop spawning and enter settlement.")]
public Button immediateSettlementButton;
[Tooltip("Optional: the pause UI GameObject to disable when immediate settlement is triggered.")]
public GameObject pausePanel;
[Tooltip("Optional: animations GameObject to disable when entering settlement. Will be restored on Start().")]
public GameObject animations;
// optional: constants for runtime clamping (kept for internal use)
private const float SpeedMultiplierMin = 0.8f;
private const float SpeedMultiplierMax = 1.25f;
@@ -54,8 +72,17 @@ public class NoteSpawner : MonoBehaviour
private static int holdNoteIdCounter = 0; // 全局唯一长音符 ID 计数器
// map from beatmap note index -> assigned holdNoteId (for hold notes only)
private Dictionary<int, int> noteIndexToHoldId = new Dictionary<int, int>();
private const string NoteSpeedPrefKey = "noteSpeedMultiplier";
private Coroutine spawnCoroutine;
private Coroutine settlementCoroutine;
// Guard to avoid double-trigger / deadlock
private bool immediateSettlementTriggered = false;
private void Awake()
{
// Load saved visual speed multiplier before any spawning logic uses it
@@ -65,6 +92,83 @@ public class NoteSpawner : MonoBehaviour
if (GameConfig.verboseLogs) Debug.Log($"[NoteSpawner] Loaded speedMultiplier={speedMultiplier} from PlayerPrefs");
}
private void OnEnable()
{
// 每次场景启用/加载时:清空立刻结算状态,避免进入场景即锁定
ResetImmediateSettlementState();
BindImmediateSettlementButton();
// subscribe to JudgeManager.AllNotesJudged so we disable animations on normal settlement as well
TrySubscribeJudgeManager();
}
private void Start()
{
// restore animations active state on Start
if (animations != null)
{
try { animations.SetActive(true); }
catch { }
}
// ensure subscription if JudgeManager.Instance was not ready during OnEnable
TrySubscribeJudgeManager();
}
private void OnDisable()
{
UnbindImmediateSettlementButton();
TryUnsubscribeJudgeManager();
}
private void ResetImmediateSettlementState()
{
immediateSettlementTriggered = false;
// 不强制设置 isSpawning,这个状态由 LoadBeatmap 驱动;这里只清锁
}
private void BindImmediateSettlementButton()
{
if (immediateSettlementButton == null) return;
try { immediateSettlementButton.onClick.RemoveListener(ForceImmediateSettlement); } catch { }
immediateSettlementButton.onClick.AddListener(ForceImmediateSettlement);
}
private void UnbindImmediateSettlementButton()
{
if (immediateSettlementButton == null) return;
try { immediateSettlementButton.onClick.RemoveListener(ForceImmediateSettlement); } catch { }
}
private void TrySubscribeJudgeManager()
{
var jm = judgeManager != null ? judgeManager : JudgeManager.Instance;
if (jm != null)
{
try { jm.AllNotesJudged -= OnSettlementTriggered; } catch { }
jm.AllNotesJudged += OnSettlementTriggered;
}
}
private void TryUnsubscribeJudgeManager()
{
var jm = judgeManager != null ? judgeManager : JudgeManager.Instance;
if (jm != null)
{
try { jm.AllNotesJudged -= OnSettlementTriggered; } catch { }
}
}
private void OnSettlementTriggered()
{
// Disable animations when settlement begins
if (animations != null)
{
try { animations.SetActive(false); }
catch { }
}
}
public void LoadBeatmap(Beatmap loadedBeatmap)
{
if (isSpawning) return;
@@ -89,7 +193,9 @@ public class NoteSpawner : MonoBehaviour
bpm = beatmap.bpm;
startTime = Time.time;
Debug.Log($"歌曲开始时间: {startTime}");
StartCoroutine(SpawnNotes());
// keep reference so we can stop spawning when doing immediate settlement
spawnCoroutine = StartCoroutine(SpawnNotes());
}
private IEnumerator SpawnNotes()
@@ -101,8 +207,14 @@ public class NoteSpawner : MonoBehaviour
yield break;
}
foreach (NoteData note in beatmap.notes)
// iterate with index so we can map hold notes to generated ids
for (int i = 0; i < beatmap.notes.Length; i++)
{
// 允许外部在运行中打断生成(例如“立刻结算”)
if (!isSpawning)
yield break;
NoteData note = beatmap.notes[i];
// clamp speed multiplier to supported range
float sm = Mathf.Clamp(speedMultiplier, SpeedMultiplierMin, SpeedMultiplierMax);
@@ -128,7 +240,9 @@ public class NoteSpawner : MonoBehaviour
if (note.type == "hold")
{
SpawnHoldNote(note);
// create the hold note once and record its id mapping
int hid = SpawnHoldNote(note);
noteIndexToHoldId[i] = hid;
}
else
{
@@ -137,6 +251,110 @@ public class NoteSpawner : MonoBehaviour
}
isSpawning = false;
spawnCoroutine = null;
// Notify subscribers that all notes have been spawned
AllNotesSpawned?.Invoke();
// Settlement routine will be started by GameManager via StartSettlementRoutine() call
// after receiving the AllNotesSpawned event
}
/// <summary>
/// 立刻结算:
/// - 停止继续生成音符(停止 SpawnNotes 协程)
/// - 取消 NoteSpawner 自己的延迟结算协程(PostSpawnSettlementRoutine
/// - 立刻触发结算流程(JudgeManager.TriggerAllNotesJudged
/// 注意:该方法不会清理场上已生成的音符,仅停止后续生成并进入结算。
/// </summary>
[ContextMenu("Force Immediate Settlement")]
public void ForceImmediateSettlement()
{
if (immediateSettlementTriggered)
{
Debug.LogWarning("[NoteSpawner] ForceImmediateSettlement ignored: already triggered.");
return;
}
immediateSettlementTriggered = true;
Debug.LogWarning("[NoteSpawner] ForceImmediateSettlement called: stopping further note spawning and triggering settlement now.");
// If a pause UI is assigned, disable it immediately to avoid stuck paused UI during settlement
if (pausePanel != null)
{
try
{
pausePanel.SetActive(false);
Debug.Log("[NoteSpawner] pausePanel has been disabled by immediate settlement.");
}
catch (Exception ex)
{
Debug.LogWarning("[NoteSpawner] Failed to disable pausePanel: " + ex.Message);
}
}
// Also disable animations if assigned
if (animations != null)
{
try { animations.SetActive(false); }
catch { }
}
// Restore pause manager state if present, otherwise fallback to setting timeScale
try
{
var pm = PauseManager.Instance;
if (pm != null)
{
pm.Pause(false);
if (GameConfig.verboseLogs) Debug.Log("[NoteSpawner] PauseManager.Pause(false) called to resume time.");
}
else
{
Time.timeScale = 1f;
if (GameConfig.verboseLogs) Debug.Log("[NoteSpawner] PauseManager not found; Time.timeScale set to 1 as fallback.");
}
}
catch (Exception ex)
{
try { Time.timeScale = 1f; } catch { }
Debug.LogWarning("[NoteSpawner] Exception while restoring time scale: " + ex.Message);
}
// stop spawning loop
isSpawning = false;
// cancel spawn coroutine
if (spawnCoroutine != null)
{
try { StopCoroutine(spawnCoroutine); } catch { }
spawnCoroutine = null;
}
// cancel delayed settlement coroutine if any
if (settlementCoroutine != null)
{
try { StopCoroutine(settlementCoroutine); } catch { }
settlementCoroutine = null;
}
// Disable animations when settlement begins
if (animations != null)
{
try { animations.SetActive(false); }
catch { }
}
// trigger settlement
var jm = judgeManager != null ? judgeManager : JudgeManager.Instance;
if (jm != null)
{
jm.TriggerAllNotesJudged();
}
else
{
Debug.LogError("[NoteSpawner] ForceImmediateSettlement failed: JudgeManager reference missing.");
}
}
// 生成短音符
@@ -217,25 +435,26 @@ public class NoteSpawner : MonoBehaviour
}
}
public void SpawnHoldNote(NoteData noteData)
// Modified: return generated holdNoteId so callers can map notes to ids
public int SpawnHoldNote(NoteData noteData)
{
if (noteData.trackIndex < 0 || noteData.trackIndex >= spawnPoints.Length)
{
Debug.LogError("轨道索引超出范围!");
return;
return -1;
}
KeyCode key = KeyBindingManager.GetKeyForColor(noteData.color);
if (key == KeyCode.None)
{
Debug.LogError($"未找到颜色 {noteData.color} 对应的按键!");
return;
return -1;
}
if (string.IsNullOrEmpty(noteData.color))
{
Debug.LogError("[NoteSpawner] noteData.color 为空,无法生成音符!");
return;
return -1;
}
Debug.Log($"生成时间:{Time.time}");
@@ -271,19 +490,11 @@ public class NoteSpawner : MonoBehaviour
if (startObj == null)
{
Debug.LogError("对象池返回空 hold note start");
return;
return -1;
}
// 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;
// Hold segments use absolute positioning; spawn at the lane origin.
startObj.transform.position = spawnPoint.position;
startObj.transform.rotation = Quaternion.identity;
HoldNote holdNote = startObj.GetComponent<HoldNote>();
@@ -301,7 +512,7 @@ public class NoteSpawner : MonoBehaviour
else
{
Debug.LogError("长音符 start 部分缺少 HoldNote 组件!");
return;
return -1;
}
// 生成 Middle 片段(1 .. segmentCount-1
@@ -317,16 +528,7 @@ public class NoteSpawner : MonoBehaviour
continue;
}
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.position = spawnPoint.position;
segObj.transform.rotation = Quaternion.identity;
HoldNote holdSeg = segObj.GetComponent<HoldNote>();
@@ -341,7 +543,7 @@ public class NoteSpawner : MonoBehaviour
holdSeg.visualSpeedMultiplier = smLocalHold;
// Immediately calibrate position and schedule additional checks to correct any offset
Vector3 calibSpawnPos = spawnPoint.position + Vector3.down * segYOffset;
Vector3 calibSpawnPos = spawnPoint.position;
holdSeg.CalibratePosition(calibSpawnPos, calibrateTolerance);
StartCoroutine(CalibrateAfterSpawn(holdSeg, calibSpawnPos));
}
@@ -356,21 +558,12 @@ public class NoteSpawner : MonoBehaviour
if (endObj == null)
{
Debug.LogError("对象池返回空 hold note 片段(end)!");
return;
return -1;
}
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.position = spawnPoint.position;
endObj.transform.rotation = Quaternion.identity;
// 为便于识别,将实例名追加后缀
@@ -389,7 +582,7 @@ public class NoteSpawner : MonoBehaviour
holdEnd.visualSpeedMultiplier = smLocalHold;
// schedule calibration for end as well to be safe
Vector3 calibEndPos = spawnPoint.position + Vector3.down * endYOffset;
Vector3 calibEndPos = spawnPoint.position;
holdEnd.CalibratePosition(calibEndPos, calibrateTolerance);
StartCoroutine(CalibrateAfterSpawn(holdEnd, calibEndPos));
}
@@ -397,6 +590,8 @@ public class NoteSpawner : MonoBehaviour
{
Debug.LogError("长音符 end 部分缺少 HoldNote 组件!");
}
return holdNoteId;
}
private IEnumerator CalibrateAfterSpawn(HoldNote seg, Vector3 spawnPos)
@@ -440,6 +635,206 @@ public class NoteSpawner : MonoBehaviour
return 10.75f / noteTravelTime;
}
/// <summary>
/// Public method to start the settlement routine. Called by GameManager when all notes have been spawned.
/// </summary>
public void StartSettlementRoutine()
{
if (GameConfig.verboseLogs) Debug.Log("[NoteSpawner] StartSettlementRoutine called - beginning settlement countdown");
// keep reference so it can be cancelled by ForceImmediateSettlement
if (settlementCoroutine != null) StopCoroutine(settlementCoroutine);
settlementCoroutine = StartCoroutine(PostSpawnSettlementRoutine(5));
}
private IEnumerator PostSpawnSettlementRoutine(int lookback)
{
// Get relevant notes: last notes within 3 seconds before the last note time
if (beatmap == null || beatmap.notes == null || beatmap.notes.Length == 0)
yield break;
NoteData lastNote = beatmap.notes[beatmap.notes.Length - 1];
float lastNoteTime = lastNote.time;
float timeRangeStart = lastNoteTime - 3f; // 3 seconds window instead of 0.5 seconds
// Collect notes within the range [timeRangeStart, lastNoteTime]
List<NoteData> relevantNotes = new List<NoteData>();
for (int i = beatmap.notes.Length - 1; i >= 0; i--)
{
NoteData note = beatmap.notes[i];
if (note.time >= timeRangeStart)
{
relevantNotes.Add(note);
}
else
{
break;
}
}
// Reverse to process in chronological order
relevantNotes.Reverse();
if (GameConfig.verboseLogs) Debug.Log($"[NoteSpawner] Found {relevantNotes.Count} relevant notes in range [{timeRangeStart:F3}, {lastNoteTime:F3}]");
// Analyze notes: check if any are hold notes and find the latest ending note
bool hasHoldNotes = false;
float maxEndTime = float.MinValue;
NoteData maxEndNote = null;
foreach (var note in relevantNotes)
{
float noteEndTime;
if (note.type == "hold")
{
hasHoldNotes = true;
noteEndTime = note.time + note.length;
}
else
{
noteEndTime = note.time;
}
if (noteEndTime > maxEndTime)
{
maxEndTime = noteEndTime;
maxEndNote = note;
}
if (GameConfig.verboseLogs)
{
Debug.Log($"[NoteSpawner] Note: type={note.type}, time={note.time:F3}, endTime={noteEndTime:F3}");
}
}
// Determine initial wait and type of max end note
float initialWait = 0f;
bool maxIsHold = false;
if (maxEndNote != null)
{
if (maxEndNote.type == "hold")
{
maxIsHold = true;
initialWait = maxEndNote.length;
}
else
{
maxIsHold = false;
initialWait = 0f;
}
}
if (GameConfig.verboseLogs)
{
Debug.Log($"[NoteSpawner] Max end note: type={maxEndNote?.type}, time={maxEndNote?.time:F3}, maxIsHold={maxIsHold}, initialWait={initialWait:F3}");
}
// Log final chosen extension method and total extension (initial wait + final buffer) as an error for visibility
float finalBuffer = 3f; // final buffer used by settlement routine (changed to 3s)
float totalWait = initialWait + finalBuffer;
string methodName = maxIsHold ? "hold" : "tap";
Debug.LogError($"[NoteSpawner] Settlement extension chosen: method={methodName}, initialWait={initialWait:F3}s, finalBuffer={finalBuffer:F3}s, totalWait={totalWait:F3}s");
// Clear the reference list to release memory after use
relevantNotes.Clear();
relevantNotes = null;
// Wait for initial period (hold length or 0)
if (initialWait > 0f)
{
if (GameConfig.verboseLogs) Debug.Log($"[NoteSpawner] Waiting for initial hold length: {initialWait:F3}s");
float target = Time.time + initialWait;
while (Time.time < target)
yield return null;
}
// Final buffer: use finalBuffer variable
if (GameConfig.verboseLogs) Debug.Log($"[NoteSpawner] Entering final {finalBuffer} s settlement buffer");
// Wait until 1s remaining, then clear input (clear at finalBuffer-1 seconds mark)
float timeBeforeClear = Mathf.Max(0f, finalBuffer - 1f);
if (timeBeforeClear > 0f)
{
float tBefore = Time.time + timeBeforeClear;
while (Time.time < tBefore)
yield return null;
}
// Force clear keyboard/input state at the 1s-before-end mark
ForceClearInputState(null);
// Wait the remaining 1s
float tAfter = Time.time + 1f;
while (Time.time < tAfter)
yield return null;
// Trigger settlement
try
{
if (JudgeManager.Instance != null)
{
JudgeManager.Instance.OnAllNotesJudged();
}
else
{
Debug.LogWarning("[NoteSpawner] JudgeManager.Instance is null when trying to trigger settlement");
}
}
catch (Exception ex)
{
Debug.LogWarning("[NoteSpawner] Exception while triggering settlement: " + ex);
}
}
private void ForceClearInputState(List<NoteData> relevantNotes)
{
// Fire UI key release handlers: reset InputManager key indicator colors
var im = InputManager.Instance;
if (im != null)
{
var texts = im.trackKeyTexts;
if (texts != null)
{
for (int ti = 0; ti < texts.Length; ti++)
{
if (texts[ti] != null)
texts[ti].color = im.keyInactiveColor;
}
}
}
// Reset per-frame consumption and unlock any track locks
if (TrackKeyManager.Instance != null)
{
TrackKeyManager.Instance.ResetConsumptionState();
TrackKeyManager.Instance.ClearAllLocks();
if (GameConfig.verboseLogs) Debug.Log("[NoteSpawner] TrackKeyManager consumption state reset and locks cleared");
}
// Mark all relevant hold notes as released in JudgeManager
// Only process if relevantNotes is provided (non-null)
if (relevantNotes != null && JudgeManager.Instance != null && beatmap != null && beatmap.notes != null)
{
foreach (var note in relevantNotes)
{
if (note == null) continue;
if (note.type == "hold")
{
// Find the index of this note in beatmap to get its hold ID
for (int i = 0; i < beatmap.notes.Length; i++)
{
if (beatmap.notes[i] == note && noteIndexToHoldId.ContainsKey(i))
{
int holdId = noteIndexToHoldId[i];
try { JudgeManager.Instance.RegisterNoteReleased(holdId.ToString(), true); }
catch { }
}
}
}
}
}
}
private void Update()
{
//if (globalGameTime.text=="0.01")