Files
bansonic_beta_main/Assets/scripts/gamePlay_gameplay/HoldNote.cs
T

452 lines
19 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<HoldNoteController>();
anim = GetComponent<AnimationController>();
}
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 = falseEnd 段在 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.05f) // 更严格的完美判定窗口
{
result = "Perfect";
}
else if (diff < 0.15f) // 适当放宽的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] OnDisableEnd段在离开判定区时玩家仍未松手,判定为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();
}
}