using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
namespace game
{
///
/// UGUI 通用「摆动指针 + 分档倍率」:在 Inspector 配置 ,确认时按箭头落在哪一档返回倍率;
/// 未命中任意 zone 时返回 。重叠区域按 从高到低判定。
///
public class UiSwingTierQte : MonoBehaviour
{
[Header("多档")]
[SerializeField] private List tierBindings = new List();
[Tooltip("箭头未落在任一命中 zone 内时的倍率")]
[SerializeField] private float missWhenNoHitMultiplier = 1f;
[Header("引用")]
[SerializeField] private RectTransform trackRect;
[SerializeField] private RectTransform arrowRect;
[SerializeField] private Button confirmButton;
[Header("摆动")]
[SerializeField] private float oscillationSpeed = 6f;
[SerializeField] private bool deriveSwingExtentFromTrack;
[SerializeField] private float swingEdgePadding;
[SerializeField] private float swingHalfExtent = 200f;
[SerializeField] private bool useUnscaledTime;
[Header("事件")]
[SerializeField] private UnityEvent onConfirmed;
/// 供代码订阅 QTE 确认(Inspector 的 为 private)。
public void AddOnConfirmedListener(UnityAction listener) => onConfirmed?.AddListener(listener);
public void RemoveOnConfirmedListener(UnityAction listener) => onConfirmed?.RemoveListener(listener);
private bool _active;
private float _phase;
private readonly List _runtimeMultipliers = new List();
private float _missMultiplierRuntime = 1f;
private readonly List _sortedEvalRows = new List();
private struct TierEvalRow
{
public RectTransform Zone;
public float Multiplier;
public int Priority;
public int OriginalIndex;
}
public bool IsActive => _active;
public IReadOnlyList TierBindings => tierBindings;
public float MissWhenNoHitMultiplier => _missMultiplierRuntime;
private bool HasTierBindings => tierBindings != null && tierBindings.Count > 0;
///
/// 与 行数相同的规格列表(倍率 + 可选归一化宽度)。(0,0) 归一化表示不改该档 Rect 锚点。
///
public void ConfigureFromSpecs(IReadOnlyList specs)
{
if (!HasTierBindings)
{
Debug.LogWarning($"[{nameof(UiSwingTierQte)}] 请先在 Inspector 配置 tierBindings。", this);
return;
}
if (specs == null || specs.Count != tierBindings.Count)
{
Debug.LogError($"[{nameof(UiSwingTierQte)}] specs 数量必须为 tierBindings.Count = {tierBindings.Count}。", this);
return;
}
_runtimeMultipliers.Clear();
for (int i = 0; i < specs.Count; i++)
{
var s = specs[i];
_runtimeMultipliers.Add(s.Multiplier);
var b = tierBindings[i];
if (b.zone == null)
continue;
if (!IsNormalizedSpanUnset(s.NormalizedMin, s.NormalizedMax))
ApplyHorizontalSpanFromNormalized(b.zone, s.NormalizedMin, s.NormalizedMax);
}
_missMultiplierRuntime = missWhenNoHitMultiplier;
RebuildHitEvaluationOrder();
}
/// 仅更新各档倍率,不改变 Rect 布局。
public void ConfigureMultipliers(IReadOnlyList multipliers)
{
if (!HasTierBindings)
{
Debug.LogWarning($"[{nameof(UiSwingTierQte)}] 请先在 Inspector 配置 tierBindings。", this);
return;
}
if (multipliers == null || multipliers.Count != tierBindings.Count)
{
Debug.LogError($"[{nameof(UiSwingTierQte)}] multipliers 数量必须为 {tierBindings.Count}。", this);
return;
}
_runtimeMultipliers.Clear();
for (int i = 0; i < multipliers.Count; i++)
_runtimeMultipliers.Add(multipliers[i]);
_missMultiplierRuntime = missWhenNoHitMultiplier;
RebuildHitEvaluationOrder();
}
/// 仅按归一化区间更新各档水平锚点;spans 与 tierBindings 等长,(0,0) 跳过该行。
public void ApplyNormalizedSpans(IReadOnlyList spans)
{
if (!HasTierBindings || spans == null || spans.Count != tierBindings.Count)
{
Debug.LogError($"[{nameof(UiSwingTierQte)}] ApplyNormalizedSpans 需要 tierBindings 非空且 spans.Count == tierBindings.Count。", this);
return;
}
for (int i = 0; i < spans.Count; i++)
{
var z = tierBindings[i].zone;
if (z == null)
continue;
var sp = spans[i];
if (!IsNormalizedSpanUnset(sp.x, sp.y))
ApplyHorizontalSpanFromNormalized(z, sp.x, sp.y);
}
}
/// 运行时修改未命中倍率(与 Inspector 字段同步)。
public void SetMissWhenNoHitMultiplier(float missMultiplier)
{
missWhenNoHitMultiplier = missMultiplier;
_missMultiplierRuntime = missMultiplier;
}
private void RebuildHitEvaluationOrder()
{
_sortedEvalRows.Clear();
if (!HasTierBindings)
{
_missMultiplierRuntime = missWhenNoHitMultiplier;
return;
}
for (int i = 0; i < tierBindings.Count; i++)
{
var b = tierBindings[i];
if (b.zone == null)
continue;
float m = i < _runtimeMultipliers.Count ? _runtimeMultipliers[i] : 1f;
_sortedEvalRows.Add(new TierEvalRow
{
Zone = b.zone,
Multiplier = m,
Priority = b.priority,
OriginalIndex = i
});
}
_sortedEvalRows.Sort(CompareEvalRows);
}
private static int CompareEvalRows(TierEvalRow a, TierEvalRow b)
{
int c = b.Priority.CompareTo(a.Priority);
return c != 0 ? c : a.OriginalIndex.CompareTo(b.OriginalIndex);
}
private static bool IsNormalizedSpanUnset(float min, float max)
{
return min == 0f && max == 0f;
}
private void ApplyHorizontalSpanFromNormalized(RectTransform zone, float minNorm, float maxNorm)
{
if (trackRect == null || zone == null)
return;
if (zone.parent != trackRect)
{
Debug.LogWarning(
$"[{nameof(UiSwingTierQte)}] 「{zone.name}」应为「{trackRect.name}」的直接子节点,否则无法按归一化宽度设置档位区域。",
this);
return;
}
float lo = Mathf.Clamp01(Mathf.Min(minNorm, maxNorm));
float hi = Mathf.Clamp01(Mathf.Max(minNorm, maxNorm));
zone.anchorMin = new Vector2(lo, zone.anchorMin.y);
zone.anchorMax = new Vector2(hi, zone.anchorMax.y);
}
private void Awake()
{
if (confirmButton != null)
confirmButton.onClick.AddListener(Confirm);
}
private void OnEnable()
{
SyncRuntimeMultipliersFromInspectorDefaults();
_missMultiplierRuntime = missWhenNoHitMultiplier;
RebuildHitEvaluationOrder();
}
private void SyncRuntimeMultipliersFromInspectorDefaults()
{
if (!HasTierBindings)
return;
if (_runtimeMultipliers.Count == tierBindings.Count)
return;
_runtimeMultipliers.Clear();
for (int i = 0; i < tierBindings.Count; i++)
_runtimeMultipliers.Add(1f);
}
private void OnDestroy()
{
if (confirmButton != null)
confirmButton.onClick.RemoveListener(Confirm);
}
private void Update()
{
if (!_active || trackRect == null || arrowRect == null)
return;
float dt = useUnscaledTime ? Time.unscaledDeltaTime : Time.deltaTime;
_phase += dt * oscillationSpeed;
float t = Mathf.Sin(_phase);
float half = deriveSwingExtentFromTrack
? Mathf.Max(0f, trackRect.rect.width * 0.5f - swingEdgePadding)
: swingHalfExtent;
float x = t * half;
var ap = arrowRect.anchoredPosition;
ap.x = x;
arrowRect.anchoredPosition = ap;
}
public void Begin()
{
_active = true;
enabled = true;
if (confirmButton != null)
confirmButton.interactable = true;
}
public void RandomizePhase()
{
_phase = UnityEngine.Random.Range(0f, Mathf.PI * 2f);
}
public void Stop()
{
_active = false;
if (confirmButton != null)
confirmButton.interactable = false;
}
public void Confirm()
{
if (!_active)
return;
float mult = EvaluateMultiplierAtArrow();
Stop();
onConfirmed?.Invoke(mult);
}
public float PeekCurrentMultiplier()
{
return EvaluateMultiplierAtArrow();
}
public float EvaluateMultiplierAtArrow()
{
if (trackRect == null || arrowRect == null)
return _missMultiplierRuntime;
Vector3 local = trackRect.InverseTransformPoint(arrowRect.position);
for (int i = 0; i < _sortedEvalRows.Count; i++)
{
var row = _sortedEvalRows[i];
if (row.Zone != null && RectContainsLocalXByRectWidth(trackRect, row.Zone, local.x))
return row.Multiplier;
}
return _missMultiplierRuntime;
}
private static bool RectContainsLocalXByRectWidth(RectTransform track, RectTransform zone, float trackLocalX)
{
var corners = new Vector3[4];
zone.GetWorldCorners(corners);
float minX = float.PositiveInfinity;
float maxX = float.NegativeInfinity;
for (int i = 0; i < 4; i++)
{
Vector3 zLocal = track.InverseTransformPoint(corners[i]);
minX = Mathf.Min(minX, zLocal.x);
maxX = Mathf.Max(maxX, zLocal.x);
}
return trackLocalX >= minX && trackLocalX <= maxX;
}
#if UNITY_EDITOR
private void OnValidate()
{
oscillationSpeed = Mathf.Max(0.01f, oscillationSpeed);
swingHalfExtent = Mathf.Max(0f, swingHalfExtent);
swingEdgePadding = Mathf.Max(0f, swingEdgePadding);
}
#endif
}
}