备份,准备做双端
This commit is contained in:
@@ -42,8 +42,14 @@ public class HoldNote : BaseNote
|
||||
private string queueNoteId = null;
|
||||
private bool isQueued = false;
|
||||
|
||||
// Cached instance-id string used by NotePool to unregister without re-allocating.
|
||||
// May be null if this segment never registered a queue id (e.g. non-start segment).
|
||||
public string GetQueueNoteId() => queueNoteId;
|
||||
|
||||
public int id;
|
||||
public int trackIndex;
|
||||
// Reused by EvaluateHoldEnd's skill-trigger dispatch to avoid a per-hold-end List<int> allocation.
|
||||
private readonly List<int> _holdEndCandidatesBuffer = new List<int>(4);
|
||||
public float speed;
|
||||
public float startTime;
|
||||
public float delay;
|
||||
@@ -65,6 +71,40 @@ public class HoldNote : BaseNote
|
||||
// Input.GetKeyDown/Up directly (which touch cannot drive on Android).
|
||||
private bool prevTrackHeld = false;
|
||||
|
||||
// Latest press/release dspTime captured from the Input System (via InputManager events)
|
||||
// for this note's track. When the polled keyDown/keyUp edge fires this frame, we convert
|
||||
// the captured dspTime → song time for sub-frame-accurate timing instead of reading the
|
||||
// frame-quantized NowSongTime. Falls back to NowSongTime if no event fired (e.g. touch
|
||||
// paths that don't carry a distinct timestamp still work via the AudioSettings.dspTime default).
|
||||
private double lastPressDspTime = double.NaN;
|
||||
private double lastReleaseDspTime = double.NaN;
|
||||
|
||||
private void OnTrackPressedDsp(int track, double dspTime)
|
||||
{
|
||||
if (track == trackIndex) lastPressDspTime = dspTime;
|
||||
}
|
||||
|
||||
private void OnTrackReleasedDsp(int track, double dspTime)
|
||||
{
|
||||
if (track == trackIndex) lastReleaseDspTime = dspTime;
|
||||
}
|
||||
|
||||
// Convert the most-recent captured press dspTime to song time; fall back to NowSongTime.
|
||||
private float PressSongTime()
|
||||
{
|
||||
return double.IsNaN(lastPressDspTime)
|
||||
? GameplayClock.NowSongTime
|
||||
: GameplayClock.SongTimeFromDsp(lastPressDspTime);
|
||||
}
|
||||
|
||||
// Convert the most-recent captured release dspTime to song time; fall back to NowSongTime.
|
||||
private float ReleaseSongTime()
|
||||
{
|
||||
return double.IsNaN(lastReleaseDspTime)
|
||||
? GameplayClock.NowSongTime
|
||||
: GameplayClock.SongTimeFromDsp(lastReleaseDspTime);
|
||||
}
|
||||
|
||||
// Query the shared held-state table (keyboard OR touch). Falls back to legacy
|
||||
// Input.GetKey if the InputManager is somehow absent so editor/standalone still works.
|
||||
private bool IsHeld()
|
||||
@@ -96,6 +136,26 @@ public class HoldNote : BaseNote
|
||||
// track when the hold actually started (real-time) so we can compute held fraction
|
||||
private float holdStartTime = -1f;
|
||||
|
||||
// ==== 单条 Tiled 长音符 body(纯视觉,不参与任何判定/计分) ====
|
||||
// 原本中段由 N 个 middle 段密铺组成;本 body 用一条 Tiled sprite 覆盖 head→end 的整段,
|
||||
// 按绝对时间下落,底边在判定线处被"消耗"(drain),end 到线时回收。
|
||||
// 它绝不注册 JudgeManager/TrackKeyManager 状态,start/end 判定路径完全不受影响。
|
||||
// 抗失真关键:Tiled 会按 sprite 原生像素在"局部空间"平铺,再乘 transform 世界缩放;
|
||||
// prefab 里烘焙的非等比 Y 缩放(如 3.75)会把每块拉长导致失真。故 body 实例把渲染器
|
||||
// transform 的 Y 缩放归一为与 X 相同(等比),长度改由 SpriteRenderer.size.y 承载(世界单位)。
|
||||
private bool bodyMode = false;
|
||||
private bool bodyReturning = false;
|
||||
private float bodyTravelTime;
|
||||
private float bodyHeadActivationTime; // head 发车的绝对歌曲时间(= baseHit - travelTime)
|
||||
private float bodyLengthSeconds; // 长音符时长(秒)
|
||||
private float bodySpeed; // 世界速度(单位/秒)
|
||||
private float bodySpawnX, bodySpawnZ; // 车道原点(x,z 固定)
|
||||
private float bodySpawnY; // 发车高度(head 在 activation 时刻的 Y)
|
||||
private SpriteRenderer[] bodyRenderers;
|
||||
private SpriteDrawMode[] bodyRendererOrigDrawMode;
|
||||
private Vector2[] bodyRendererOrigSize;
|
||||
private Vector3[] bodyRendererOrigLocalScale;
|
||||
|
||||
// Cache allies per track to avoid GameObject.Find on first judgement.
|
||||
private static AllyCombatant[] allyCache;
|
||||
private static BeatmapManager beatmapManagerCache;
|
||||
@@ -335,6 +395,8 @@ public class HoldNote : BaseNote
|
||||
private void OnDisable()
|
||||
{
|
||||
prevTrackHeld = false;
|
||||
InputManager.OnTrackPressedWithDspTime -= OnTrackPressedDsp;
|
||||
InputManager.OnTrackReleasedWithDspTime -= OnTrackReleasedDsp;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
@@ -360,6 +422,8 @@ public class HoldNote : BaseNote
|
||||
private void OnEnable()
|
||||
{
|
||||
ResetState();
|
||||
InputManager.OnTrackPressedWithDspTime += OnTrackPressedDsp;
|
||||
InputManager.OnTrackReleasedWithDspTime += OnTrackReleasedDsp;
|
||||
}
|
||||
|
||||
private void PlayHitAnimation()
|
||||
@@ -459,7 +523,7 @@ public class HoldNote : BaseNote
|
||||
}
|
||||
else
|
||||
{
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"HoldNoteJudgeConfig: perfect={judgeConfig.perfectRange}, great={judgeConfig.greatRange}, good={judgeConfig.goodRange}, miss={judgeConfig.missRange}");
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"HoldNoteJudgeConfig: perfectE={judgeConfig.perfectEarlyRange} perfectL={judgeConfig.perfectLateRange}, greatE={judgeConfig.greatEarlyRange} greatL={judgeConfig.greatLateRange}, goodE={judgeConfig.goodEarlyRange} goodL={judgeConfig.goodLateRange}, miss={judgeConfig.missRange}");
|
||||
}
|
||||
|
||||
this.noteData = noteData;
|
||||
@@ -508,6 +572,13 @@ public class HoldNote : BaseNote
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// 纯视觉 body:只更新位置/尺寸(drain),不进入任何判定逻辑。
|
||||
if (bodyMode)
|
||||
{
|
||||
UpdateBodyVisual(GameplayClock.NowSongTime);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isJudged) return;
|
||||
|
||||
var jm = cachedJudgeManager;
|
||||
@@ -525,14 +596,25 @@ public class HoldNote : BaseNote
|
||||
bool keyUp = !keyHeld && prevTrackHeld;
|
||||
prevTrackHeld = keyHeld;
|
||||
|
||||
// Optimization: Early exit if the note is far from judgment line and hasn't entered yet
|
||||
if (!hasEnteredLine)
|
||||
{
|
||||
// For all segments, if they haven't triggered collision yet and are far away, skip input checks
|
||||
// We use travelTime as a safe buffer. Only check input if note is within 0.5s of hitTime.
|
||||
if (now < hitTime - 0.5f) return;
|
||||
}
|
||||
|
||||
// 池化泄漏兜底:不依赖 hasEnteredLine / 几何进入离开事件。
|
||||
// 正常情况下 middle/start/end 由 HandleZoneEnter/Exit(几何驱动)或第 556 行的
|
||||
// 正常情况下 middle/start/end 由 HandleZoneEnter/Exit(几何驱动)或下方的
|
||||
// in-zone auto-miss 负责回收;但当几何阈值未就绪、或帧尖峰导致音符 Y 未被采样到判定区内时,
|
||||
// 上述事件可能永不触发,音符会一直下落不回池,严重拖累性能(尤其 useMultiSegmentFill 使
|
||||
// 中段数量随速度倍增)。此兜底在"正常判定窗口 + backstopBuffer"之后才触发,严格晚于所有正常
|
||||
// 路径,因此对正常音符零行为改变——仅回收那些几何事件从未触发、否则永远泄漏的段。
|
||||
// 【性能】本 block 放在上方"远距离早退"之后:兜底触发要求 now > hitTime(或 scheduledEndTime)
|
||||
// + missRange + 0.5,而早退只在 now < hitTime - 0.5 时发生,两条件互斥,故早退帧本就不会命中
|
||||
// 任何兜底分支。移到早退后仅避免对数百个"离判定线尚远"的段每帧空算,行为与之前完全等效。
|
||||
{
|
||||
float missRangeBackstop = (judgeConfig?.missRange ?? 0.5f) * holdWindowMultiplier;
|
||||
float missRangeBackstop = (judgeConfig != null ? judgeConfig.missRange : 0.2f) * holdWindowMultiplier;
|
||||
const float backstopBuffer = 0.5f;
|
||||
bool startJudgedBs = jm != null && jm.IsStartJudged(noteID);
|
||||
|
||||
@@ -541,9 +623,9 @@ public class HoldNote : BaseNote
|
||||
if (scheduledEndTime > 0f && now > scheduledEndTime + missRangeBackstop + backstopBuffer)
|
||||
{
|
||||
// 兜底触发时音符早已越过判定线,几何仅是漏采样。置 hasEnteredLine 让 ReturnToPool
|
||||
// 的 Start 守卫(第 1519 行,拒收未入线 Start)不再阻挡回收。
|
||||
// 的 Start 守卫(拒收未入线 Start)不再阻挡回收。
|
||||
hasEnteredLine = true;
|
||||
// 已按住到尾部的 End 早已在第 677 行按 Perfect 自动完成并 return;
|
||||
// 已按住到尾部的 End 早已在下方按 Perfect 自动完成并 return;
|
||||
// 走到这里说明几何事件从未触发。按原 HandleZoneExit(End) 的收尾语义处理。
|
||||
bool releasedBs = jm != null && jm.HasNoteReleased(noteID);
|
||||
if (!startJudgedBs)
|
||||
@@ -562,10 +644,10 @@ public class HoldNote : BaseNote
|
||||
else if (hitTime > 0f && now > hitTime + missRangeBackstop + backstopBuffer)
|
||||
{
|
||||
// 兜底触发时音符早已越过判定线,几何仅是漏采样。置 hasEnteredLine 让 ReturnToPool
|
||||
// 的 Start 守卫(第 1519 行,拒收未入线 Start)不再阻挡回收。
|
||||
// 的 Start 守卫(拒收未入线 Start)不再阻挡回收。
|
||||
hasEnteredLine = true;
|
||||
// Start 未判定 → 走正常 Miss(与第 569 行一致);已判定的 start / middle → 静默回收
|
||||
// (与第 579-581 行、第 883 行一致,绝不 StopHoldParticles 杀掉长按持续特效)。
|
||||
// Start 未判定 → 走正常 Miss;已判定的 start / middle → 静默回收
|
||||
// (绝不 StopHoldParticles 杀掉长按持续特效)。
|
||||
if (segment == NoteSegment.Start && !startJudgedBs)
|
||||
{
|
||||
EvaluateHoldEnd(now, true);
|
||||
@@ -576,14 +658,6 @@ public class HoldNote : BaseNote
|
||||
}
|
||||
}
|
||||
|
||||
// Optimization: Early exit if the note is far from judgment line and hasn't entered yet
|
||||
if (!hasEnteredLine)
|
||||
{
|
||||
// For all segments, if they haven't triggered collision yet and are far away, skip input checks
|
||||
// We use travelTime as a safe buffer. Only check input if note is within 0.5s of hitTime.
|
||||
if (now < hitTime - 0.5f) return;
|
||||
}
|
||||
|
||||
if (GameConfig.autoPlayEnabled)
|
||||
{
|
||||
UpdateAutoplay(now, jm, tkm, debugEnabled);
|
||||
@@ -606,7 +680,7 @@ public class HoldNote : BaseNote
|
||||
// Auto-miss checks when inside judge line
|
||||
if (hasEnteredLine)
|
||||
{
|
||||
float missRangeScaled = (judgeConfig?.missRange ?? 0.5f) * holdWindowMultiplier;
|
||||
float missRangeScaled = (judgeConfig != null ? judgeConfig.missRange : 0.2f) * holdWindowMultiplier;
|
||||
// 只对"尚未判定"的 start 段做超时 auto-Miss。已成功判定的 start 段(玩家已按下、
|
||||
// 正持续按住)绝不能在此被误判 Miss——否则 EvaluateHoldEnd(forceMiss) 会调 StopHoldParticles,
|
||||
// 把长按持续特效杀掉。物理版时 collider 早已离开判定区不会走到这里;几何版 exit 更晚才暴露此隐患。
|
||||
@@ -653,7 +727,7 @@ public class HoldNote : BaseNote
|
||||
if (!hasReleased)
|
||||
{
|
||||
hasReleased = true;
|
||||
releaseTime = now;
|
||||
releaseTime = ReleaseSongTime();
|
||||
jm?.RegisterNoteReleased(noteID, true);
|
||||
if (debugEnabled) Debug.Log($"[HoldNote] RegisterNoteReleased on KeyUp. NoteID: {noteID}, releaseTime={releaseTime:F2}");
|
||||
|
||||
@@ -679,8 +753,10 @@ public class HoldNote : BaseNote
|
||||
if (debugEnabled)
|
||||
Debug.Log($"[HoldNote] KeyDown detected for {noteID} key={keyToPress} time={now:F3} hitTime={hitTime:F3} hasEnteredLine={hasEnteredLine}");
|
||||
|
||||
float pressTimeLocal = now;
|
||||
float maxWindow = (judgeConfig?.missRange ?? 0.5f) * holdWindowMultiplier;
|
||||
float pressTimeLocal = PressSongTime();
|
||||
float maxWindow = (judgeConfig != null
|
||||
? Mathf.Max(judgeConfig.goodEarlyRange, judgeConfig.goodLateRange)
|
||||
: 0.3f) * holdWindowMultiplier;
|
||||
|
||||
// Only consume the frame if this hold start is actually within the timing window.
|
||||
if (Mathf.Abs(pressTimeLocal - hitTime) > maxWindow)
|
||||
@@ -776,7 +852,9 @@ public class HoldNote : BaseNote
|
||||
}
|
||||
|
||||
float pressTimeLocal = hitTime; // force Perfect regardless of frame timing
|
||||
float maxWindow = (judgeConfig?.missRange ?? 0.5f) * holdWindowMultiplier;
|
||||
float maxWindow = (judgeConfig != null
|
||||
? Mathf.Max(judgeConfig.goodEarlyRange, judgeConfig.goodLateRange)
|
||||
: 0.3f) * holdWindowMultiplier;
|
||||
|
||||
if (tkm != null && !tkm.IsBestCandidate(trackIndex, queueNoteId, pressTimeLocal, maxWindow))
|
||||
{
|
||||
@@ -1053,17 +1131,22 @@ public class HoldNote : BaseNote
|
||||
return;
|
||||
}
|
||||
|
||||
float pressTime = GameplayClock.NowSongTime;
|
||||
float pressTime = PressSongTime();
|
||||
float rawOffsetMs = (hitTime - pressTime) * 1000f;
|
||||
float offset = Mathf.Abs(pressTime - hitTime);
|
||||
float offset = pressTime - hitTime; // positive = late, negative = early
|
||||
string result;
|
||||
|
||||
float visualScaleFactor = Mathf.Clamp(visualSpeedMultiplier, 0.5f, 2f);
|
||||
float pRange = (judgeConfig?.perfectRange ?? 0.1f) * holdWindowMultiplier * visualScaleFactor;
|
||||
float gRange = (judgeConfig?.greatRange ?? 0.2f) * holdWindowMultiplier * visualScaleFactor;
|
||||
float gdRange = (judgeConfig?.goodRange ?? 0.3f) * holdWindowMultiplier * visualScaleFactor;
|
||||
float windowScale = holdWindowMultiplier * visualScaleFactor;
|
||||
|
||||
if (offset <= pRange)
|
||||
float perfectEarly = (judgeConfig?.perfectEarlyRange ?? 0.1f) * windowScale;
|
||||
float perfectLate = (judgeConfig?.perfectLateRange ?? 0.1f) * windowScale;
|
||||
float greatEarly = (judgeConfig?.greatEarlyRange ?? 0.2f) * windowScale;
|
||||
float greatLate = (judgeConfig?.greatLateRange ?? 0.2f) * windowScale;
|
||||
float goodEarly = (judgeConfig?.goodEarlyRange ?? 0.3f) * windowScale;
|
||||
float goodLate = (judgeConfig?.goodLateRange ?? 0.3f) * windowScale;
|
||||
|
||||
if (offset >= -perfectEarly && offset <= perfectLate)
|
||||
{
|
||||
result = "Perfect";
|
||||
// Score statistics for Hold notes are now handled exclusively in EvaluateHoldEnd
|
||||
@@ -1082,7 +1165,7 @@ public class HoldNote : BaseNote
|
||||
// store into pool for use on end
|
||||
HoldNoteJudgePool.RegisterStart(noteID, pressTime, hitTime, scheduledEndTime, noteColor, trackIndex, (object)noteData);
|
||||
}
|
||||
else if (offset <= gRange)
|
||||
else if (offset >= -greatEarly && offset <= greatLate)
|
||||
{
|
||||
result = "Great";
|
||||
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
|
||||
@@ -1096,7 +1179,7 @@ public class HoldNote : BaseNote
|
||||
|
||||
HoldNoteJudgePool.RegisterStart(noteID, pressTime, hitTime, scheduledEndTime, noteColor, trackIndex, (object)noteData);
|
||||
}
|
||||
else if (offset <= gdRange)
|
||||
else if (offset >= -goodEarly && offset <= goodLate)
|
||||
{
|
||||
result = "Good";
|
||||
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
|
||||
@@ -1256,83 +1339,38 @@ public class HoldNote : BaseNote
|
||||
}
|
||||
else
|
||||
{
|
||||
// try to use pool record for accurate press duration
|
||||
// 松手时刻判定:以玩家松手时间(releaseTime)与预定结束时间(scheduledEndTime)的绝对时间差
|
||||
// 评级,与头段"按下时刻判定"完全对称,不再依赖按住时长占比或几何进出判定。
|
||||
// forcePerfect(按住到尾部自动完成)仍直接判 Perfect。
|
||||
float endScheduled = scheduledEndTime;
|
||||
if (HoldNoteJudgePool.TryGet(noteID, out var info))
|
||||
{
|
||||
if (JudgeManager.IsDebugEnabled) Debug.Log($"[HoldNote] Found pool info for {noteID}: pressTime={info.pressTime:F3} length={info.length:F3} scheduledEnd={info.scheduledEnd:F3}");
|
||||
float playerHeld = Mathf.Clamp(releaseTime - info.pressTime, 0f, info.length);
|
||||
float frac = info.length <= 0f ? 0f : (playerHeld / info.length);
|
||||
|
||||
if (forcePerfect || frac > 0.7f)
|
||||
{
|
||||
result = "Perfect";
|
||||
ScoreManager.Instance.countPerfect += 1;
|
||||
if (trackIndex >= 0 && trackIndex < 5) ScoreManager.Instance.trackPerfectCounts[trackIndex]++;
|
||||
}
|
||||
else if (frac > 0.4f)
|
||||
{
|
||||
result = "Great";
|
||||
ScoreManager.Instance.countGreat += 1;
|
||||
if (trackIndex >= 0 && trackIndex < 5) ScoreManager.Instance.trackGreatCounts[trackIndex]++;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = "Good";
|
||||
ScoreManager.Instance.countGood += 1;
|
||||
if (trackIndex >= 0 && trackIndex < 5) ScoreManager.Instance.trackGoodCounts[trackIndex]++;
|
||||
}
|
||||
|
||||
if (noteData != null && result != "Miss")
|
||||
{
|
||||
float offsetEnd = (info.scheduledEnd - releaseTime) * 1000f;
|
||||
noteData.judgeOffsetMsEnd = offsetEnd;
|
||||
|
||||
// Record the head offset (stored during HandleStart) as the representative timing for this note
|
||||
float headOffset = noteData.judgeOffsetMs;
|
||||
if (float.IsNaN(headOffset)) headOffset = 0;
|
||||
ScoreManager.Instance?.RecordOffset(headOffset);
|
||||
}
|
||||
|
||||
// cleanup pool
|
||||
endScheduled = info.scheduledEnd;
|
||||
HoldNoteJudgePool.Unregister(noteID);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (JudgeManager.IsDebugEnabled) Debug.LogWarning($"[HoldNote] No pool info for {noteID}, falling back to local held fraction calculation");
|
||||
// fallback logic if no pool info
|
||||
float holdRequired = Mathf.Max(0.0001f, scheduledEndTime - hitTime);
|
||||
float held = Mathf.Clamp(actualReleaseTime - hitTime, 0f, holdRequired);
|
||||
float frac = holdRequired <= 0f ? 0f : (held / holdRequired);
|
||||
if (JudgeManager.IsDebugEnabled) Debug.LogWarning($"[HoldNote] No pool info for {noteID}, using field scheduledEndTime for release timing");
|
||||
}
|
||||
|
||||
if (forcePerfect || frac > 0.7f)
|
||||
{
|
||||
result = "Perfect";
|
||||
ScoreManager.Instance.countPerfect += 1;
|
||||
if (trackIndex >= 0 && trackIndex < 5) ScoreManager.Instance.trackPerfectCounts[trackIndex]++;
|
||||
}
|
||||
else if (frac > 0.4f)
|
||||
{
|
||||
result = "Great";
|
||||
ScoreManager.Instance.countGreat += 1;
|
||||
if (trackIndex >= 0 && trackIndex < 5) ScoreManager.Instance.trackGreatCounts[trackIndex]++;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = "Good";
|
||||
ScoreManager.Instance.countGood += 1;
|
||||
if (trackIndex >= 0 && trackIndex < 5) ScoreManager.Instance.trackGoodCounts[trackIndex]++;
|
||||
}
|
||||
float endOffset = Mathf.Abs(releaseTime - endScheduled);
|
||||
result = GradeAndCountHoldEndByReleaseTiming(forcePerfect, endOffset);
|
||||
|
||||
float rawOffsetMsEnd = (scheduledEndTime - actualReleaseTime) * 1000f;
|
||||
if (noteData != null && result != "Miss")
|
||||
{
|
||||
noteData.judgeOffsetMsEnd = rawOffsetMsEnd;
|
||||
|
||||
// Fallback: use head offset if available, otherwise 0
|
||||
float headOffset = noteData.judgeOffsetMs;
|
||||
if (float.IsNaN(headOffset)) headOffset = 0;
|
||||
ScoreManager.Instance?.RecordOffset(headOffset);
|
||||
}
|
||||
if (result == "Miss")
|
||||
{
|
||||
// 松手时刻严重偏离预定结束点 → 记 Miss(与头段 Miss 语义一致)。
|
||||
if (JudgeManager.IsDebugEnabled) Debug.LogWarning($"[HoldNote] END judged as Miss by release timing: {noteColor} endOffset={endOffset:F3}");
|
||||
ScoreManager.Instance?.RecordOffset(0);
|
||||
ReleaseTrackJudgeLockSafe();
|
||||
}
|
||||
else if (noteData != null)
|
||||
{
|
||||
noteData.judgeOffsetMsEnd = (endScheduled - releaseTime) * 1000f;
|
||||
// 以头段偏移作为本音符的代表性时序记录(与原逻辑一致,保证每条长音符只记一次 offset)。
|
||||
float headOffset = noteData.judgeOffsetMs;
|
||||
if (float.IsNaN(headOffset)) headOffset = 0;
|
||||
ScoreManager.Instance?.RecordOffset(headOffset);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1412,7 +1450,8 @@ public class HoldNote : BaseNote
|
||||
bool triggeredHold = false;
|
||||
bool triggeredTap = false;
|
||||
|
||||
var candidates = new List<int>();
|
||||
var candidates = _holdEndCandidatesBuffer;
|
||||
candidates.Clear();
|
||||
candidates.Add(this.trackIndex);
|
||||
if (!candidates.Contains(this.TrackIndex)) candidates.Add(this.TrackIndex);
|
||||
var ui = teamUIController.Instance;
|
||||
@@ -1488,6 +1527,50 @@ public class HoldNote : BaseNote
|
||||
ScheduleReturnToPool(0.2f);
|
||||
}
|
||||
|
||||
// 按"松手时刻偏移"评级并累加对应计数,返回评级字符串。与头段 HandleStart 的判定窗口
|
||||
// 计算方式一致(perfect/great/good * holdWindowMultiplier * visualScaleFactor),超出 good 窗口判 Miss。
|
||||
// forcePerfect(按住到尾部自动完成)直接判 Perfect。
|
||||
private string GradeAndCountHoldEndByReleaseTiming(bool forcePerfect, float endOffset)
|
||||
{
|
||||
float visualScaleFactor = Mathf.Clamp(visualSpeedMultiplier, 0.5f, 2f);
|
||||
float windowScale = holdWindowMultiplier * visualScaleFactor;
|
||||
|
||||
float perfectEarly = (judgeConfig?.perfectEarlyRange ?? 0.1f) * windowScale;
|
||||
float perfectLate = (judgeConfig?.perfectLateRange ?? 0.1f) * windowScale;
|
||||
float greatEarly = (judgeConfig?.greatEarlyRange ?? 0.2f) * windowScale;
|
||||
float greatLate = (judgeConfig?.greatLateRange ?? 0.2f) * windowScale;
|
||||
float goodEarly = (judgeConfig?.goodEarlyRange ?? 0.3f) * windowScale;
|
||||
float goodLate = (judgeConfig?.goodLateRange ?? 0.3f) * windowScale;
|
||||
|
||||
// endOffset = releaseTime - scheduledEndTime, positive = late, negative = early
|
||||
string result;
|
||||
if (forcePerfect || (endOffset >= -perfectEarly && endOffset <= perfectLate))
|
||||
{
|
||||
result = "Perfect";
|
||||
ScoreManager.Instance.countPerfect += 1;
|
||||
if (trackIndex >= 0 && trackIndex < 5) ScoreManager.Instance.trackPerfectCounts[trackIndex]++;
|
||||
}
|
||||
else if (endOffset >= -greatEarly && endOffset <= greatLate)
|
||||
{
|
||||
result = "Great";
|
||||
ScoreManager.Instance.countGreat += 1;
|
||||
if (trackIndex >= 0 && trackIndex < 5) ScoreManager.Instance.trackGreatCounts[trackIndex]++;
|
||||
}
|
||||
else if (endOffset >= -goodEarly && endOffset <= goodLate)
|
||||
{
|
||||
result = "Good";
|
||||
ScoreManager.Instance.countGood += 1;
|
||||
if (trackIndex >= 0 && trackIndex < 5) ScoreManager.Instance.trackGoodCounts[trackIndex]++;
|
||||
}
|
||||
else
|
||||
{
|
||||
result = "Miss";
|
||||
ScoreManager.Instance.countMiss += 1;
|
||||
if (trackIndex >= 0 && trackIndex < 5) ScoreManager.Instance.trackMissCounts[trackIndex]++;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void ApplyJudgedMaterial()
|
||||
{
|
||||
if (JudgeManager.Instance == null || !JudgeManager.Instance.enableJudgedMaterial) return;
|
||||
@@ -1590,6 +1673,23 @@ public class HoldNote : BaseNote
|
||||
|
||||
public void ResetState()
|
||||
{
|
||||
// 纯视觉 body 回收:还原渲染器到 prefab 原始状态,退出 body 模式,不走判定相关重置。
|
||||
if (bodyMode || bodyReturning)
|
||||
{
|
||||
// body 与 head/end 共享 noteID,OnHoldAlphaSync 会让 body 也 ApplyJudgedMaterial
|
||||
// (与原中段行为一致)。回收时必须还原材质,否则换过的材质会残留到下次池化复用。
|
||||
RestoreOriginalMaterials();
|
||||
RestoreBodyRenderers();
|
||||
bodyMode = false;
|
||||
bodyReturning = false;
|
||||
segment = NoteSegment.None;
|
||||
noteColor = string.Empty;
|
||||
noteID = string.Empty;
|
||||
type = string.Empty;
|
||||
if (controller != null) controller.DisableVisualBodyMode();
|
||||
return;
|
||||
}
|
||||
|
||||
// also release any stale lock from previous lifecycle
|
||||
ReleaseTrackJudgeLockSafe();
|
||||
|
||||
@@ -1616,6 +1716,8 @@ public class HoldNote : BaseNote
|
||||
isHoldActive = false;
|
||||
hasBeenHeldFromStart = false;
|
||||
prevTrackHeld = false;
|
||||
lastPressDspTime = double.NaN;
|
||||
lastReleaseDspTime = double.NaN;
|
||||
// 去物理判定:池化复用时重置 controller 的几何判定状态(重新算阈值、清进入标志)。
|
||||
if (controller != null) controller.ResetJudgeZoneGeometry();
|
||||
|
||||
@@ -1660,6 +1762,139 @@ public class HoldNote : BaseNote
|
||||
}
|
||||
}
|
||||
|
||||
// 以纯视觉 body 身份初始化:一条 Tiled 长条覆盖 head→end 整段,按绝对时间下落,
|
||||
// end 越过判定线后回收(时机与原分段一致)。不注册任何判定/计分状态。
|
||||
// baseHit=head 到达判定线的绝对歌曲时间;lengthSeconds=长音符时长;speed=世界速度;
|
||||
// travelTime=从发车到判定线的时间;spawnPoint=车道原点。
|
||||
public void SetupBody(int holdNoteId, int trackIndexArg, float speed, float baseHit,
|
||||
float lengthSeconds, float travelTime, string color, Vector3 spawnPoint)
|
||||
{
|
||||
this.id = holdNoteId;
|
||||
this.noteID = holdNoteId.ToString();
|
||||
this.trackIndex = trackIndexArg;
|
||||
this.TrackIndex = trackIndexArg;
|
||||
this.color = color;
|
||||
this.noteColor = color;
|
||||
this.type = "body";
|
||||
this.segment = NoteSegment.None; // 不参与任何 segment 判定分支
|
||||
|
||||
bodyMode = true;
|
||||
bodyReturning = false;
|
||||
bodySpeed = Mathf.Max(0f, speed);
|
||||
bodyLengthSeconds = Mathf.Max(0f, lengthSeconds);
|
||||
bodyTravelTime = Mathf.Max(0f, travelTime);
|
||||
bodyHeadActivationTime = baseHit - bodyTravelTime;
|
||||
bodySpawnX = spawnPoint.x;
|
||||
bodySpawnZ = spawnPoint.z;
|
||||
bodySpawnY = spawnPoint.y;
|
||||
|
||||
if (controller == null) controller = GetComponent<HoldNoteController>();
|
||||
controller?.EnableVisualBodyMode();
|
||||
|
||||
CaptureBodyRenderers();
|
||||
UpdateBodyVisual(GameplayClock.NowSongTime);
|
||||
}
|
||||
|
||||
// 缓存 body 上的所有 SpriteRenderer 并切到 Tiled。
|
||||
// 不改 transform 缩放(保留 prefab 里 outer/inner 各自的构图比例),长度全部交给 size.y:
|
||||
// 世界高度 = size.y × lossyScale.y ⇒ 设 size.y = 目标世界高度 / lossyScale.y。
|
||||
// 相比原来的 Simple+整体拉伸,Tiled 会沿 Y 重复贴图而非把单张越拉越长,消除拉伸失真。
|
||||
private void CaptureBodyRenderers()
|
||||
{
|
||||
if (bodyRenderers == null)
|
||||
{
|
||||
bodyRenderers = GetComponentsInChildren<SpriteRenderer>(true);
|
||||
int n = bodyRenderers.Length;
|
||||
bodyRendererOrigDrawMode = new SpriteDrawMode[n];
|
||||
bodyRendererOrigSize = new Vector2[n];
|
||||
bodyRendererOrigLocalScale = new Vector3[n];
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
var sr = bodyRenderers[i];
|
||||
if (sr == null) continue;
|
||||
bodyRendererOrigDrawMode[i] = sr.drawMode;
|
||||
bodyRendererOrigSize[i] = sr.size;
|
||||
bodyRendererOrigLocalScale[i] = sr.transform.localScale;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < bodyRenderers.Length; i++)
|
||||
{
|
||||
var sr = bodyRenderers[i];
|
||||
if (sr == null) continue;
|
||||
sr.drawMode = SpriteDrawMode.Tiled;
|
||||
sr.tileMode = SpriteTileMode.Continuous;
|
||||
// 不改 transform 缩放(保留 outer/inner 各自构图比例)。宽度取一块 tile 宽,
|
||||
// 使世界宽度 = sprite 原生宽 × lossyScale.x,与原 Simple 模式一致,仅水平不平铺。
|
||||
if (sr.sprite != null)
|
||||
{
|
||||
var sz = sr.size;
|
||||
sz.x = sr.sprite.bounds.size.x;
|
||||
sr.size = sz;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 逐帧驱动 body 的位置(纯视觉,不触判定)。
|
||||
// 复刻原 N 个 middle 段的整体行为:一条定高长条(全高 = speed×length)随 head 绝对时间下落、
|
||||
// 穿过判定线继续下移,直到 end 越过判定线后(≈ scheduledEnd)再回收——与原分段"经过判定线
|
||||
// 后 ~0.2s 回收"的滚动/消失时机一致,不做额外的底部裁剪(避免改变既有视觉行为)。
|
||||
private void UpdateBodyVisual(float t)
|
||||
{
|
||||
if (!bodyMode) return;
|
||||
|
||||
float scheduledEnd = bodyHeadActivationTime + bodyTravelTime + bodyLengthSeconds;
|
||||
if (t > scheduledEnd + 0.2f) { ReturnBodyToPool(); return; }
|
||||
|
||||
float elapsed = t - bodyHeadActivationTime;
|
||||
float headY = bodySpawnY - bodySpeed * elapsed; // head(底端)世界 Y
|
||||
float fullHeight = bodySpeed * bodyLengthSeconds; // 定高(= end 高出 head 的量)
|
||||
float centerY = headY + fullHeight * 0.5f;
|
||||
|
||||
transform.position = new Vector3(bodySpawnX, centerY, bodySpawnZ);
|
||||
ApplyBodyHeight(fullHeight);
|
||||
}
|
||||
|
||||
// 世界高度 → 每个渲染器的 size.y(= 目标世界高度 / 该渲染器世界 Y 缩放)。
|
||||
private void ApplyBodyHeight(float worldHeight)
|
||||
{
|
||||
if (bodyRenderers == null) return;
|
||||
for (int i = 0; i < bodyRenderers.Length; i++)
|
||||
{
|
||||
var sr = bodyRenderers[i];
|
||||
if (sr == null) continue;
|
||||
float ly = Mathf.Abs(sr.transform.lossyScale.y);
|
||||
if (ly < 1e-4f) ly = 1f;
|
||||
var sz = sr.size;
|
||||
sz.y = Mathf.Max(0f, worldHeight / ly);
|
||||
sr.size = sz;
|
||||
}
|
||||
}
|
||||
|
||||
private void ReturnBodyToPool()
|
||||
{
|
||||
if (bodyReturning) return;
|
||||
bodyReturning = true;
|
||||
if (!gameObject.activeSelf) return;
|
||||
gameObject.SetActive(false);
|
||||
if (NotePool.Instance != null)
|
||||
NotePool.Instance.ReturnHoldNoteSegment(gameObject, noteColor);
|
||||
}
|
||||
|
||||
// 池化复用/回收时把 body 渲染器还原到 prefab 原始状态。
|
||||
private void RestoreBodyRenderers()
|
||||
{
|
||||
if (bodyRenderers == null) return;
|
||||
for (int i = 0; i < bodyRenderers.Length; i++)
|
||||
{
|
||||
var sr = bodyRenderers[i];
|
||||
if (sr == null) continue;
|
||||
sr.drawMode = bodyRendererOrigDrawMode[i];
|
||||
sr.size = bodyRendererOrigSize[i];
|
||||
sr.transform.localScale = bodyRendererOrigLocalScale[i];
|
||||
}
|
||||
}
|
||||
|
||||
public void ApplyVisualScale(float scaleY)
|
||||
{
|
||||
float s = Mathf.Clamp(scaleY, 0.1f, 4f);
|
||||
|
||||
Reference in New Issue
Block a user