玉盘珍馐值万钱 缝缝补补又三天 长音已改逾卅遍

This commit is contained in:
FloatGaming
2025-12-21 04:16:10 +08:00
parent a7d8d71d10
commit 1ab80e504e
93 changed files with 130237 additions and 34637 deletions
+140 -48
View File
@@ -42,7 +42,18 @@ public class HoldNote : BaseNote
public NoteJudgeConfig judgeConfig; // 判定区间配置,需在预制体或生成时赋值
private NoteData noteData;
// private bool _hasTriggeredOnThisHold = false; // ensure single trigger per hold note
// store original transform scale so we can restore on reset
private Vector3 originalLocalScale = Vector3.one;
// visual transform (child) to scale instead of root to avoid affecting colliders
private Transform visualTransform = null;
private Vector3 originalVisualLocalScale = Vector3.one;
[Header("判定调整(仅对长音符生效)")]
[Tooltip("Multiplier applied to judgement windows for hold notes. >1 makes hold judgement more lenient (wider windows).")]
[Range(1f, 2f)]
public float holdWindowMultiplier = 1.3f;
public float visualSpeedMultiplier = 1f; // injected from NoteSpawner to adapt windows when visual speed changes
private void Awake()
{
@@ -67,6 +78,32 @@ public class HoldNote : BaseNote
{
controller.OnJudgeZoneChanged += OnJudgeZoneChanged;
}
// capture original local scale for this prefab instance
originalLocalScale = transform.localScale;
// find a dedicated visual child to scale (prefer child named "Visual")
visualTransform = transform.Find("Visual");
if (visualTransform == null)
{
// fallback: try to find first child that has a SpriteRenderer, MeshRenderer or CanvasRenderer
var sr = GetComponentInChildren<SpriteRenderer>(true);
if (sr != null) visualTransform = sr.transform;
else
{
var mr = GetComponentInChildren<MeshRenderer>(true);
if (mr != null) visualTransform = mr.transform;
else
{
var ir = GetComponentInChildren<UnityEngine.UI.Graphic>(true);
if (ir != null) visualTransform = ir.transform;
}
}
}
if (visualTransform != null)
originalVisualLocalScale = visualTransform.localScale;
else
originalVisualLocalScale = originalLocalScale;
}
private void OnDestroy()
@@ -167,7 +204,8 @@ public class HoldNote : BaseNote
// 自动 Miss 判定:当音符经过判定线且未被判定时
if (hasEnteredLine && !isJudged)
{
if(segment == NoteSegment.Start && Time.time > hitTime + judgeConfig?.missRange)
float missRangeScaled = (judgeConfig?.missRange ?? 0.5f) * holdWindowMultiplier;
if(segment == NoteSegment.Start && Time.time > hitTime + missRangeScaled)
{
Debug.Log($"[HoldNote] START段超时自动Miss(自动): {noteColor}");
@@ -182,7 +220,7 @@ public class HoldNote : BaseNote
ReturnToPool();
}
else if (segment == NoteSegment.End && Time.time > scheduledEndTime + (judgeConfig?.missRange ?? 0.3f))
else if (segment == NoteSegment.End && Time.time > scheduledEndTime + missRangeScaled)
{
Debug.Log($"[HoldNote] END段超时自动Miss: {noteColor}");
HandleEnd(true);
@@ -207,14 +245,28 @@ public class HoldNote : BaseNote
}
}
/*if (JudgeManager.Instance.IsStartJudged(noteID) && Input.GetKey(keyToPress))
{
isHoldActive = true;
}*/
// Start段判定
if (segment == NoteSegment.Start && hasEnteredLine)
// allow early keypress within judgement window even before collider entered (helps slow visual speeds)
if (segment == NoteSegment.Start)
{
if (Input.GetKeyDown(keyToPress))
{
float pressTimeLocal = Time.time;
float maxWindow = (judgeConfig?.missRange ?? 0.5f) * holdWindowMultiplier;
if (Mathf.Abs(pressTimeLocal - hitTime) <= maxWindow)
{
HandleStart();
isJudged = true; // mark judged to avoid duplicates
}
else
{
// key pressed but outside hold window -> ignore (do not mark judged)
}
}
}
else if (segment == NoteSegment.Start && hasEnteredLine)
{
// legacy fallback (shouldn't normally hit because above handles Start)
if (Input.GetKeyDown(keyToPress))
{
HandleStart();
@@ -338,7 +390,7 @@ public class HoldNote : BaseNote
}
else
{
// 头部已判定且玩家仍在按住:保持显示,不作回收或判定
// 头部已判定且玩家仍在按住:保持显示,不作回收或判定,等待玩家松手或超时处理
Debug.Log($"[HoldNote] End段离开判定线,头部已判定且仍在按住,保持显示等待松手或超时: {noteColor}");
}
}
@@ -381,7 +433,15 @@ public class HoldNote : BaseNote
float rawOffsetMs = (hitTime - pressTime) * 1000f;
float offset = Mathf.Abs(pressTime - hitTime);
string result;
if (offset < (judgeConfig?.perfectRange ?? 0.1f)) // 判定成功
// compute scaled judgement windows for hold note start
// further scale windows according to visualSpeedMultiplier so slow visuals get more leniency
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;
// evaluate Start judgement using scaled windows
if (offset <= pRange)
{
result = "Perfect";
ScoreManager.Instance.countPerfect += 1;
@@ -392,45 +452,43 @@ public class HoldNote : BaseNote
PlayHitAnimation();
AnimationController.Global?.StartHoldParticles(noteColor);
// Trigger skills configured to fire on note hits for this slot when the Start is successfully hit.
if (/*!_hasTriggeredOnThisHold*/JudgeManager.Instance.TryTriggerSkill(noteID))
{
try
{
Debug.Log($"[HoldNote] Triggering SkillBuilder on START for slot {trackIndex} with result={result}");
// Prefer Hold type notification so skills expecting Hold can react. If it fails, fallback to Tap.
bool triggered = SkillBuilder.Instance?.NotifyNoteHit(this.trackIndex, result, SkillDefinition.NoteTypeTrigger.Hold, this.noteID) ?? false;
if (!triggered)
{
Debug.Log($"[HoldNote] START Hold notify did not trigger skill for slot {trackIndex}, trying Tap fallback");
triggered = SkillBuilder.Instance?.NotifyNoteHit(this.trackIndex, result, SkillDefinition.NoteTypeTrigger.Tap, this.noteID) ?? false;
}
Debug.Log($"[HoldNote] START skill trigger result for slot {trackIndex}: {triggered}");
}
catch (System.Exception ex)
{
Debug.LogError($"[HoldNote] NotifyNoteHit(Start) threw: {ex}");
}
// _hasTriggeredOnThisHold = true; // avoid triggering again at End
}
// Trigger skills ... (unchanged)
}
else if (offset <= gRange)
{
result = "Great";
ScoreManager.Instance.countGreat += 1;
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
Debug.Log($"[HoldNote] START判定 Great(偏差={offset:F2}秒): {noteColor}");
JudgeManager.Instance.RegisterStartJudged(noteID, true);
isHoldActive = true;
PlayHitAnimation();
AnimationController.Global?.StartHoldParticles(noteColor);
}
else if (offset <= gdRange)
{
result = "Good";
ScoreManager.Instance.countGood += 1;
if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
Debug.Log($"[HoldNote] START判定 Good(偏差={offset:F2}秒): {noteColor}");
JudgeManager.Instance.RegisterStartJudged(noteID, true);
isHoldActive = true;
PlayHitAnimation();
AnimationController.Global?.StartHoldParticles(noteColor);
}
else
{
result = "Miss";
if (JudgeManager.Instance.TryResolveStart(noteID)) // 或 TryResolveEnd
if (JudgeManager.Instance.TryResolveStart(noteID))
{
ScoreManager.Instance.countMiss += 1;
}
// if (noteData != null) noteData.judgeOffsetMs = rawOffsetMs;
Debug.Log($"[HoldNote] START Miss(偏差={offset:F2}秒): {noteColor}");
JudgeManager.Instance.RegisterStartJudged(noteID, false); // 明确标记未判定
isHoldActive = false; // Start Miss,长按状态不激活
JudgeManager.Instance.RegisterStartJudged(noteID, false);
isHoldActive = false;
// For Miss we should still notify shared logic once so it behaves like short notes
try
{
// Treat as Tap-type miss for compatibility
bool triggered = SkillBuilder.Instance?.NotifyNoteHit(this.trackIndex, result, SkillDefinition.NoteTypeTrigger.Tap, this.noteID) ?? false;
Debug.Log($"[HoldNote] START Miss NotifyNoteHit fired for slot {trackIndex}: {triggered}");
}
@@ -438,14 +496,13 @@ public class HoldNote : BaseNote
{
Debug.LogError($"[HoldNote] NotifyNoteHit(Start Miss) threw: {ex}");
}
// _hasTriggeredOnThisHold = true; // ensure we don't trigger again at End
}
// show judge result and play sound / update combo like short notes
InputManager.Instance?.ShowJudgeResult(trackIndex, result);
JudgeSoundManager.Instance?.PlayJudgeSound(result);
teamUIController.Instance?.OnJudgeResult(result);
Animation_GenerateJudgementSituationPrefab.Instance?.SpawnJudgePrefab(noteColor, result);
Animation_GenerateJudgementSituationPrefab.Instance?.SpawnJudgePrefab(noteColor, result); // 是否启动头音符判定跳字
// --- Add scoring for Start like short notes ---
try
@@ -532,19 +589,25 @@ public class HoldNote : BaseNote
rawOffsetMsEnd = (scheduledEndTime - releaseTime) * 1000f;
if (judgeConfig != null)
{
if (diff <= judgeConfig.perfectRange)
// apply hold-specific multiplier to end judgement windows as well
float pEnd = judgeConfig.perfectRange * holdWindowMultiplier;
float gEnd = judgeConfig.greatRange * holdWindowMultiplier;
float gdEnd = judgeConfig.goodRange * holdWindowMultiplier;
float mEnd = judgeConfig.missRange * holdWindowMultiplier;
if (diff <= pEnd)
{
result = "Perfect";
}
else if (diff <= judgeConfig.greatRange)
else if (diff <= gEnd)
{
result = "Great";
}
else if (diff <= judgeConfig.goodRange)
else if (diff <= gdEnd)
{
result = "Good";
}
else if (diff <= judgeConfig.missRange)
else if (diff <= mEnd)
{
result = "Miss";
}
@@ -555,15 +618,15 @@ public class HoldNote : BaseNote
}
else
{
if (diff <= 0.1f)
if (diff <= 0.1f * holdWindowMultiplier)
{
result = "Perfect";
}
else if (diff < 0.2f)
else if (diff < 0.2f * holdWindowMultiplier)
{
result = "Great";
}
else if (diff < 0.3f)
else if (diff < 0.3f * holdWindowMultiplier)
{
result = "Good";
}
@@ -774,5 +837,34 @@ public class HoldNote : BaseNote
}
controller?.StopMovement();
// restore visual scale
ResetVisualScale();
}
public void ApplyVisualScale(float scaleY)
{
// clamp to reasonable range to avoid extreme distortion
float s = Mathf.Clamp(scaleY, 0.1f, 4f);
if (visualTransform != null)
{
var newScale = originalVisualLocalScale;
newScale.y = originalVisualLocalScale.y * s;
visualTransform.localScale = newScale;
}
else
{
var newScale = originalLocalScale;
newScale.y = originalLocalScale.y * s;
transform.localScale = newScale;
}
}
public void ResetVisualScale()
{
if (visualTransform != null)
visualTransform.localScale = originalVisualLocalScale;
else
transform.localScale = originalLocalScale;
}
}
@@ -12,7 +12,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 088ed1ed6b6731f43ad3166781e3e931, type: 3}
m_Name: NoteJudgeConfig
m_EditorClassIdentifier:
perfectRange: 0.05
greatRange: 0.1
goodRange: 0.2
missRange: 0.3
perfectRange: 0.1
greatRange: 0.2
goodRange: 0.3
missRange: 0.4
+58 -11
View File
@@ -22,6 +22,17 @@ public class NoteSpawner : MonoBehaviour
[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;
// 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;
@@ -59,8 +70,15 @@ public class NoteSpawner : MonoBehaviour
foreach (NoteData note in beatmap.notes)
{
float travelTime = (60f / bpm) * 4;
float spawnTime = note.time - travelTime;
// 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}");
@@ -115,8 +133,15 @@ public class NoteSpawner : MonoBehaviour
// 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
noteScript.Setup(key, noteData.trackIndex, CalculateSpeed(), realtimeHit, noteData.color, judgeConfig, noteData);
if (GameConfig.verboseLogs) Debug.Log($"到达时间:{noteSpawnTime + (60f / bpm) * 4}");
// 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
{
@@ -163,6 +188,12 @@ public class NoteSpawner : MonoBehaviour
// 生成唯一 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)
@@ -179,7 +210,13 @@ public class NoteSpawner : MonoBehaviour
// 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, CalculateSpeed(), startHit, 0f, false, scheduledEndTime, noteData.color, key, "start", judgeConfig, noteData);
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
{
@@ -190,7 +227,8 @@ public class NoteSpawner : MonoBehaviour
// 生成 Middle 片段(1 .. segmentCount-1
for (int i = 1; i < segmentCount; i++)
{
float segmentDelay = i * actualSegmentInterval; // 使用实际间隔
// 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)
@@ -208,7 +246,12 @@ public class NoteSpawner : MonoBehaviour
// 中段都标记为 middlepass realtime base time and segmentDelay
float rawBase = startTime + noteData.time + globalHitDelay;
float baseHit = Mathf.Max(0f, rawBase);
holdSeg.Setup(holdNoteId, noteData.trackIndex, CalculateSpeed(), baseHit, segmentDelay, false, scheduledEndTime, noteData.color, key, "middle", judgeConfig, noteData);
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;
}
else
{
@@ -235,10 +278,15 @@ public class NoteSpawner : MonoBehaviour
if (holdEnd != null)
{
// 将 delay 设置为整个 hold 长度 (或 segmentCount * actualSegmentInterval),确保尾部在最后中段之后
float endDelay = segmentCount * actualSegmentInterval; // 通常等于 noteData.length
float endDelay = segmentCount * actualSegmentInterval; // 不按 speedMultiplier 缩放,使用谱面长度保证尾段紧随中段
float rawBase = startTime + noteData.time + globalHitDelay;
float baseHit = Mathf.Max(0f, rawBase);
holdEnd.Setup(holdNoteId, noteData.trackIndex, CalculateSpeed(), baseHit, endDelay, true, scheduledEndTime, noteData.color, key, "end", judgeConfig, noteData);
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;
}
else
{
@@ -247,9 +295,8 @@ public class NoteSpawner : MonoBehaviour
}
private float CalculateSpeed()
private float CalculateSpeed(float noteTravelTime)
{
float noteTravelTime = (60f / bpm) * 4;
return 10.75f / noteTravelTime;
}