超大量的更新 修复很多问题,gameplay特效初步
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using UnityEngine;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
@@ -9,27 +9,23 @@ 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; // 短音符预制体
|
||||
public NotePool notePool; // Documentation text normalized.
|
||||
public Transform[] spawnPoints; // Documentation text normalized.
|
||||
public GameObject[] notePrefabs; // Documentation text normalized.
|
||||
|
||||
public TextMeshProUGUI globalGameTime;
|
||||
|
||||
// 长音符预制体数组(分别为 start, middle, end)
|
||||
public GameObject[] holdNoteStartPrefabs;
|
||||
// Documentation text normalized.
|
||||
public GameObject[] holdNoteMiddlePrefabs;
|
||||
public GameObject[] holdNoteEndPrefabs;
|
||||
|
||||
public float spawnOffset = 0f; // 生成音符时的时间偏移量
|
||||
public float bpm;
|
||||
public float spawnOffset = 0f; // Documentation text normalized.
|
||||
|
||||
[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")]
|
||||
// Documentation text normalized.
|
||||
[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;
|
||||
@@ -44,7 +40,7 @@ public class NoteSpawner : MonoBehaviour
|
||||
private WaitForSecondsRealtime calibrateWait;
|
||||
private float calibrateWaitSeconds = float.NaN;
|
||||
|
||||
// 控制是否启用“追赶偏移量(y-offset compensation)”计算,默认关闭
|
||||
// Documentation text normalized.
|
||||
[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;
|
||||
@@ -66,13 +62,13 @@ public class NoteSpawner : MonoBehaviour
|
||||
private const float SpeedMultiplierMin = 0.8f;
|
||||
private const float SpeedMultiplierMax = 1.25f;
|
||||
|
||||
public NoteJudgeConfig judgeConfig; // 判定区间配置,需在 Inspector 赋值
|
||||
|
||||
public NoteJudgeConfig judgeConfig; // Documentation text normalized.
|
||||
private Beatmap beatmap;
|
||||
private float startTime; // 存储歌曲开始时间
|
||||
private float startTime; // Documentation text normalized.
|
||||
private float bpm = 120f;
|
||||
private bool isSpawning = false;
|
||||
|
||||
// 生成唯一 id(改为自增计数器,避免随机冲突)
|
||||
// Documentation text normalized.
|
||||
private static int holdNoteIdCounter = 0;
|
||||
|
||||
// map from beatmap note index -> assigned holdNoteId (for hold notes only)
|
||||
@@ -100,8 +96,7 @@ public class NoteSpawner : MonoBehaviour
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
// 每次场景启用/加载时:清空立刻结算状态,避免进入场景即锁定
|
||||
ResetImmediateSettlementState();
|
||||
// Documentation text normalized.
|
||||
BindImmediateSettlementButton();
|
||||
|
||||
// subscribe to JudgeManager.AllNotesJudged so we disable animations on normal settlement as well
|
||||
@@ -130,7 +125,7 @@ public class NoteSpawner : MonoBehaviour
|
||||
private void ResetImmediateSettlementState()
|
||||
{
|
||||
immediateSettlementTriggered = false;
|
||||
// 不强制设置 isSpawning,这个状态由 LoadBeatmap 驱动;这里只清锁
|
||||
// Documentation text normalized.
|
||||
}
|
||||
|
||||
private void BindImmediateSettlementButton()
|
||||
@@ -208,7 +203,7 @@ public class NoteSpawner : MonoBehaviour
|
||||
{
|
||||
if (beatmap == null || beatmap.notes == null)
|
||||
{
|
||||
Debug.LogError("谱面数据为空!");
|
||||
Debug.LogError("Beatmap note data is null.");
|
||||
isSpawning = false;
|
||||
yield break;
|
||||
}
|
||||
@@ -223,7 +218,7 @@ public class NoteSpawner : MonoBehaviour
|
||||
// iterate with index so we can map hold notes to generated ids
|
||||
for (int i = 0; i < beatmap.notes.Length; i++)
|
||||
{
|
||||
// 允许外部在运行中打断生成(例如“立刻结算”)
|
||||
// Documentation text normalized.
|
||||
if (!isSpawning)
|
||||
yield break;
|
||||
|
||||
@@ -273,26 +268,25 @@ public class NoteSpawner : MonoBehaviour
|
||||
return key;
|
||||
}
|
||||
|
||||
// 生成短音符
|
||||
public void SpawnNote(NoteData noteData, float sm, float effectiveTravelTime, float noteSpeed)
|
||||
{
|
||||
if (noteData.trackIndex < 0 || noteData.trackIndex >= spawnPoints.Length)
|
||||
{
|
||||
Debug.LogError("轨道索引超出范围!");
|
||||
Debug.LogError("Track index out of range.");
|
||||
return;
|
||||
}
|
||||
|
||||
KeyCode key = GetCachedKeyCode(noteData.color);
|
||||
if (key == KeyCode.None)
|
||||
{
|
||||
Debug.LogError($"未找到颜色 {noteData.color} 对应的按键!请检查 KeyBindingManager 是否正确初始化。");
|
||||
Debug.LogError($"No key binding found for note color '{noteData.color}'.");
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject note = notePool.GetNote(noteData.color);
|
||||
if (note == null)
|
||||
{
|
||||
if (JudgeManager.IsDebugEnabled) Debug.LogError("对象池返回了一个空音符!");
|
||||
if (JudgeManager.IsDebugEnabled) Debug.LogError("NotePool returned a null short note object.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -337,7 +331,7 @@ public class NoteSpawner : MonoBehaviour
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("音符预制体缺少 Note 组件!");
|
||||
Debug.LogError("Note prefab is missing Note component.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,7 +340,7 @@ public class NoteSpawner : MonoBehaviour
|
||||
{
|
||||
if (noteData.trackIndex < 0 || noteData.trackIndex >= spawnPoints.Length)
|
||||
{
|
||||
Debug.LogError("轨道索引超出范围!");
|
||||
Debug.LogError("Track index out of range.");
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -364,28 +358,25 @@ public class NoteSpawner : MonoBehaviour
|
||||
}
|
||||
|
||||
int segmentCount = Mathf.CeilToInt(noteData.length / segmentInterval);
|
||||
if (segmentCount < 1) segmentCount = 1; // 至少一个片段
|
||||
// 如果用节拍计算的片段间隔与实际长度不整除,实际用于分段的间隔应当按实际长度均分,
|
||||
// 以确保尾部音符排在最后一个中段之后并且到达时间等于谱面结束时间。
|
||||
float actualSegmentInterval = noteData.length / segmentCount; // 实际用于中段间隔
|
||||
if (segmentCount < 1) segmentCount = 1; // Documentation text normalized.
|
||||
float actualSegmentInterval = segmentCount > 0 ? noteData.length / segmentCount : segmentInterval;
|
||||
|
||||
Transform spawnPoint = spawnPoints[noteData.trackIndex];
|
||||
// scheduledEndTime 使用谱面时间(note.time + length),但转换为实时
|
||||
// Documentation text normalized.
|
||||
float rawScheduledEnd = startTime + (noteData.time + noteData.length) + globalHitDelay;
|
||||
float scheduledEndTime = Mathf.Max(0f, rawScheduledEnd); // clamp to non-negative
|
||||
|
||||
// 生成唯一 id(改为自增计数器,避免随机冲突)
|
||||
// Documentation text normalized.
|
||||
int holdNoteId = ++holdNoteIdCounter;
|
||||
|
||||
// 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)
|
||||
{
|
||||
Debug.LogError("对象池返回空 hold note start!");
|
||||
Debug.LogError("NotePool returned null hold start object.");
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -407,20 +398,18 @@ public class NoteSpawner : MonoBehaviour
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("长音符 start 部分缺少 HoldNote 组件!");
|
||||
Debug.LogError("Hold start object is missing HoldNote component.");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 生成 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 缩放,保证中段连续
|
||||
|
||||
float segmentDelay = i * actualSegmentInterval; // Documentation text normalized.
|
||||
GameObject segObj = notePool.GetHoldNoteSegment(noteData.color);
|
||||
if (segObj == null)
|
||||
{
|
||||
Debug.LogError("对象池返回空 hold note 片段!");
|
||||
Debug.LogError("NotePool returned null hold segment object.");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -430,7 +419,7 @@ public class NoteSpawner : MonoBehaviour
|
||||
HoldNote holdSeg = segObj.GetComponent<HoldNote>();
|
||||
if (holdSeg != null)
|
||||
{
|
||||
// 中段都标记为 middle,pass realtime base time and segmentDelay
|
||||
// Documentation text normalized.
|
||||
holdSeg.Setup(holdNoteId, noteData.trackIndex, noteSpeed, baseHit, segmentDelay, false, scheduledEndTime, noteData.color, key, "middle", judgeConfig, noteData);
|
||||
|
||||
// apply same visual scale so middle pieces visually connect
|
||||
@@ -445,11 +434,10 @@ public class NoteSpawner : MonoBehaviour
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("长音符片段缺少 HoldNote 组件!");
|
||||
Debug.LogError("Hold segment object is missing HoldNote component.");
|
||||
}
|
||||
}
|
||||
|
||||
// 始终在中段之后生成一个明确的 End 段(尾部音符)
|
||||
GameObject endObj = notePool.GetHoldNoteEndSegment(noteData.color);
|
||||
if (endObj == null)
|
||||
{
|
||||
@@ -457,8 +445,7 @@ public class NoteSpawner : MonoBehaviour
|
||||
return -1;
|
||||
}
|
||||
|
||||
float endDelay = segmentCount * actualSegmentInterval; // 不按 speedMultiplier 缩放,使用谱面长度保证尾段紧随中段
|
||||
|
||||
float endDelay = segmentCount * actualSegmentInterval; // Documentation text normalized.
|
||||
endObj.transform.position = spawnPoint.position;
|
||||
endObj.transform.rotation = Quaternion.identity;
|
||||
|
||||
@@ -479,19 +466,14 @@ public class NoteSpawner : MonoBehaviour
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("长音符 end 部分缺少 HoldNote 组件!");
|
||||
Debug.LogError("Hold end object is missing HoldNote component.");
|
||||
}
|
||||
|
||||
return holdNoteId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 立刻结算:
|
||||
/// - 停止继续生成音符(停止 SpawnNotes 协程)
|
||||
/// - 取消 NoteSpawner 自己的延迟结算协程(PostSpawnSettlementRoutine)
|
||||
/// - 立刻触发结算流程(JudgeManager.TriggerAllNotesJudged)
|
||||
/// 注意:该方法不会清理场上已生成的音符,仅停止后续生成并进入结算。
|
||||
/// </summary>
|
||||
/// Documentation text normalized.
|
||||
[ContextMenu("Force Immediate Settlement")]
|
||||
public void ForceImmediateSettlement()
|
||||
{
|
||||
@@ -595,8 +577,7 @@ public class NoteSpawner : MonoBehaviour
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验短音符的位置,防止其因计时偏差而位移
|
||||
/// </summary>
|
||||
/// Documentation text normalized.
|
||||
private IEnumerator CalibrateNoteAfterSpawn(NoteController noteController, Vector3 expectedSpawnPos)
|
||||
{
|
||||
if (noteController == null) yield break;
|
||||
@@ -642,129 +623,39 @@ public class NoteSpawner : MonoBehaviour
|
||||
if (JudgeManager.IsDebugEnabled) 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));
|
||||
settlementCoroutine = StartCoroutine(PostSpawnSettlementRoutine());
|
||||
}
|
||||
|
||||
private IEnumerator PostSpawnSettlementRoutine(int lookback)
|
||||
private IEnumerator PostSpawnSettlementRoutine()
|
||||
{
|
||||
// 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--)
|
||||
float chartEndTime = 0f;
|
||||
for (int i = 0; i < beatmap.notes.Length; i++)
|
||||
{
|
||||
NoteData note = beatmap.notes[i];
|
||||
if (note.time >= timeRangeStart)
|
||||
{
|
||||
relevantNotes.Add(note);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (note == null) continue;
|
||||
|
||||
float end = note.type == "hold" ? (note.time + note.length) : note.time;
|
||||
if (end > chartEndTime) chartEndTime = end;
|
||||
}
|
||||
chartEndTime += Mathf.Max(0f, globalHitDelay);
|
||||
|
||||
// Reverse to process in chronological order
|
||||
relevantNotes.Reverse();
|
||||
|
||||
if (JudgeManager.IsDebugEnabled) 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
|
||||
float maxEndTime = float.MinValue;
|
||||
NoteData maxEndNote = null;
|
||||
|
||||
foreach (var note in relevantNotes)
|
||||
{
|
||||
float noteEndTime;
|
||||
if (note.type == "hold")
|
||||
{
|
||||
noteEndTime = note.time + note.length;
|
||||
}
|
||||
else
|
||||
{
|
||||
noteEndTime = note.time;
|
||||
}
|
||||
|
||||
if (noteEndTime > maxEndTime)
|
||||
{
|
||||
maxEndTime = noteEndTime;
|
||||
maxEndNote = note;
|
||||
}
|
||||
|
||||
if (JudgeManager.IsDebugEnabled)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
const float postChartDelay = 2f;
|
||||
float settlementTime = startTime + chartEndTime + postChartDelay;
|
||||
|
||||
if (JudgeManager.IsDebugEnabled)
|
||||
{
|
||||
Debug.Log($"[NoteSpawner] Max end note: type={maxEndNote?.type}, time={maxEndNote?.time:F3}, maxIsHold={maxIsHold}, initialWait={initialWait:F3}");
|
||||
Debug.Log($"[NoteSpawner] Settlement scheduled at chartEnd+{postChartDelay:F1}s " +
|
||||
$"(chartEnd={chartEndTime:F3}, startTime={startTime:F3}, now={Time.time: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 (JudgeManager.IsDebugEnabled) 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 (JudgeManager.IsDebugEnabled) 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)
|
||||
while (Time.time < settlementTime)
|
||||
yield return null;
|
||||
|
||||
ForceClearInputState(null);
|
||||
|
||||
// Trigger settlement
|
||||
try
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user