using UnityEngine; using System; public class HoldNoteController : MonoBehaviour { private bool isInsideJudgeZone = false; public event Action OnJudgeZoneChanged; // 去物理判定:几何驱动的进入/离开回调(替代 HoldNote 自身的 OnTriggerEnter2D/Exit2D)。 public event Action OnZoneEnterGeometry; public event Action OnZoneExitGeometry; private bool isMoving = false; private float speed = 0f; private float activationTime; // 去物理判定:用几何阈值复现 Trigger 进入/离开时刻。 private Collider2D _col; private float _enterY, _exitY; private bool _thresholdsReady; // 单条 Tiled 长音符 body 模式:本 controller 不再自行移动/几何判定, // 位置与尺寸完全由 HoldNote.UpdateBodyVisual 逐帧驱动(纯视觉,不参与判定)。 private bool visualBodyMode = false; // Absolute positioning for hold segments (keeps segments connected under frame spikes) private bool useAbsolutePositioning = false; private Vector3 spawnPosition; private float baseSpawnYOffset = 0f; // can be negative to offset upward for delayed segments private float visualOffset = 0f; // additional Y offset for visual compensation private void Awake() { _col = GetComponent(); // 去物理判定:关闭 Rigidbody2D 模拟,消除每帧移动刚体的物理开销(判定改几何驱动)。 var rb = GetComponent(); if (rb != null) rb.simulated = false; } private void OnEnable() { NoteRuntimeTickManager.Register(this); } private void OnDisable() { NoteRuntimeTickManager.Unregister(this); } public void ManualTick(float songTime, float deltaTime) { // body 模式:位置/尺寸由 HoldNote 驱动,controller 不移动、不做几何判定(纯视觉)。 if (visualBodyMode) return; if (useAbsolutePositioning) { if (isMoving) ApplyAbsolutePosition(songTime); } else { // Start moving after activation time in legacy mode if (!isMoving && songTime >= activationTime) { isMoving = true; } if (isMoving) { transform.Translate(Vector3.down * speed * deltaTime); } } // 去物理判定:用几何阈值复现进入/离开判定区(替代 OnTriggerEnter2D/Exit2D)。 UpdateJudgeZoneByGeometry(); } private void UpdateJudgeZoneByGeometry() { if (!_thresholdsReady) { if (!JudgeZoneGeometry.LineReady) return; if (_col == null) _col = GetComponent(); if (!JudgeZoneGeometry.TryGetThresholds(_col, transform, out _enterY, out _exitY)) return; _thresholdsReady = true; } float noteY = transform.position.y; if (!isInsideJudgeZone && noteY <= _enterY) { isInsideJudgeZone = true; // 顺序与原物理一致:先置 hasEnteredLine(OnJudgeZoneChanged),再跑 HoldNote 分段逻辑。 OnJudgeZoneChanged?.Invoke(true); OnZoneEnterGeometry?.Invoke(); // 单帧穿透:一帧内越过整个判定区,立即补离开,行为与物理一致。 if (noteY <= _exitY) { isInsideJudgeZone = false; OnJudgeZoneChanged?.Invoke(false); OnZoneExitGeometry?.Invoke(); } } else if (isInsideJudgeZone && noteY <= _exitY) { isInsideJudgeZone = false; OnJudgeZoneChanged?.Invoke(false); OnZoneExitGeometry?.Invoke(); } } /// /// Configure legacy timing (per-segment activation). /// public void ConfigureTiming(float baseHitTime, float segmentDelay, float travelTime) { float hitTime = baseHitTime + segmentDelay; activationTime = hitTime - travelTime; isMoving = false; useAbsolutePositioning = false; visualOffset = 0f; } /// /// Legacy delay setter (not recommended). /// public void SetSegmentDelay(float segmentDelay) { activationTime = GameplayClock.NowSongTime + segmentDelay; isMoving = false; useAbsolutePositioning = false; visualOffset = 0f; } public void SetSpeed(float newSpeed) { speed = newSpeed; } /// /// Configure absolute positioning for hold segments so they stay connected. /// spawnPoint should be the original spawn point for the lane. /// baseSpawnYOffset is applied to keep delayed segments offset upward (can be negative). /// public void ConfigureAbsolutePositioning(Vector3 spawnPoint, float newActivationTime, float spawnYOffset, float newVisualOffset = 0f) { spawnPosition = spawnPoint; activationTime = newActivationTime; baseSpawnYOffset = spawnYOffset; visualOffset = newVisualOffset; useAbsolutePositioning = true; isMoving = true; // Snap immediately to the expected position to avoid a 1-frame pop. ApplyAbsolutePosition(GameplayClock.NowSongTime); } public void StopMovement() { isMoving = false; visualOffset = 0f; } // 单条 body 由 HoldNote 驱动,关闭 controller 自身的移动与几何判定。 public void EnableVisualBodyMode() { visualBodyMode = true; useAbsolutePositioning = false; isMoving = false; } // 池化复用时清掉 body 模式,让实例可回到普通段语义。 public void DisableVisualBodyMode() { visualBodyMode = false; } // 去物理判定:进入/离开判定区改由 UpdateJudgeZoneByGeometry 驱动,原 OnTrigger 回调移除。 // 池化复用时重置几何判定状态。 public void ResetJudgeZoneGeometry() { isInsideJudgeZone = false; _thresholdsReady = false; visualBodyMode = false; } public bool IsInsideJudgArea() { return isInsideJudgeZone; } public float ActivationTime => activationTime; public float CurrentSpeed => speed; public bool UsesAbsolutePositioning => useAbsolutePositioning; public float BaseSpawnYOffset => baseSpawnYOffset; public Vector3 SpawnPosition => spawnPosition; public float VisualOffset => visualOffset; private void ApplyAbsolutePosition(float time) { float elapsed = time - activationTime; float travelDistance = speed * elapsed; Vector3 newPos = spawnPosition; // Apply visual offset (positive moves the note down its path earlier) newPos.y -= (baseSpawnYOffset + travelDistance + visualOffset); transform.position = newPos; } }