using UnityEngine; using System.Collections; using TMPro; public class NoteSpawner : MonoBehaviour { public NotePool notePool; // 引用 NotePool public Transform[] spawnPoints; // 对应轨道的生成点 public GameObject[] notePrefabs; // 短音符预制体 public TextMeshProUGUI globalGameTime; // 长音符预制体数组(分别为 start, middle, end) public GameObject[] holdNoteStartPrefabs; public GameObject[] holdNoteMiddlePrefabs; public GameObject[] holdNoteEndPrefabs; public float spawnOffset = 0f; // 生成音符时的时间偏移量 public float bpm; [Header("Global timing adjustments")] [Tooltip("Global additional realtime offset (seconds) added to hit times for all notes. Use to test input latency or adjust judgement timing. Default 0. Can be negative to make notes arrive earlier.")] public float globalHitDelay = 0f; // 新增:视觉下落速度倍率(public,可在 Inspector 调整) // 默认 1.0 = 原始速度。>1 加速下落(视觉更快),<1 减速下落(视觉更慢)。 [Header("Visual speed settings")] [Tooltip("Multiplier applied to visual fall speed. Changing this will automatically adjust spawn timing so notes still arrive at their original beat times.")] [Range(0.8f, 1.25f)] public float speedMultiplier = 1f; [Header("Calibration")] [Tooltip("Tolerance (world units) for snapping middle segments to expected position after spawn.")] public float calibrateTolerance = 0.02f; [Tooltip("How many calibration checks to perform after spawn (spread over frames).")] public int calibrateChecks = 2; [Tooltip("Interval (seconds realtime) between calibration checks.")] public float calibrateInterval = 0.01f; // optional: constants for runtime clamping (kept for internal use) private const float SpeedMultiplierMin = 0.8f; private const float SpeedMultiplierMax = 1.25f; public NoteJudgeConfig judgeConfig; // 判定区间配置,需在 Inspector 赋值 private Beatmap beatmap; private float startTime; // 存储歌曲开始时间 private bool isSpawning = false; private static int holdNoteIdCounter = 0; // 全局唯一长音符 ID 计数器 private const string NoteSpeedPrefKey = "noteSpeedMultiplier"; private void Awake() { // Load saved visual speed multiplier before any spawning logic uses it float saved = PlayerPrefs.GetFloat(NoteSpeedPrefKey, speedMultiplier); saved = Mathf.Clamp(saved, SpeedMultiplierMin, SpeedMultiplierMax); speedMultiplier = saved; if (GameConfig.verboseLogs) Debug.Log($"[NoteSpawner] Loaded speedMultiplier={speedMultiplier} from PlayerPrefs"); } public void LoadBeatmap(Beatmap loadedBeatmap) { if (isSpawning) return; isSpawning = true; beatmap = loadedBeatmap; if (beatmap == null) { Debug.LogError("加载的谱面为空!"); return; } bpm = beatmap.bpm; startTime = Time.time; Debug.Log($"歌曲开始时间: {startTime}"); StartCoroutine(SpawnNotes()); } private IEnumerator SpawnNotes() { if (beatmap == null || beatmap.notes == null) { Debug.LogError("谱面数据为空!"); isSpawning = false; yield break; } foreach (NoteData note in beatmap.notes) { // clamp speed multiplier to supported range float sm = Mathf.Clamp(speedMultiplier, SpeedMultiplierMin, SpeedMultiplierMax); // base travel time derived from BPM (beats -> seconds) float baseTravelTime = (60f / bpm) * 4f; // effective travel time adjusted by speedMultiplier. Clamp to avoid divide by zero. float effectiveTravelTime = baseTravelTime / Mathf.Max(0.0001f, sm); float spawnTime = note.time - effectiveTravelTime; float delay = spawnTime - (Time.time - startTime) + spawnOffset; if (GameConfig.verboseLogs) Debug.Log($"delay: {delay}"); if (delay > 0) { // Use scaled-time wait so spawning is paused while Time.timeScale==0 (PauseManager pause) float target = Time.time + delay; // Wait using frames so this loop respects Time.timeScale (Time.time won't advance when paused) while (Time.time < target) { yield return null; } } if (note.type == "hold") { SpawnHoldNote(note); } else { SpawnNote(note); } } isSpawning = false; } // 生成短音符 public void SpawnNote(NoteData noteData) { if (noteData.trackIndex < 0 || noteData.trackIndex >= spawnPoints.Length) { Debug.LogError("轨道索引超出范围!"); return; } KeyCode key = KeyBindingManager.GetKeyForColor(noteData.color); if (key == KeyCode.None) { Debug.LogError($"未找到颜色 {noteData.color} 对应的按键!请检查 KeyBindingManager 是否正确初始化。"); return; } if (GameConfig.verboseLogs) Debug.Log($"生成短音符:颜色={noteData.color}, 按键={key}, 轨道={noteData.trackIndex}"); GameObject note = notePool.GetNote(noteData.color); if (note == null) { Debug.LogError("对象池返回了一个空音符!"); return; } note.transform.position = spawnPoints[noteData.trackIndex].position; note.transform.rotation = Quaternion.identity; Note noteScript = note.GetComponent(); if (noteScript != null) { float noteSpawnTime = Time.time; // pass realtime hit time to Note.Setup, include globalHitDelay float rawHit = startTime + noteData.time + globalHitDelay; float realtimeHit = Mathf.Max(0f, rawHit); // clamp to non-negative // clamp multiplier locally and calculate travel time and speed consistent with speedMultiplier float smLocal = Mathf.Clamp(speedMultiplier, SpeedMultiplierMin, SpeedMultiplierMax); float baseTravelTime = (60f / bpm) * 4f; float effectiveTravelTime = baseTravelTime / Mathf.Max(0.0001f, smLocal); float speed = CalculateSpeed(effectiveTravelTime); noteScript.Setup(key, noteData.trackIndex, speed, realtimeHit, noteData.color, judgeConfig, noteData); if (GameConfig.verboseLogs) Debug.Log($"到达时间:{noteSpawnTime + baseTravelTime}"); } else { Debug.LogError("音符预制体缺少 Note 组件!"); } } public void SpawnHoldNote(NoteData noteData) { if (noteData.trackIndex < 0 || noteData.trackIndex >= spawnPoints.Length) { Debug.LogError("轨道索引超出范围!"); return; } KeyCode key = KeyBindingManager.GetKeyForColor(noteData.color); if (key == KeyCode.None) { Debug.LogError($"未找到颜色 {noteData.color} 对应的按键!"); return; } if (string.IsNullOrEmpty(noteData.color)) { Debug.LogError("[NoteSpawner] noteData.color 为空,无法生成音符!"); return; } Debug.Log($"生成时间:{Time.time}"); Debug.Log($"生成长音符:颜色={noteData.color}, 按键={key}, 轨道={noteData.trackIndex}"); float segmentInterval = (60f / bpm) / 4f; int segmentCount = Mathf.CeilToInt(noteData.length / segmentInterval); if (segmentCount < 1) segmentCount = 1; // 至少一个片段 // 如果用节拍计算的片段间隔与实际长度不整除,实际用于分段的间隔应当按实际长度均分, // 以确保尾部音符排在最后一个中段之后并且到达时间等于谱面结束时间。 float actualSegmentInterval = noteData.length / segmentCount; // 实际用于中段间隔 Transform spawnPoint = spawnPoints[noteData.trackIndex]; // scheduledEndTime 使用谱面时间(note.time + length),但转换为实时 float rawScheduledEnd = startTime + (noteData.time + noteData.length) + globalHitDelay; float scheduledEndTime = Mathf.Max(0f, rawScheduledEnd); // clamp to non-negative // 生成唯一 id(改为自增计数器,避免随机冲突) int holdNoteId = ++holdNoteIdCounter; // calculate speed using effective travel time float smLocalHold = Mathf.Clamp(speedMultiplier, SpeedMultiplierMin, SpeedMultiplierMax); float baseTravelTimeHold = (60f / bpm) * 4f; float effectiveTravelTimeHold = baseTravelTimeHold / Mathf.Max(0.0001f, smLocalHold); float holdSpeed = CalculateSpeed(effectiveTravelTimeHold); // 生成 Start 段 GameObject startObj = notePool.GetStartNote(noteData.color); if (startObj == null) { Debug.LogError("对象池返回空 hold note start!"); return; } startObj.transform.position = spawnPoint.position; startObj.transform.rotation = Quaternion.identity; HoldNote holdNote = startObj.GetComponent(); if (holdNote != null) { // pass realtime hit time (startTime + note.time) and delay 0, include globalHitDelay float rawStartHit = startTime + noteData.time + globalHitDelay; float startHit = Mathf.Max(0f, rawStartHit); holdNote.Setup(holdNoteId, noteData.trackIndex, holdSpeed, startHit, 0f, false, scheduledEndTime, noteData.color, key, "start", judgeConfig, noteData); // 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 holdNote.ApplyVisualScale(visualScale); // inform hold note of visual speed so it can adapt judgement windows if needed holdNote.visualSpeedMultiplier = smLocalHold; } else { Debug.LogError("长音符 start 部分缺少 HoldNote 组件!"); return; } // 生成 Middle 片段(1 .. segmentCount-1) for (int i = 1; i < segmentCount; i++) { // keep segment delays based on actualSegmentInterval (unscaled) so middle pieces are consecutive regardless of visual speed float segmentDelay = i * actualSegmentInterval; // 不按 speedMultiplier 缩放,保证中段连续 GameObject segObj = notePool.GetHoldNoteSegment(noteData.color); if (segObj == null) { Debug.LogError("对象池返回空 hold note 片段!"); continue; } segObj.transform.position = spawnPoint.position; segObj.transform.rotation = Quaternion.identity; HoldNote holdSeg = segObj.GetComponent(); if (holdSeg != null) { // 中段都标记为 middle,pass realtime base time and segmentDelay float rawBase = startTime + noteData.time + globalHitDelay; float baseHit = Mathf.Max(0f, rawBase); holdSeg.Setup(holdNoteId, noteData.trackIndex, holdSpeed, baseHit, segmentDelay, false, scheduledEndTime, noteData.color, key, "middle", judgeConfig, noteData); // apply same visual scale so middle pieces visually connect float visualScaleMid = 0.9f * smLocalHold + 0.1f; holdSeg.ApplyVisualScale(visualScaleMid); holdSeg.visualSpeedMultiplier = smLocalHold; // Immediately calibrate position and schedule additional checks to correct any offset holdSeg.CalibratePosition(spawnPoint.position, calibrateTolerance); StartCoroutine(CalibrateAfterSpawn(holdSeg, spawnPoint.position)); } else { Debug.LogError("长音符片段缺少 HoldNote 组件!"); } } // 始终在中段之后生成一个明确的 End 段(尾部音符) GameObject endObj = notePool.GetHoldNoteEndSegment(noteData.color); if (endObj == null) { Debug.LogError("对象池返回空 hold note 片段(end)!"); return; } endObj.transform.position = spawnPoint.position; endObj.transform.rotation = Quaternion.identity; // 为便于识别,将实例名追加后缀 if (GameConfig.verboseLogs) endObj.name = endObj.name + "_end"; if (GameConfig.verboseLogs) Debug.Log($"生成尾部音符: {endObj.name} (holdId={holdNoteId}, color={noteData.color})"); HoldNote holdEnd = endObj.GetComponent(); if (holdEnd != null) { // 将 delay 设置为整个 hold 长度 (或 segmentCount * actualSegmentInterval),确保尾部在最后中段之后 float endDelay = segmentCount * actualSegmentInterval; // 不按 speedMultiplier 缩放,使用谱面长度保证尾段紧随中段 float rawBase = startTime + noteData.time + globalHitDelay; float baseHit = Mathf.Max(0f, rawBase); holdEnd.Setup(holdNoteId, noteData.trackIndex, holdSpeed, baseHit, endDelay, true, scheduledEndTime, noteData.color, key, "end", judgeConfig, noteData); // apply visual scale to end piece as well float visualScaleEnd = 0.9f * smLocalHold + 0.1f; holdEnd.ApplyVisualScale(visualScaleEnd); holdEnd.visualSpeedMultiplier = smLocalHold; // schedule calibration for end as well to be safe holdEnd.CalibratePosition(spawnPoint.position, calibrateTolerance); StartCoroutine(CalibrateAfterSpawn(holdEnd, spawnPoint.position)); } else { Debug.LogError("长音符 end 部分缺少 HoldNote 组件!"); } } private IEnumerator CalibrateAfterSpawn(HoldNote seg, Vector3 spawnPos) { if (seg == null) yield break; for (int i = 0; i < Mathf.Max(1, calibrateChecks); i++) { yield return new WaitForSecondsRealtime(calibrateInterval); if (seg == null || !seg.gameObject.activeSelf) yield break; seg.CalibratePosition(spawnPos, calibrateTolerance); } } private float CalculateSpeed(float noteTravelTime) { return 10.75f / noteTravelTime; } private void Update() { //if (globalGameTime.text=="0.01") //{ // bgMusicAudioSource.Play(); //} //else return; } }