172 lines
5.3 KiB
C#
172 lines
5.3 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UnityEngine.UI;
|
|
|
|
/// <summary>
|
|
/// 判定线旁的实时偏移条:每次判定在横条上生成一个彩色点,
|
|
/// x 位置表示偏移大小(左=偏晚 Late,右=偏早 Early),随后淡出。
|
|
/// 纯显示组件,订阅 ScoreManager.OnOffsetRecorded,不参与任何判定/计分逻辑。
|
|
/// 用法:挂到一个 RectTransform 上,作为横条容器;在 Inspector 指定 dotPrefab。
|
|
/// </summary>
|
|
[RequireComponent(typeof(RectTransform))]
|
|
public class OffsetBarDisplay : MonoBehaviour
|
|
{
|
|
[Header("References")]
|
|
[Tooltip("单个偏移点的预制(应带 Image/RectTransform)。若留空则运行时自动生成一个圆点。")]
|
|
public RectTransform dotPrefab;
|
|
[Tooltip("横条容器;留空则用本物体的 RectTransform。")]
|
|
public RectTransform barContainer;
|
|
[Tooltip("可选:中线(完美判定 0ms)指示线,仅用于视觉参考。")]
|
|
public RectTransform centerMarker;
|
|
|
|
[Header("Mapping")]
|
|
[Tooltip("映射到横条边缘的最大偏移(毫秒)。超出按此裁剪。")]
|
|
public float maxOffsetMs = 120f;
|
|
[Tooltip("横条半宽(像素)。点的 x 在 [-halfWidth, halfWidth] 内。留 <=0 则用容器宽度的一半。")]
|
|
public float halfWidthPixels = 0f;
|
|
|
|
[Header("Appearance")]
|
|
public float dotLifetime = 0.9f;
|
|
public float dotStartSize = 14f;
|
|
public Color earlyColor = new Color(0.35f, 0.7f, 1f, 1f);
|
|
public Color lateColor = new Color(1f, 0.55f, 0.35f, 1f);
|
|
public Color perfectColor = new Color(0.6f, 1f, 0.6f, 1f);
|
|
[Tooltip("|偏移| 小于此值视为完美,用 perfectColor。")]
|
|
public float perfectThresholdMs = 8f;
|
|
[Tooltip("对象池上限,避免高连击时无限实例化。")]
|
|
public int maxPooledDots = 48;
|
|
|
|
private class Dot
|
|
{
|
|
public RectTransform rect;
|
|
public Image image;
|
|
public float age;
|
|
public bool active;
|
|
}
|
|
|
|
private readonly List<Dot> pool = new List<Dot>();
|
|
|
|
private void OnEnable()
|
|
{
|
|
ScoreManager.OnOffsetRecorded += HandleOffset;
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
ScoreManager.OnOffsetRecorded -= HandleOffset;
|
|
}
|
|
|
|
private void Awake()
|
|
{
|
|
if (barContainer == null) barContainer = GetComponent<RectTransform>();
|
|
}
|
|
|
|
private float HalfWidth()
|
|
{
|
|
if (halfWidthPixels > 0f) return halfWidthPixels;
|
|
if (barContainer != null) return barContainer.rect.width * 0.5f;
|
|
return 200f;
|
|
}
|
|
|
|
// 正数 = 偏早(Early),负数 = 偏晚(Late)。见 ScoreManager.RecordOffset。
|
|
private void HandleOffset(float offsetMs)
|
|
{
|
|
if (!isActiveAndEnabled) return;
|
|
|
|
Dot dot = GetPooledDot();
|
|
if (dot == null) return;
|
|
|
|
float clamped = Mathf.Clamp(offsetMs, -maxOffsetMs, maxOffsetMs);
|
|
float norm = maxOffsetMs > 0f ? clamped / maxOffsetMs : 0f; // [-1,1]
|
|
// Early(正) 放右侧,Late(负) 放左侧。
|
|
float x = norm * HalfWidth();
|
|
|
|
Vector2 pos = dot.rect.anchoredPosition;
|
|
pos.x = x;
|
|
pos.y = 0f;
|
|
dot.rect.anchoredPosition = pos;
|
|
dot.rect.sizeDelta = new Vector2(dotStartSize, dotStartSize);
|
|
|
|
Color c;
|
|
if (Mathf.Abs(offsetMs) < perfectThresholdMs) c = perfectColor;
|
|
else c = offsetMs > 0f ? earlyColor : lateColor;
|
|
dot.image.color = c;
|
|
|
|
dot.age = 0f;
|
|
dot.active = true;
|
|
dot.rect.gameObject.SetActive(true);
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
float dt = Time.unscaledDeltaTime;
|
|
float life = Mathf.Max(0.01f, dotLifetime);
|
|
|
|
for (int i = 0; i < pool.Count; i++)
|
|
{
|
|
Dot d = pool[i];
|
|
if (!d.active) continue;
|
|
|
|
d.age += dt;
|
|
float t = d.age / life;
|
|
if (t >= 1f)
|
|
{
|
|
d.active = false;
|
|
d.rect.gameObject.SetActive(false);
|
|
continue;
|
|
}
|
|
|
|
Color c = d.image.color;
|
|
c.a = 1f - t;
|
|
d.image.color = c;
|
|
}
|
|
}
|
|
|
|
private Dot GetPooledDot()
|
|
{
|
|
for (int i = 0; i < pool.Count; i++)
|
|
{
|
|
if (!pool[i].active) return pool[i];
|
|
}
|
|
|
|
if (pool.Count >= maxPooledDots)
|
|
{
|
|
// 全部占用:复用最老的一个。
|
|
Dot oldest = pool[0];
|
|
for (int i = 1; i < pool.Count; i++)
|
|
if (pool[i].age > oldest.age) oldest = pool[i];
|
|
return oldest;
|
|
}
|
|
|
|
return CreateDot();
|
|
}
|
|
|
|
private Dot CreateDot()
|
|
{
|
|
RectTransform rect;
|
|
Image img;
|
|
|
|
if (dotPrefab != null)
|
|
{
|
|
rect = Instantiate(dotPrefab, barContainer);
|
|
img = rect.GetComponent<Image>();
|
|
if (img == null) img = rect.gameObject.AddComponent<Image>();
|
|
}
|
|
else
|
|
{
|
|
GameObject go = new GameObject("OffsetDot", typeof(RectTransform), typeof(Image));
|
|
rect = go.GetComponent<RectTransform>();
|
|
rect.SetParent(barContainer, false);
|
|
img = go.GetComponent<Image>();
|
|
}
|
|
|
|
rect.anchorMin = rect.anchorMax = new Vector2(0.5f, 0.5f);
|
|
rect.pivot = new Vector2(0.5f, 0.5f);
|
|
rect.gameObject.SetActive(false);
|
|
|
|
Dot dot = new Dot { rect = rect, image = img, age = 0f, active = false };
|
|
pool.Add(dot);
|
|
return dot;
|
|
}
|
|
}
|