备份,准备做双端
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user