207 lines
6.8 KiB
C#
207 lines
6.8 KiB
C#
using UnityEngine;
|
|
using System;
|
|
|
|
public class HoldNoteController : MonoBehaviour
|
|
{
|
|
private bool isInsideJudgeZone = false;
|
|
public event Action<bool> 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<Collider2D>();
|
|
// 去物理判定:关闭 Rigidbody2D 模拟,消除每帧移动刚体的物理开销(判定改几何驱动)。
|
|
var rb = GetComponent<Rigidbody2D>();
|
|
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<Collider2D>();
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Configure legacy timing (per-segment activation).
|
|
/// </summary>
|
|
public void ConfigureTiming(float baseHitTime, float segmentDelay, float travelTime)
|
|
{
|
|
float hitTime = baseHitTime + segmentDelay;
|
|
activationTime = hitTime - travelTime;
|
|
isMoving = false;
|
|
useAbsolutePositioning = false;
|
|
visualOffset = 0f;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Legacy delay setter (not recommended).
|
|
/// </summary>
|
|
public void SetSegmentDelay(float segmentDelay)
|
|
{
|
|
activationTime = GameplayClock.NowSongTime + segmentDelay;
|
|
isMoving = false;
|
|
useAbsolutePositioning = false;
|
|
visualOffset = 0f;
|
|
}
|
|
|
|
public void SetSpeed(float newSpeed)
|
|
{
|
|
speed = newSpeed;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
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;
|
|
}
|
|
}
|