先修复一下,错误的场景 删除不必要的钓场资产 修复into场景 update:更新meta文件,修复报错 update:修复资源 修复第一章节建造 修复建造第三章 update:删除多余内容 update:更新README.md update:还原图标 update:更新README update:更新配置
325 lines
12 KiB
C#
325 lines
12 KiB
C#
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
|
||
}
|
||
}
|