备份,准备做双端
This commit is contained in:
@@ -23,6 +23,13 @@ public class NoteSpawner : MonoBehaviour
|
||||
public float spawnOffset = 0f; // Documentation text normalized.
|
||||
public float static_value_add_to_spawnoffset = 0f;
|
||||
|
||||
[Header("Visual Spawn Lead")]
|
||||
[Tooltip("Seconds to spawn notes earlier for visual readability only. Positive values make notes appear sooner without changing hit timing or judgement.")]
|
||||
public float visualPreSpawnTime = 0f;
|
||||
// Inspector 设定的视觉提前量基准;玩家 PlayerPrefs 视觉偏移在此之上叠加。
|
||||
private float visualPreSpawnTimeBase = 0f;
|
||||
private bool visualPreSpawnBaseCaptured = false;
|
||||
|
||||
[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;
|
||||
@@ -35,7 +42,7 @@ public class NoteSpawner : MonoBehaviour
|
||||
|
||||
// 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(1f, 3f)]
|
||||
[Range(1f, 3.25f)]
|
||||
public float speedMultiplier = 2f;
|
||||
|
||||
[Header("Calibration")]
|
||||
@@ -84,9 +91,9 @@ public class NoteSpawner : MonoBehaviour
|
||||
public GameObject animations;
|
||||
|
||||
// optional: constants for runtime clamping (kept for internal use)
|
||||
// Player-facing speed is clamped to 1-3; FlowSpeedGlobalMultiplier makes the real range 2-6.
|
||||
// Player-facing speed is clamped to 1-3.25; FlowSpeedGlobalMultiplier makes the real range 2-6.5.
|
||||
private const float SpeedMultiplierMin = 1f;
|
||||
private const float SpeedMultiplierMax = 3f;
|
||||
private const float SpeedMultiplierMax = 3.25f;
|
||||
// 全局流速系数:与用户可调流速(speedMultiplier)相乘的独立系数,加快音符下落。
|
||||
// 为什么用它而非 bpm 倍率:noteSpeed = 10.75 * sm * bpm / 240,若靠 ×bpm 提速,
|
||||
// 长条音符的视觉长度由 ApplyVisualScale(sm) 决定(只随 sm 不随 bpm)、且分段数 segmentInterval=(60/bpm)/4
|
||||
@@ -199,14 +206,39 @@ public class NoteSpawner : MonoBehaviour
|
||||
TrySubscribeJudgeManager();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
// 计算最终 spawnOffset = 玩家校准量(PlayerPrefs) + 项目级定数(Inspector)。
|
||||
// 必须在生成协程启动前调用;Start 与 LoadBeatmap 都调,保证不受生命周期顺序影响。
|
||||
private void ApplySpawnOffsetFromPrefs()
|
||||
{
|
||||
// 读取 PlayerPrefs 中的延迟偏移值(秒)并应用
|
||||
const string DELAY_PREFS_KEY = "UserGlobalDelaySeconds";
|
||||
float savedDelay = PlayerPrefs.GetFloat(DELAY_PREFS_KEY, 0f);
|
||||
spawnOffset = savedDelay + static_value_add_to_spawnoffset;
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[NoteSpawner] Applied spawnOffset={spawnOffset} (Saved={savedDelay} + Static={static_value_add_to_spawnoffset})");
|
||||
|
||||
ApplyVisualOffsetFromPrefs();
|
||||
}
|
||||
|
||||
// 纯视觉偏移:仅影响音符出现时机(chartSpawnTime),不改变判定时间(chartHitTime),
|
||||
// 因此对业务逻辑绝对等效。玩家值叠加在 Inspector 基准之上。
|
||||
private void ApplyVisualOffsetFromPrefs()
|
||||
{
|
||||
if (!visualPreSpawnBaseCaptured)
|
||||
{
|
||||
visualPreSpawnTimeBase = visualPreSpawnTime;
|
||||
visualPreSpawnBaseCaptured = true;
|
||||
}
|
||||
|
||||
const string VISUAL_OFFSET_PREFS_KEY = "UserVisualOffsetSeconds";
|
||||
float savedVisual = PlayerPrefs.GetFloat(VISUAL_OFFSET_PREFS_KEY, 0f);
|
||||
visualPreSpawnTime = visualPreSpawnTimeBase + savedVisual;
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[NoteSpawner] Applied visualPreSpawnTime={visualPreSpawnTime} (Base={visualPreSpawnTimeBase} + Saved={savedVisual})");
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// 读取 PlayerPrefs 中的延迟偏移值(秒)并应用
|
||||
ApplySpawnOffsetFromPrefs();
|
||||
|
||||
// 读取 PlayerPrefs 中的同步音符开关状态
|
||||
enableSyncNotePrefab = PlayerPrefs.GetInt("EnableSyncNotePrefab", 1) == 1;
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[NoteSpawner] Applied enableSyncNotePrefab={enableSyncNotePrefab}");
|
||||
@@ -289,6 +321,9 @@ public class NoteSpawner : MonoBehaviour
|
||||
}
|
||||
noteIndexToHoldId.Clear();
|
||||
|
||||
// 谱面修饰符:每次加载谱面时重建映射表(Random 会固定一次洗牌结果)
|
||||
ChartModifiers.InvalidateMapping();
|
||||
|
||||
// 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;
|
||||
@@ -302,6 +337,11 @@ public class NoteSpawner : MonoBehaviour
|
||||
startTime = GameplayClock.ChartStartSongTime;
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"歌曲开始时间: {startTime}");
|
||||
|
||||
// 关键:LoadBeatmap 可能早于本组件的 Start() 执行(GameManager 在启动流程里直接调本方法),
|
||||
// 若只在 Start 里算 spawnOffset,生成协程会读到默认值 0,导致 Inspector 定数/玩家校准完全失效。
|
||||
// 故在启动生成协程前再算一次,保证不受生命周期顺序影响。
|
||||
ApplySpawnOffsetFromPrefs();
|
||||
|
||||
// keep reference so we can stop spawning when doing immediate settlement
|
||||
spawnCoroutine = StartCoroutine(SpawnNotes());
|
||||
}
|
||||
@@ -360,7 +400,11 @@ public class NoteSpawner : MonoBehaviour
|
||||
|
||||
float travelTime = GetLaneTravelTimeSeconds(laneTravelTimes, note.trackIndex);
|
||||
float spawnTime = note.time - travelTime;
|
||||
float chartSpawnTime = startTime + spawnTime + spawnOffset;
|
||||
// globalHitDelay 是输入/音画延迟补偿:它已进入判定时刻(chartHitTime/baseHit/scheduledEndTime),
|
||||
// 因而音符 activationTime = hitTime - travelTime 也被整体前/后移。生成时刻必须同样加上它,
|
||||
// 否则负补偿(音符提前到线)时 activationTime 提前、生成没提前,音符会一出生就弹到半路。
|
||||
// 加上后 chartSpawnTime 恒等于 activationTime(再减去 visualPreSpawnTime 提前量),各取值下都从出生点自然下落。
|
||||
float chartSpawnTime = startTime + spawnTime + spawnOffset + globalHitDelay - Mathf.Max(0f, visualPreSpawnTime);
|
||||
float delay = chartSpawnTime - GameplayClock.NowSongTime;
|
||||
|
||||
if (delay > 0)
|
||||
@@ -390,6 +434,39 @@ public class NoteSpawner : MonoBehaviour
|
||||
AllNotesSpawned?.Invoke();
|
||||
}
|
||||
|
||||
// Caches the pooled note's sync-note child so we can toggle it on/off per spawn instead of
|
||||
// Destroy+Instantiate (plus a child-enumerator alloc) on every note. The child is created
|
||||
// lazily the first time a note actually needs it and reused for the note's whole lifetime.
|
||||
private sealed class SyncNoteChildRef : MonoBehaviour
|
||||
{
|
||||
public GameObject child;
|
||||
}
|
||||
|
||||
private void ApplySyncNoteChild(GameObject note, bool isSync)
|
||||
{
|
||||
if (note == null || !enableSyncNotePrefab || syncNotePrefab == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var refComp = note.GetComponent<SyncNoteChildRef>();
|
||||
if (refComp == null)
|
||||
{
|
||||
refComp = note.AddComponent<SyncNoteChildRef>();
|
||||
}
|
||||
|
||||
// Create the sync child once (only if this note has ever needed it).
|
||||
if (refComp.child == null && isSync)
|
||||
{
|
||||
refComp.child = Instantiate(syncNotePrefab, note.transform);
|
||||
}
|
||||
|
||||
if (refComp.child != null && refComp.child.activeSelf != isSync)
|
||||
{
|
||||
refComp.child.SetActive(isSync);
|
||||
}
|
||||
}
|
||||
|
||||
private KeyCode GetCachedKeyCode(string color)
|
||||
{
|
||||
if (string.IsNullOrEmpty(color)) return KeyCode.None;
|
||||
@@ -409,6 +486,9 @@ public class NoteSpawner : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
// 应用谱面修饰符(Mirror/Random):重映射 trackIndex
|
||||
int remappedTrackIndex = ChartModifiers.RemapTrackIndex(noteData.trackIndex);
|
||||
|
||||
KeyCode key = GetCachedKeyCode(noteData.color);
|
||||
if (key == KeyCode.None)
|
||||
{
|
||||
@@ -423,27 +503,16 @@ public class NoteSpawner : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
if (enableSyncNotePrefab && syncNotePrefab != null)
|
||||
{
|
||||
// Clean up any existing sync prefab from previous use in pool
|
||||
foreach (Transform child in note.transform)
|
||||
{
|
||||
if (child.name.StartsWith(syncNotePrefab.name))
|
||||
{
|
||||
Destroy(child.gameObject);
|
||||
}
|
||||
}
|
||||
ApplySyncNoteChild(note, isSync);
|
||||
|
||||
if (isSync)
|
||||
{
|
||||
Instantiate(syncNotePrefab, note.transform);
|
||||
}
|
||||
}
|
||||
Transform spawnPoint = spawnPoints[remappedTrackIndex];
|
||||
|
||||
Transform spawnPoint = spawnPoints[noteData.trackIndex];
|
||||
|
||||
// Calculate hit time (realtime when note should be judged)
|
||||
float chartHitTime = Mathf.Max(0f, startTime + noteData.time + globalHitDelay);
|
||||
// spawnOffset = 玩家在设置里校准的全局延迟量(PlayerPrefs UserGlobalDelaySeconds),
|
||||
// 必须与生成时机(chartSpawnTime)一致地进入 hitTime,否则判定窗口不随校准移动、
|
||||
// 且 activation=hitTime-travelTime 与生成时机错位导致音符出生即弹跳。
|
||||
// 与谱面 globalDelaySeconds(作用于音频播放)彼此独立,不重复偏移。
|
||||
float chartHitTime = Mathf.Max(0f, startTime + noteData.time + spawnOffset + globalHitDelay);
|
||||
|
||||
Vector3 initialPosition = spawnPoint.position;
|
||||
note.transform.position = initialPosition;
|
||||
@@ -451,11 +520,11 @@ public class NoteSpawner : MonoBehaviour
|
||||
|
||||
Note noteScript = note.GetComponent<Note>();
|
||||
NoteController noteController = note.GetComponent<NoteController>();
|
||||
|
||||
|
||||
if (noteScript != null)
|
||||
{
|
||||
// Setup note script with timing parameters
|
||||
noteScript.Setup(key, noteData.trackIndex, noteSpeed, chartHitTime, noteData.color, judgeConfig, noteData, isSync);
|
||||
// Setup note script with timing parameters — 传入重映射后的 trackIndex
|
||||
noteScript.Setup(key, remappedTrackIndex, noteSpeed, chartHitTime, noteData.color, judgeConfig, noteData, isSync);
|
||||
|
||||
// Configure controller for absolute positioning (replaces relative Translate)
|
||||
if (noteController != null)
|
||||
@@ -480,6 +549,9 @@ public class NoteSpawner : MonoBehaviour
|
||||
return -1;
|
||||
}
|
||||
|
||||
// 应用谱面修饰符(Mirror/Random):重映射 trackIndex
|
||||
int remappedTrackIndex = ChartModifiers.RemapTrackIndex(noteData.trackIndex);
|
||||
|
||||
KeyCode key = GetCachedKeyCode(noteData.color);
|
||||
if (key == KeyCode.None)
|
||||
{
|
||||
@@ -512,15 +584,15 @@ public class NoteSpawner : MonoBehaviour
|
||||
// 新逻辑下不拉伸(每段保持 prefab 原始大小);旧逻辑下按流速 sm 拉伸每段。
|
||||
float holdVisualScale = useMultiSegmentFill ? 1f : sm;
|
||||
|
||||
Transform spawnPoint = spawnPoints[noteData.trackIndex];
|
||||
Transform spawnPoint = spawnPoints[remappedTrackIndex];
|
||||
// Documentation text normalized.
|
||||
float scheduledEndTime = Mathf.Max(0f, startTime + (noteData.time + noteData.length) + globalHitDelay);
|
||||
float scheduledEndTime = Mathf.Max(0f, startTime + (noteData.time + noteData.length) + spawnOffset + globalHitDelay);
|
||||
|
||||
// Documentation text normalized.
|
||||
int holdNoteId = ++holdNoteIdCounter;
|
||||
|
||||
// base realtime for hits
|
||||
float baseHit = Mathf.Max(0f, startTime + noteData.time + globalHitDelay);
|
||||
float baseHit = Mathf.Max(0f, startTime + noteData.time + spawnOffset + globalHitDelay);
|
||||
|
||||
GameObject startObj = notePool.GetStartNote(noteData.color);
|
||||
if (startObj == null)
|
||||
@@ -529,22 +601,7 @@ public class NoteSpawner : MonoBehaviour
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (enableSyncNotePrefab && syncNotePrefab != null)
|
||||
{
|
||||
// Clean up any existing sync prefab from previous use in pool
|
||||
foreach (Transform child in startObj.transform)
|
||||
{
|
||||
if (child.name.StartsWith(syncNotePrefab.name))
|
||||
{
|
||||
Destroy(child.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
if (isSync)
|
||||
{
|
||||
Instantiate(syncNotePrefab, startObj.transform);
|
||||
}
|
||||
}
|
||||
ApplySyncNoteChild(startObj, isSync);
|
||||
|
||||
// Hold segments use absolute positioning; spawn at the lane origin.
|
||||
startObj.transform.position = spawnPoint.position;
|
||||
@@ -553,8 +610,8 @@ public class NoteSpawner : MonoBehaviour
|
||||
HoldNote holdNote = startObj.GetComponent<HoldNote>();
|
||||
if (holdNote != null)
|
||||
{
|
||||
// pass realtime hit time (startTime + note.time) and delay 0, include globalHitDelay
|
||||
holdNote.Setup(holdNoteId, noteData.trackIndex, noteSpeed, baseHit, 0f, false, scheduledEndTime, noteData.color, key, "start", travelTime, judgeConfig, noteData, isSync);
|
||||
// pass realtime hit time (startTime + note.time) and delay 0, include globalHitDelay — 使用重映射后的 trackIndex
|
||||
holdNote.Setup(holdNoteId, remappedTrackIndex, noteSpeed, baseHit, 0f, false, scheduledEndTime, noteData.color, key, "start", travelTime, judgeConfig, noteData, isSync);
|
||||
|
||||
// 旧逻辑=按流速 sm 拉伸;新逻辑(多段填充)=不拉伸(holdVisualScale=1)。
|
||||
float visualScale = holdVisualScale;
|
||||
@@ -568,39 +625,31 @@ public class NoteSpawner : MonoBehaviour
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (int i = 1; i < segmentCount; i++)
|
||||
// 单条 Tiled body 取代原来的 N 个 middle 段密铺:一条 Tiled 长条覆盖 head→end 整段,
|
||||
// 按绝对时间下落,end 越过判定线后回收。它纯视觉、不参与判定/计分,故 segmentCount /
|
||||
// actualSegmentInterval 只再用于 end 段延迟(下方 endDelay),中段判定业务逻辑完全不变。
|
||||
{
|
||||
// keep segment delays based on actualSegmentInterval (unscaled) so middle pieces are consecutive regardless of visual speed
|
||||
float segmentDelay = i * actualSegmentInterval; // Documentation text normalized.
|
||||
GameObject segObj = notePool.GetHoldNoteSegment(noteData.color);
|
||||
if (segObj == null)
|
||||
GameObject bodyObj = notePool.GetHoldNoteSegment(noteData.color);
|
||||
if (bodyObj == null)
|
||||
{
|
||||
Debug.LogError("NotePool returned null hold segment object.");
|
||||
continue;
|
||||
}
|
||||
|
||||
segObj.transform.position = spawnPoint.position;
|
||||
segObj.transform.rotation = Quaternion.identity;
|
||||
|
||||
HoldNote holdSeg = segObj.GetComponent<HoldNote>();
|
||||
if (holdSeg != null)
|
||||
{
|
||||
// Documentation text normalized.
|
||||
holdSeg.Setup(holdNoteId, noteData.trackIndex, noteSpeed, baseHit, segmentDelay, false, scheduledEndTime, noteData.color, key, "middle", travelTime, judgeConfig, noteData, false);
|
||||
|
||||
// 与开始段一致:旧逻辑拉伸、新逻辑不拉伸(靠更多段密铺填满)。
|
||||
float visualScaleMid = holdVisualScale;
|
||||
holdSeg.ApplyVisualScale(visualScaleMid);
|
||||
holdSeg.visualSpeedMultiplier = sm;
|
||||
|
||||
// Immediately calibrate position and schedule additional checks to correct any offset
|
||||
Vector3 calibSpawnPos = spawnPoint.position;
|
||||
holdSeg.CalibratePosition(calibSpawnPos, calibrateTolerance);
|
||||
EnqueueHoldCalibration(holdSeg, calibSpawnPos);
|
||||
Debug.LogError("NotePool returned null hold body object.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("Hold segment object is missing HoldNote component.");
|
||||
bodyObj.transform.position = spawnPoint.position;
|
||||
bodyObj.transform.rotation = Quaternion.identity;
|
||||
|
||||
HoldNote holdBody = bodyObj.GetComponent<HoldNote>();
|
||||
if (holdBody != null)
|
||||
{
|
||||
holdBody.visualSpeedMultiplier = sm;
|
||||
holdBody.SetupBody(holdNoteId, remappedTrackIndex, noteSpeed, baseHit,
|
||||
noteData.length, travelTime, noteData.color, spawnPoint.position);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError("Hold body object is missing HoldNote component.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -618,7 +667,7 @@ public class NoteSpawner : MonoBehaviour
|
||||
HoldNote holdEnd = endObj.GetComponent<HoldNote>();
|
||||
if (holdEnd != null)
|
||||
{
|
||||
holdEnd.Setup(holdNoteId, noteData.trackIndex, noteSpeed, baseHit, endDelay, true, scheduledEndTime, noteData.color, key, "end", travelTime, judgeConfig, noteData, false);
|
||||
holdEnd.Setup(holdNoteId, remappedTrackIndex, noteSpeed, baseHit, endDelay, true, scheduledEndTime, noteData.color, key, "end", travelTime, judgeConfig, noteData, false);
|
||||
|
||||
// 与开始段一致:旧逻辑拉伸、新逻辑不拉伸。
|
||||
float visualScaleEnd = holdVisualScale;
|
||||
@@ -913,7 +962,9 @@ public class NoteSpawner : MonoBehaviour
|
||||
float end = note.type == "hold" ? (note.time + note.length) : note.time;
|
||||
if (end > chartEndTime) chartEndTime = end;
|
||||
}
|
||||
chartEndTime += Mathf.Max(0f, globalHitDelay);
|
||||
// 正向校准/延迟会把音符整体后移,结算需相应顺延以免最后一颗音符尚在判定就触发结算;
|
||||
// 负向偏移不提前结算(钳 0),避免切掉仍在场的音符。postChartDelay 另有 2s 缓冲兜底。
|
||||
chartEndTime += Mathf.Max(0f, spawnOffset + globalHitDelay);
|
||||
|
||||
const float postChartDelay = 2f;
|
||||
float settlementTime = startTime + chartEndTime + postChartDelay;
|
||||
|
||||
Reference in New Issue
Block a user