Files
MinFt/Client/Assets/Scripts/QTE/UiSwingTierQte.cs
Liubing\LB 3d8d4d18f3 first commit
先修复一下,错误的场景

删除不必要的钓场资产

修复into场景

update:更新meta文件,修复报错

update:修复资源

修复第一章节建造

修复建造第三章

update:删除多余内容

update:更新README.md

update:还原图标

update:更新README

update:更新配置
2026-04-28 02:17:22 +08:00

325 lines
12 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
namespace game
{
/// <summary>
/// UGUI 通用「摆动指针 + 分档倍率」:在 Inspector 配置 <see cref="tierBindings"/>,确认时按箭头落在哪一档返回倍率;
/// 未命中任意 zone 时返回 <see cref="missWhenNoHitMultiplier"/>。重叠区域按 <see cref="SwingQteTierBinding.priority"/> 从高到低判定。
/// </summary>
public class UiSwingTierQte : MonoBehaviour
{
[Header("多档")]
[SerializeField] private List<SwingQteTierBinding> tierBindings = new List<SwingQteTierBinding>();
[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<float> onConfirmed;
/// <summary>供代码订阅 QTE 确认Inspector 的 <see cref="onConfirmed"/> 为 private。</summary>
public void AddOnConfirmedListener(UnityAction<float> listener) => onConfirmed?.AddListener(listener);
public void RemoveOnConfirmedListener(UnityAction<float> listener) => onConfirmed?.RemoveListener(listener);
private bool _active;
private float _phase;
private readonly List<float> _runtimeMultipliers = new List<float>();
private float _missMultiplierRuntime = 1f;
private readonly List<TierEvalRow> _sortedEvalRows = new List<TierEvalRow>();
private struct TierEvalRow
{
public RectTransform Zone;
public float Multiplier;
public int Priority;
public int OriginalIndex;
}
public bool IsActive => _active;
public IReadOnlyList<SwingQteTierBinding> TierBindings => tierBindings;
public float MissWhenNoHitMultiplier => _missMultiplierRuntime;
private bool HasTierBindings => tierBindings != null && tierBindings.Count > 0;
/// <summary>
/// 与 <see cref="tierBindings"/> 行数相同的规格列表(倍率 + 可选归一化宽度)。(0,0) 归一化表示不改该档 Rect 锚点。
/// </summary>
public void ConfigureFromSpecs(IReadOnlyList<SwingQteTierRuntimeSpec> 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();
}
/// <summary>仅更新各档倍率,不改变 Rect 布局。</summary>
public void ConfigureMultipliers(IReadOnlyList<float> 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();
}
/// <summary>仅按归一化区间更新各档水平锚点spans 与 tierBindings 等长,(0,0) 跳过该行。</summary>
public void ApplyNormalizedSpans(IReadOnlyList<Vector2> 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);
}
}
/// <summary>运行时修改未命中倍率(与 Inspector 字段同步)。</summary>
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
}
}