using System.Collections; using UnityEngine; public enum NoteSegment { None, Start, Middle, End } [RequireComponent(typeof(HoldNoteController))] public class HoldNote : BaseNote { private KeyCode keyToPress = KeyCode.None; private string noteColor = string.Empty; private string noteID = string.Empty; private NoteSegment segment = NoteSegment.None; private float hitTime = 0f; private float releaseTime = 0f; private bool hasReleased = true; private bool hasEnteredLine = false; private Coroutine autoReturnCoroutine; private HoldNoteController controller; private AnimationController anim; public int id; public int trackIndex; public float speed; public float startTime; public float delay; public bool isEnd; public float scheduledEndTime; public string color; public KeyCode key; public string type; // "start", "middle", or "end" private bool isJudged = false; // 分段判定标记,一旦判定(无论成功失败)就设为true,避免重复判定 private bool isHoldActive = false; // 长按有效标记:Start段成功判定且按键未松开 // 新增变量 private bool hasBeenHeldFromStart = false; // 进入判定区时是否已按住 (用于辅助判断,但不再直接阻断判定) private void Awake() { controller = GetComponent(); anim = GetComponent(); } private void OnEnable() { ResetState(); } private void PlayHitAnimation() { if (anim != null) { // 激活动画效果 GameObject anim.gameObject.SetActive(true); anim.PlayDestroyAnimation(noteColor); // 延迟禁用动画效果 GameObject,确保动画播放完毕 StartCoroutine(DelayedDisableAnimation(anim.gameObject, 0.5f)); // 0.5秒后禁用 } } private IEnumerator DelayedDisableAnimation(GameObject animationObject, float delay) { yield return new WaitForSeconds(delay); animationObject.SetActive(false); } public void Setup(int id, int trackIndex, float speed, float time, float delay, bool isEnd, float scheduledEnd, string color, KeyCode key, string type) { this.id = id; this.trackIndex = trackIndex; this.speed = speed; this.startTime = time; this.delay = delay; this.isEnd = isEnd; this.scheduledEndTime = scheduledEnd; this.color = color; this.key = key; this.type = type; this.noteID = id.ToString(); this.keyToPress = key; this.noteColor = color; this.hitTime = time + delay; this.segment = (delay == 0f) ? NoteSegment.Start : (isEnd ? NoteSegment.End : NoteSegment.Middle); this.hasEnteredLine = false; this.hasReleased = false; // 在 Setup 时注册初始状态 JudgeManager.Instance?.RegisterNoteReleased(noteID, false); if (segment == NoteSegment.Start) JudgeManager.Instance?.RegisterStartJudged(noteID, false); // 初始为未判定 if (segment == NoteSegment.End) JudgeManager.Instance?.RegisterScheduledEndTime(noteID, scheduledEndTime); controller?.SetSpeed(speed); controller?.SetSegmentDelay(delay); } private void Update() { // 如果当前片段已经判定过,直接返回 if (isJudged) return; // **通用长按状态更新:** // 只有当isHoldActive为true时才检查松开操作,避免误判 // 且仅在GetKeyUp时更新状态,避免重复操作 if (isHoldActive && Input.GetKeyUp(keyToPress)) { isHoldActive = false; Debug.Log($"[HoldNote] 按键 {keyToPress} 松开,长按状态失效。NoteID: {noteID}, Segment: {segment}, Type: {type}"); // 如果是中间段或结束段,且因为松手导致长按失效,可以立即回收 if (segment == NoteSegment.Middle || segment == NoteSegment.End) { // 注意:End段的判定在HandleEnd里,松手只是让isHoldActive为false,不代表End段判定已完成 // 如果 End 段的判定窗口还没过,松手后 isHoldActive = false,End 段在 OnTriggerExit2D 才会回收 // 或者在 HandleEnd 之前松手导致长按失效,HandleEnd 内部会判定 Miss } } // 如果按键被按下,且Start段已经判定成功,则激活长按状态 // 考虑到玩家可能在Start段通过后瞬间松手再按下,这里需要确保只要Start段判定成功且按键按住,isHoldActive就为true if (JudgeManager.Instance.IsStartJudged(noteID) && Input.GetKey(keyToPress)) { isHoldActive = true; } // Start段判定 if (segment == NoteSegment.Start && hasEnteredLine) { // 在判定窗口内按下按键才触发判定 if (Input.GetKeyDown(keyToPress)) { HandleStart(); isJudged = true; // 无论成功失败都标记为已判定,避免重复尝试 } // 如果Start段进入判定区但玩家未按键,且已经错过最佳判定时机,可以考虑Miss并回收 // 这部分逻辑现在放在 OnTriggerExit2D 里处理更合适 } // Middle段判定: // 只有当头部已判定、且长按有效(按键持续按住),并且已经到达该Middle段的命中时间点时,才认为该Middle段通过。 // Middle段本身没有独立的按键输入判定,它只是长按的延续。 else if (segment == NoteSegment.Middle && JudgeManager.Instance.IsStartJudged(noteID) // 头部必须已判定 && isHoldActive // 必须处于长按有效状态(按键持续按住) && !JudgeManager.Instance.HasNoteReleased(noteID)) // 确保End段还未被判定释放 { // Debug.Log($"[HoldNote] Middle段状态检查: Time.time={Time.time:F2}, hitTime={hitTime:F2}, hasEnteredLine={hasEnteredLine}"); if (Time.time >= hitTime) { // 确保它进入了判定线,但在Update中,即使没完全进入,只要时间到了且条件满足,也算通过 // 但是,我们主要希望它在通过判定线后被回收 // 因此,Middle段的回收逻辑更多依赖于DelayedAutoReturnCheck或OnTriggerExit2D if (hasEnteredLine) // 确保已经进入过判定线,防止音符提前消失 { Debug.Log($"[HoldNote] Middle段通过 (持续长按): {noteColor}"); PlayHitAnimation(); ReturnToPool(); isJudged = true; } } } // End段判定 else if (segment == NoteSegment.End && JudgeManager.Instance.IsStartJudged(noteID) // 头部必须已判定 && !JudgeManager.Instance.HasNoteReleased(noteID)) // 确保还未被判定释放 { // 只有当玩家松开按键时才触发End段判定 if (Input.GetKeyUp(keyToPress)) { HandleEnd(); isJudged = true; // 标记为已判定 } // 如果 End 段已经达到或超过了预定的释放时间 (scheduledEndTime), // 但玩家仍未松手,且长按状态仍然有效,这可能是玩家按得过长 // 这种情况应该给予一个差的判定或者Miss else if (Time.time >= scheduledEndTime + 0.1f && isHoldActive) // 给予一点容错时间 { Debug.Log($"[HoldNote] End段超时未松手,判定为Miss: {noteColor}"); HandleEnd(true); // 传入true表示强制Miss isJudged = true; } } } private void OnTriggerEnter2D(Collider2D collision) { if (!collision.CompareTag("JudgmentLine")) return; hasEnteredLine = true; // 只要进入判定线,就标记 if (segment == NoteSegment.Middle) { // 进入判定区时记录是否已按住 hasBeenHeldFromStart = Input.GetKey(keyToPress); Debug.Log($"[HoldNote] Middle段进入判定线: color={noteColor}, isStartJudged={JudgeManager.Instance.IsStartJudged(noteID)}, hasReleased={JudgeManager.Instance.HasNoteReleased(noteID)}, isKeyHeld={Input.GetKey(keyToPress)}, hasBeenHeldFromStart={hasBeenHeldFromStart}"); if (autoReturnCoroutine != null) StopCoroutine(autoReturnCoroutine); // Middle段的延迟回收主要确保即使按住也能在适当时间回收 // 重新思考这里的必要性,可能让Update和OnTriggerExit2D处理更直接 // 暂时移除这里的 DelayedAutoReturnCheck,让 Update 和 OnTriggerExit2D 主导 Middle 段的生命周期 // autoReturnCoroutine = StartCoroutine(DelayedAutoReturnCheck()); } else if (segment == NoteSegment.End) { if (autoReturnCoroutine != null) StopCoroutine(autoReturnCoroutine); // End段的延迟回收主要用于处理玩家是否松手,以及超时回收 autoReturnCoroutine = StartCoroutine(DelayedAutoReturnCheck()); } } private void OnTriggerExit2D(Collider2D collision) { if (!collision.CompareTag("JudgmentLine")) return; // Start段离开判定线时如果未被判定,则视为Miss并回收 if (segment == NoteSegment.Start) { if (!JudgeManager.Instance.IsStartJudged(noteID)) { Debug.Log($"[HoldNote] Start段离开判定线未判定,强制回收 (Miss): {noteColor}"); // 可以触发一个Miss判定 // JudgeManager.Instance.RecordMiss(noteID); ReturnToPool(); } // 如果Start段已判定,那么它会通过DelayedReturn被回收,这里不做额外处理 } // Middle段离开判定线时,如果头部未判定,或者长按已经失效(松手),则回收 else if (segment == NoteSegment.Middle) // 这里修正了拼写错误 { // 无论Start段是否判定,如果Middle段离开了判定线,就应该回收 // 因为它已经完成了它的“通过”或者“错过”的职责 Debug.Log($"[HoldNote] Middle段离开判定线强制回收: {noteColor}"); ReturnToPool(); } // End段离开判定线时,如果头部未判定,或者长按已经失效,或者 End 段已经完成判定,则回收 else if (segment == NoteSegment.End) { if (!isJudged) // 如果End段还没有被判定 { // 如果头部没判,或者长按中断,或者玩家按得过久(已经过了scheduledEndTime很多了) if (!JudgeManager.Instance.IsStartJudged(noteID) || !isHoldActive || Time.time > scheduledEndTime + 0.15f) { Debug.Log($"[HoldNote] End段离开判定线强制回收 (头部未判定或长按中断或超时): {noteColor}"); // 可以在这里强制一个Miss判定,如果之前没触发过 if (!JudgeManager.Instance.HasNoteReleased(noteID)) // 确保没有重复判定 { HandleEnd(true); // 传入true表示强制Miss } ReturnToPool(); } } else // 如果End段已经被判定过了,直接回收 { ReturnToPool(); } } } private IEnumerator DelayedAutoReturnCheck() { // 这里的 DelayedAutoReturnCheck 主要是针对 End 段的超时未松手检查 // 或者 Start 段的延迟补判(这部分已经移到 Update 和 HandleStart) yield return null; if (segment == NoteSegment.End) { // 在这里等待,看玩家是否在 scheduledEndTime 之前松手 // 如果到达 scheduledEndTime 玩家仍未松手,且长按有效,则继续等待 // 如果玩家按得过长,可以在 Update 中触发 Miss 判定 float timeToWait = Mathf.Max(0f, scheduledEndTime - Time.time); if (timeToWait > 0f) { yield return new WaitForSeconds(timeToWait); } // 到达 scheduledEndTime 后再次检查 // 如果此时玩家仍在按住,并且 Start 段已判定,但 End 段还没被判定释放 // 说明玩家按得太久,判定为 Miss if (!isJudged && JudgeManager.Instance.IsStartJudged(noteID) && Input.GetKey(keyToPress)) { Debug.Log($"[HoldNote] DelayedAutoReturnCheck (End段) 判定:玩家按得过长,Miss! {noteColor}"); HandleEnd(true); // 强制 Miss } // 无论如何,如果 End 段已经过去了,但还没回收,就回收 if (!gameObject.activeSelf) { yield break; // 修正!使用 yield break 来结束协程 } ReturnToPool(); } } private void HandleStart() { float pressTime = Time.time; float offset = Mathf.Abs(pressTime - hitTime); if (offset < 0.1f) // 判定成功 { Debug.Log($"[HoldNote] START判定成功(偏差={offset:F2}秒): {noteColor}"); JudgeManager.Instance.RegisterStartJudged(noteID, true); isHoldActive = true; // 成功判定Start,长按状态激活 PlayHitAnimation(); } else { Debug.Log($"[HoldNote] START Miss(偏差={offset:F2}秒): {noteColor}"); JudgeManager.Instance.RegisterStartJudged(noteID, false); // 明确标记未判定 isHoldActive = false; // Start Miss,长按状态不激活 } // Start段判定后立即回收,因为它的视觉部分可能只需要短暂显示 StartCoroutine(DelayedReturn()); } private IEnumerator DelayedReturn() { yield return new WaitForSeconds(0.05f); // 短暂延迟,确保视觉效果或数据更新 if (gameObject.activeSelf) // 确保对象还活跃,防止重复回收 { ReturnToPool(); } } // 重载 HandleEnd 方法,增加一个强制Miss的参数 private void HandleEnd(bool forceMiss = false) { releaseTime = Time.time; hasReleased = true; // 标记此音符已被释放 JudgeManager.Instance.RegisterNoteReleased(noteID, true); // 在 JudgeManager 中注册释放 string result; // 如果强制Miss,或者头部未判定,或者长按已经中断,则直接判定为Miss if (forceMiss || !JudgeManager.Instance.IsStartJudged(noteID) || !isHoldActive) { result = "Miss"; Debug.LogWarning($"[HoldNote] END判定失败({noteColor}):{(forceMiss ? "强制Miss" : (!JudgeManager.Instance.IsStartJudged(noteID) ? "头部未判定" : "长按中断"))}"); } else { float diff = Mathf.Abs(releaseTime - scheduledEndTime); // End段的判定规则 if (diff <= 0.1f) // 更严格的完美判定窗口 { result = "Perfect"; } else if (diff < 0.2f) // 适当放宽的Great判定 { result = "Great"; } else if (diff < 0.3f) // 再次放宽的Good判定 { result = "Good"; } else { result = "Bad"; // 更大的偏差可能视为Bad或Miss } } Debug.Log($"[HoldNote] END判定结果: {noteColor} {result} (release={releaseTime:F2}, target={scheduledEndTime:F2})"); ReturnToPool(); // 判定后回收 } private void OnDisable() { if (autoReturnCoroutine != null) { StopCoroutine(autoReturnCoroutine); autoReturnCoroutine = null; } // OnDisable时对End段的补偿处理: // 如果是End段,且尚未被判定,且Start段已判定,但此时按键已经松开 (通过 !Input.GetKey(keyToPress) 判断) // 这意味着玩家在离开判定区或因其他原因导致 OnDisable 时松手,但未触发正常的 HandleEnd if (segment == NoteSegment.End && !isJudged && // 尚未判定 JudgeManager.Instance.IsStartJudged(noteID)) // 头部已判定 { // 如果按键已经松开,或者长按状态已经失效 (可能因为松开过快) if (!Input.GetKey(keyToPress) || !isHoldActive) { Debug.Log($"[HoldNote] OnDisable补偿End段Miss判定: {noteColor}"); HandleEnd(true); // 强制Miss isJudged = true; // 标记已判定 } else if (Input.GetKey(keyToPress)) { // 如果End段离开了判定区,但是玩家还在按住,这属于按得过长 Debug.Log($"[HoldNote] OnDisable:End段在离开判定区时玩家仍未松手,判定为Miss: {noteColor}"); HandleEnd(true); // 强制Miss isJudged = true; } } controller?.StopMovement(); } private void ReturnToPool() { if (!gameObject.activeSelf) return; // 再次检查以防止对象已被禁用 if (segment == NoteSegment.Start && !hasEnteredLine) { Debug.LogWarning($"[HoldNote] 阻止回收尚未进入判定线的 Start段: {noteColor}"); return; } gameObject.SetActive(false); if (segment == NoteSegment.Start) NotePool.Instance.ReturnStartNote(gameObject, noteColor); else NotePool.Instance.ReturnHoldNoteSegment(gameObject, noteColor); } public void ResetState() { hasReleased = false; segment = NoteSegment.None; keyToPress = KeyCode.None; noteColor = string.Empty; noteID = string.Empty; hitTime = 0f; scheduledEndTime = 0f; hasEnteredLine = false; isJudged = false; isHoldActive = false; // 重置长按标记 hasBeenHeldFromStart = false; // 重置 // canJudgeThisSegment = false; // 此变量已被移除或不再关键 if (autoReturnCoroutine != null) { StopCoroutine(autoReturnCoroutine); autoReturnCoroutine = null; } controller?.StopMovement(); } }