776 lines
29 KiB
C#
776 lines
29 KiB
C#
using System.Linq;
|
||
using AibisDream.Framework;
|
||
using UnityEngine;
|
||
using UnityEngine.Events;
|
||
using UnityEngine.EventSystems;
|
||
using DG.Tweening;
|
||
using System.Collections;
|
||
|
||
namespace AibisDream.MiniGame.HuoShan
|
||
{
|
||
/// <summary>
|
||
/// 发条控制器 - 实现发条的拖拽旋转交互
|
||
/// 功能:拖拽旋转,最大260°,缓慢回位,触发事件
|
||
/// 交互通过 EventTriggerEx 统一管理,支持 EventSystemEx.isLocked 和 IInteraction 状态
|
||
/// 在发条 GameObject 上添加 EventTriggerEx + Collider2D 即可接入;未添加则回退到 Input+Raycast
|
||
/// </summary>
|
||
public class CrankController : MonoBehaviour, IInteraction
|
||
{
|
||
[Header("发条配置")]
|
||
[Tooltip("最大旋转角度(度)")]
|
||
[SerializeField] private float maxRotationAngle = 260f;
|
||
|
||
[Tooltip("回位时长(秒),从满弦回到 0 的匀速时间,与滑片滑动时长匹配")]
|
||
[SerializeField] private float returnDuration = 3f;
|
||
|
||
[Tooltip("触发事件的最小角度(度)")]
|
||
[SerializeField] private float triggerAngle = 240f;
|
||
|
||
[Tooltip("拖拽阻力,值越大越难拖动")]
|
||
[SerializeField] private float dragResistance = 0.1f;
|
||
|
||
[Tooltip("是否自动返回初始位置")]
|
||
[SerializeField] private bool autoReturn = true;
|
||
|
||
[Header("事件" )]
|
||
[Tooltip("发条达到最大角度并释放时触发")]
|
||
public UnityEvent onCrankComplete;
|
||
|
||
[Tooltip("发条旋转时触发(参数:当前角度0-1)")]
|
||
public UnityEvent<float> onCrankRotate;
|
||
|
||
[Tooltip("发条开始拖拽时触发")]
|
||
public UnityEvent onCrankStartDrag;
|
||
|
||
[Header("逆时针转圈表现")]
|
||
[Tooltip("逆时针转一圈的动画时长(秒)")]
|
||
[SerializeField] private float spinOnceDuration = 1f;
|
||
|
||
[Tooltip("逆时针转一圈完成后触发")]
|
||
public UnityEvent onSpinOnceComplete;
|
||
|
||
[Header("圆环/中心配置")]
|
||
[Tooltip("环中心(内环旋转中心),配置后发条将沿环轨道移动;不填则原地旋转")]
|
||
[SerializeField] private Transform ringCenter;
|
||
|
||
[Tooltip("环半径(发条到中心的距离),0 则从初始位置自动计算")]
|
||
[SerializeField] private float ringRadius = 0f;
|
||
|
||
[Tooltip("起始角度偏移(度),0=右侧3点钟方向")]
|
||
[SerializeField] private float startAngleOffset = 0f;
|
||
|
||
[Tooltip("面向圆心时的旋转偏移(度),用于调整图片朝向,不同 sprite 的正面方向不同")]
|
||
[SerializeField] private float facingRotationOffset = 0f;
|
||
|
||
[Tooltip("角度正方向:勾选=顺时针为正,不勾选=逆时针为正")]
|
||
[SerializeField] private bool clockwisePositive = false;
|
||
|
||
[Header("视觉反馈")]
|
||
[Tooltip("发条旋转的 Transform(手柄 visuals)")]
|
||
[SerializeField] private Transform crankTransform;
|
||
|
||
[Tooltip("用于检测点击的 Collider2D,不填则尝试从 crankTransform 获取")]
|
||
[SerializeField] private Collider2D interactionCollider;
|
||
|
||
[Tooltip("拖拽时的缩放效果")]
|
||
[SerializeField] private float dragScale = 1.05f;
|
||
|
||
[Tooltip("缩放动画时长")]
|
||
[SerializeField] private float scaleDuration = 0.1f;
|
||
|
||
[Header("交互提示")]
|
||
[Tooltip("高亮目标:发条 Sprite(材质需含 _ShineFade);不填则使用 crankTransform 上 SpriteRenderer")]
|
||
[SerializeField] private SpriteRenderer interactionHighlightSprite;
|
||
|
||
[Tooltip("开始拖拽发条时是否自动关闭高亮")]
|
||
[SerializeField] private bool clearHighlightWhenDragStarts = true;
|
||
|
||
[Header("上弦判定")]
|
||
[Tooltip("进入成功范围后吸附到终点的时长")]
|
||
[SerializeField] private float completionSnapDuration = 0.15f;
|
||
|
||
[Tooltip("当前轮未上满多少次后显示 Yarn 文本提示")]
|
||
[SerializeField] private int failedAttemptHintThreshold = 2;
|
||
|
||
[Tooltip("连续未上满后调用的 Yarn 节点")]
|
||
[SerializeField] private string failedAttemptHintNode = "发条操作提示";
|
||
|
||
private static readonly int ShineFadeId = Shader.PropertyToID("_ShineFade");
|
||
private SpriteRenderer _shineHighlightSprite;
|
||
|
||
private Vector3 _dragStartPosition;
|
||
private float _dragStartAngle; // 按下时鼠标相对中心的角度
|
||
private float _dragStartRotation; // 按下时发条的角度
|
||
private float _lastMouseAngle; // 上一帧鼠标角度,用于累计增量
|
||
private float _currentRotation = 0f;
|
||
private bool _isDragging = false;
|
||
private bool _hasTriggered = false;
|
||
private Camera _mainCamera;
|
||
private Vector3 _initialScale;
|
||
private Tweener _scaleTween;
|
||
private Tweener _returnTween;
|
||
private Tweener _spinTween;
|
||
private Tweener _completeTween;
|
||
private float _spinAngleOffset; // 逆时针转圈时的附加角度(0~360)
|
||
private bool _isSpinning; // 正在播放逆时针转圈
|
||
private float _effectiveRadius; // 实际使用的环半径
|
||
private bool _isReturning;
|
||
private bool _isCompleting;
|
||
private float _returnSpeedPerSec; // 回位时的度/秒,匀速
|
||
private int _failedAttemptCount;
|
||
private bool _failedAttemptHintShown;
|
||
|
||
private EventTriggerEx _eventTrigger;
|
||
private bool _useEventTrigger;
|
||
|
||
#region 生命周期
|
||
|
||
private void Awake()
|
||
{
|
||
_mainCamera = Camera.main;
|
||
if (crankTransform != null)
|
||
{
|
||
_initialScale = crankTransform.localScale;
|
||
}
|
||
else
|
||
{
|
||
_initialScale = transform.localScale;
|
||
crankTransform = transform;
|
||
}
|
||
|
||
maxRotationAngle = Mathf.Max(0.01f, maxRotationAngle);
|
||
dragResistance = Mathf.Clamp01(dragResistance);
|
||
|
||
// 完成判定不能超过画面上的旋转终点。
|
||
// 旧逻辑会在配置越界时悄悄改为 80%,导致画面、手感和判定使用不同的角度。
|
||
if (triggerAngle > maxRotationAngle)
|
||
{
|
||
Debug.LogWarning($"[CrankController] triggerAngle ({triggerAngle}°) 超过 maxRotationAngle ({maxRotationAngle}°),已对齐到旋转终点", this);
|
||
triggerAngle = maxRotationAngle;
|
||
}
|
||
triggerAngle = Mathf.Clamp(triggerAngle, 0f, maxRotationAngle);
|
||
|
||
// 未配置交互碰撞体时,尝试从 crankTransform 获取
|
||
if (interactionCollider == null && crankTransform != null)
|
||
{
|
||
interactionCollider = crankTransform.GetComponent<Collider2D>();
|
||
#if UNITY_EDITOR
|
||
if (interactionCollider == null)
|
||
{
|
||
Debug.LogWarning($"[CrankController] 未找到 Collider2D!请在「发条」或其父物体上添加 Collider2D(如 CircleCollider2D),或在 interactionCollider 中手动指定。", this);
|
||
}
|
||
#endif
|
||
}
|
||
|
||
// 计算环半径:优先使用配置值,否则从发条到 ringCenter 的距离
|
||
if (ringCenter != null && crankTransform != null)
|
||
{
|
||
_effectiveRadius = ringRadius > 0f
|
||
? ringRadius
|
||
: Vector2.Distance(
|
||
new Vector2(crankTransform.position.x, crankTransform.position.y),
|
||
new Vector2(ringCenter.position.x, ringCenter.position.y));
|
||
if (_effectiveRadius < 0.01f) _effectiveRadius = 1f; // 防止除零
|
||
}
|
||
else
|
||
{
|
||
_effectiveRadius = 0f; // 原地旋转模式
|
||
}
|
||
|
||
// 事件目标:Collider 所在的对象(可能与 CrankController 不同,如手柄在子物体)
|
||
var eventTargetGo = interactionCollider != null ? interactionCollider.gameObject : (crankTransform != null ? crankTransform.gameObject : gameObject);
|
||
|
||
_eventTrigger = eventTargetGo.GetComponent<EventTriggerEx>();
|
||
if (_eventTrigger == null && eventTargetGo.GetComponent<Collider2D>() != null)
|
||
{
|
||
if (eventTargetGo != gameObject)
|
||
{
|
||
var bridge = eventTargetGo.GetComponent<CrankInteractionBridge>();
|
||
if (bridge == null) eventTargetGo.AddComponent<CrankInteractionBridge>();
|
||
}
|
||
_eventTrigger = eventTargetGo.AddComponent<EventTriggerEx>();
|
||
}
|
||
if (_eventTrigger != null)
|
||
{
|
||
_useEventTrigger = true;
|
||
EnsureEventTriggerEntries();
|
||
_eventTrigger.Register(EventTriggerType.PointerDown, OnEventPointerDown);
|
||
_eventTrigger.Register(EventTriggerType.PointerUp, OnEventPointerUp);
|
||
_eventTrigger.Register(EventTriggerType.Drag, OnEventDrag);
|
||
}
|
||
|
||
EnsureInteractionHighlightSprite();
|
||
SetInteractionHighlightVisible(false);
|
||
}
|
||
|
||
private void EnsureInteractionHighlightSprite()
|
||
{
|
||
if (interactionHighlightSprite != null)
|
||
{
|
||
_shineHighlightSprite = interactionHighlightSprite;
|
||
return;
|
||
}
|
||
if (crankTransform != null)
|
||
_shineHighlightSprite = crankTransform.GetComponent<SpriteRenderer>()
|
||
?? crankTransform.GetComponentInChildren<SpriteRenderer>(true);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 控制发条交互提示高亮(材质 _ShineFade)。由外部脚本、Yarn 命令等在合适时机调用。
|
||
/// </summary>
|
||
public void SetInteractionHighlightVisible(bool visible)
|
||
{
|
||
EnsureInteractionHighlightSprite();
|
||
if (_shineHighlightSprite == null) return;
|
||
var mat = _shineHighlightSprite.material;
|
||
if (mat == null || !mat.HasProperty(ShineFadeId)) return;
|
||
mat.SetFloat(ShineFadeId, visible ? 1f : 0f);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 确保 EventTrigger 有需要的触发类型(若使用 EventTriggerEx 管理交互)
|
||
/// </summary>
|
||
private void EnsureEventTriggerEntries()
|
||
{
|
||
if (_eventTrigger == null) return;
|
||
var triggers = _eventTrigger.triggers;
|
||
var needed = new[] { EventTriggerType.PointerDown, EventTriggerType.PointerUp, EventTriggerType.Drag };
|
||
foreach (var t in needed)
|
||
{
|
||
if (triggers.Any(e => e.eventID == t)) continue;
|
||
triggers.Add(new EventTrigger.Entry { eventID = t });
|
||
}
|
||
}
|
||
|
||
private void OnEnable()
|
||
{
|
||
ResetFailedAttempts();
|
||
ResetCrank();
|
||
}
|
||
|
||
#if UNITY_EDITOR
|
||
private void OnDrawGizmosSelected()
|
||
{
|
||
if (ringCenter == null) return;
|
||
float r = ringRadius > 0f ? ringRadius : 0f;
|
||
if (r <= 0f && crankTransform != null && !Application.isPlaying)
|
||
{
|
||
r = Vector2.Distance(
|
||
new Vector2(crankTransform.position.x, crankTransform.position.y),
|
||
new Vector2(ringCenter.position.x, ringCenter.position.y));
|
||
}
|
||
if (r < 0.01f) return;
|
||
|
||
Vector3 center = ringCenter.position;
|
||
Gizmos.color = new Color(0.3f, 0.9f, 0.55f, 0.6f);
|
||
const int segments = 64;
|
||
for (int i = 0; i < segments; i++)
|
||
{
|
||
float a0 = (float)i / segments * 2f * Mathf.PI;
|
||
float a1 = (float)(i + 1) / segments * 2f * Mathf.PI;
|
||
Vector3 p0 = center + new Vector3(Mathf.Cos(a0) * r, Mathf.Sin(a0) * r, center.z);
|
||
Vector3 p1 = center + new Vector3(Mathf.Cos(a1) * r, Mathf.Sin(a1) * r, center.z);
|
||
Gizmos.DrawLine(p0, p1);
|
||
}
|
||
// 起点 / 终点标记(红=起点,橙=终点)
|
||
float startVisual = startAngleOffset;
|
||
float endVisual = clockwisePositive
|
||
? startAngleOffset - maxRotationAngle
|
||
: startAngleOffset + maxRotationAngle;
|
||
float startRadian = startVisual * Mathf.Deg2Rad;
|
||
float endRadian = endVisual * Mathf.Deg2Rad;
|
||
Vector3 startPos = center + new Vector3(Mathf.Cos(startRadian) * r, Mathf.Sin(startRadian) * r, center.z);
|
||
Vector3 endPos = center + new Vector3(Mathf.Cos(endRadian) * r, Mathf.Sin(endRadian) * r, center.z);
|
||
Gizmos.color = new Color(1f, 0f, 0f, 0.9f);
|
||
Gizmos.DrawWireSphere(startPos, 0.08f);
|
||
Gizmos.color = new Color(1f, 0.55f, 0f, 0.9f);
|
||
Gizmos.DrawWireSphere(endPos, 0.08f);
|
||
Gizmos.color = new Color(1f, 0.5f, 0f, 0.8f);
|
||
Gizmos.DrawWireSphere(center, 0.05f);
|
||
}
|
||
#endif
|
||
|
||
private void Update()
|
||
{
|
||
if (_useEventTrigger)
|
||
{
|
||
// 交互由 EventTriggerEx 驱动,此处仅处理回位
|
||
}
|
||
else
|
||
{
|
||
HandleMouseInputFallback();
|
||
}
|
||
HandleReturn();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 输入处理(EventTriggerEx 驱动 / 回退)
|
||
|
||
private void OnEventPointerDown(BaseEventData eventData)
|
||
{
|
||
if (eventData is PointerEventData { button: PointerEventData.InputButton.Left })
|
||
StartDrag();
|
||
}
|
||
|
||
private void OnEventPointerUp(BaseEventData eventData)
|
||
{
|
||
if (eventData is PointerEventData { button: PointerEventData.InputButton.Left })
|
||
EndDrag();
|
||
}
|
||
|
||
private void OnEventDrag(BaseEventData eventData)
|
||
{
|
||
if (_isDragging && eventData is PointerEventData pointerData)
|
||
ContinueDrag(pointerData.position);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 回退:无 EventTriggerEx 时使用 Input + Raycast(如 prefab 未迁移场景)
|
||
/// </summary>
|
||
private void HandleMouseInputFallback()
|
||
{
|
||
if (Input.GetMouseButtonDown(0))
|
||
{
|
||
Ray ray = _mainCamera.ScreenPointToRay(Input.mousePosition);
|
||
RaycastHit2D hit = Physics2D.GetRayIntersection(ray, float.MaxValue);
|
||
|
||
if (hit.collider != null && IsValidHitTarget(hit.collider.gameObject))
|
||
{
|
||
StartDrag();
|
||
}
|
||
}
|
||
|
||
if (_isDragging)
|
||
{
|
||
if (Input.GetMouseButton(0))
|
||
{
|
||
ContinueDrag(Input.mousePosition);
|
||
}
|
||
else if (Input.GetMouseButtonUp(0))
|
||
{
|
||
EndDrag();
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取旋转中心(环中心优先,否则用发条父节点)
|
||
/// </summary>
|
||
private Vector3 GetRotationPivot()
|
||
{
|
||
if (ringCenter != null) return ringCenter.position;
|
||
if (crankTransform != null && crankTransform.parent != null) return crankTransform.parent.position;
|
||
return crankTransform != null ? crankTransform.position : transform.position;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 应用发条的位置和旋转。配置了 ringCenter 时沿环轨道移动,否则原地旋转。
|
||
/// </summary>
|
||
private void ApplyCrankTransform(float angle)
|
||
{
|
||
if (crankTransform == null) return;
|
||
|
||
// 顺时针为正时,视觉上角度取反(数学角度逆时针为正)
|
||
float visualAngle = clockwisePositive ? -angle : angle;
|
||
float positionAngle = startAngleOffset + visualAngle + _spinAngleOffset;
|
||
|
||
if (ringCenter != null && _effectiveRadius > 0f)
|
||
{
|
||
// 沿环轨道移动:更新世界坐标位置
|
||
float radian = positionAngle * Mathf.Deg2Rad;
|
||
Vector3 center = ringCenter.position;
|
||
float z = crankTransform.position.z;
|
||
Vector3 position = center + new Vector3(
|
||
Mathf.Cos(radian) * _effectiveRadius,
|
||
Mathf.Sin(radian) * _effectiveRadius,
|
||
z
|
||
);
|
||
crankTransform.position = position;
|
||
|
||
// 始终面向圆心(径向朝内),加 offset 调整 sprite 正面朝向
|
||
Vector3 toCenter = center - position;
|
||
float radialAngle = Mathf.Atan2(toCenter.y, toCenter.x) * Mathf.Rad2Deg;
|
||
crankTransform.rotation = Quaternion.Euler(0f, 0f, radialAngle + facingRotationOffset);
|
||
}
|
||
else
|
||
{
|
||
// 原地旋转:-visualAngle 为基础朝向,+_spinAngleOffset 为逆时针转圈叠加
|
||
crankTransform.localRotation = Quaternion.Euler(0f, 0f, -visualAngle + _spinAngleOffset);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查点击目标是否有效(本物体、发条、交互碰撞体或其子物体)
|
||
/// </summary>
|
||
private bool IsValidHitTarget(GameObject hitGo)
|
||
{
|
||
if (hitGo == gameObject) return true;
|
||
if (crankTransform != null && (hitGo == crankTransform.gameObject || hitGo.transform.IsChildOf(crankTransform))) return true;
|
||
if (interactionCollider != null && hitGo == interactionCollider.gameObject) return true;
|
||
return false;
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 拖拽逻辑
|
||
|
||
private void StartDrag()
|
||
{
|
||
if (clearHighlightWhenDragStarts)
|
||
SetInteractionHighlightVisible(false);
|
||
|
||
_isDragging = true;
|
||
_dragStartPosition = Input.mousePosition;
|
||
_dragStartRotation = _currentRotation;
|
||
_dragStartAngle = GetMouseAngle();
|
||
_lastMouseAngle = _dragStartAngle;
|
||
_hasTriggered = false;
|
||
|
||
if (_returnTween != null)
|
||
{
|
||
_returnTween.Kill();
|
||
_returnTween = null;
|
||
}
|
||
if (_spinTween != null)
|
||
{
|
||
_spinTween.Kill();
|
||
_spinTween = null;
|
||
_spinAngleOffset = 0f;
|
||
_isSpinning = false;
|
||
}
|
||
_isReturning = false;
|
||
|
||
// 拖拽缩放效果
|
||
if (_scaleTween != null) _scaleTween.Kill();
|
||
_scaleTween = crankTransform.DOScale(_initialScale * dragScale, scaleDuration)
|
||
.SetEase(Ease.OutQuad);
|
||
|
||
onCrankStartDrag?.Invoke();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取鼠标相对环中心的角度(0-360)
|
||
/// </summary>
|
||
private float GetMouseAngle(Vector3? screenPos = null)
|
||
{
|
||
Vector3 pivotWorld = GetRotationPivot();
|
||
Vector3 screenCenter = _mainCamera.WorldToScreenPoint(pivotWorld);
|
||
Vector3 pos = screenPos ?? Input.mousePosition;
|
||
float a = Mathf.Atan2(pos.y - screenCenter.y, pos.x - screenCenter.x) * Mathf.Rad2Deg;
|
||
return a < 0f ? a + 360f : a;
|
||
}
|
||
|
||
private void ContinueDrag(Vector3 screenPosition)
|
||
{
|
||
if (_isCompleting) return;
|
||
|
||
// 每帧累计角度增量,支持超过 180° 的拖拽(原先用总 delta 会在 180° 处反转)
|
||
float currentAngle = GetMouseAngle(screenPosition);
|
||
float frameDelta = currentAngle - _lastMouseAngle;
|
||
|
||
// 仅对单帧小增量做 0/360 边界 wrap
|
||
if (frameDelta > 180f) frameDelta -= 360f;
|
||
else if (frameDelta < -180f) frameDelta += 360f;
|
||
|
||
_lastMouseAngle = currentAngle;
|
||
|
||
if (clockwisePositive) frameDelta = -frameDelta;
|
||
|
||
float targetRotation = _currentRotation + frameDelta * (1f - dragResistance);
|
||
|
||
// 拖拽一帧跨过起点/终点时直接夹到边界。
|
||
// 若拒绝越界的整帧移动,当触发角度等于最大角度时就可能永远到不了终点。
|
||
_currentRotation = Mathf.Clamp(targetRotation, 0f, maxRotationAngle);
|
||
|
||
ApplyCrankTransform(_currentRotation);
|
||
|
||
float normalizedRotation = _currentRotation / maxRotationAngle;
|
||
onCrankRotate?.Invoke(normalizedRotation);
|
||
|
||
// 检查是否达到触发角度
|
||
if (_currentRotation >= triggerAngle && !_hasTriggered)
|
||
{
|
||
_hasTriggered = true;
|
||
Debug.Log($"[CrankController] 达到触发角度!当前角度: {_currentRotation:F1}°, 触发角度: {triggerAngle}°", this);
|
||
BeginAutoComplete();
|
||
}
|
||
}
|
||
|
||
private void BeginAutoComplete()
|
||
{
|
||
if (_isCompleting) return;
|
||
|
||
_isCompleting = true;
|
||
_isDragging = false;
|
||
_isReturning = false;
|
||
_failedAttemptCount = 0;
|
||
_failedAttemptHintShown = false;
|
||
|
||
_scaleTween?.Kill();
|
||
_scaleTween = crankTransform.DOScale(_initialScale, scaleDuration).SetEase(Ease.OutQuad);
|
||
|
||
_completeTween?.Kill();
|
||
_completeTween = DOTween.To(
|
||
() => _currentRotation,
|
||
value =>
|
||
{
|
||
_currentRotation = value;
|
||
ApplyCrankTransform(_currentRotation);
|
||
onCrankRotate?.Invoke(_currentRotation / maxRotationAngle);
|
||
},
|
||
maxRotationAngle,
|
||
Mathf.Max(0.01f, completionSnapDuration))
|
||
// OutBack 会超过 maxRotationAngle,导致实际画面角度大于配置上限。
|
||
.SetEase(Ease.OutCubic)
|
||
.OnComplete(() =>
|
||
{
|
||
_completeTween = null;
|
||
_isCompleting = false;
|
||
onCrankComplete?.Invoke();
|
||
});
|
||
}
|
||
|
||
private void EndDrag()
|
||
{
|
||
if (_isCompleting) return;
|
||
_isDragging = false;
|
||
|
||
// 恢复缩放
|
||
if (_scaleTween != null) _scaleTween.Kill();
|
||
_scaleTween = crankTransform.DOScale(_initialScale, scaleDuration)
|
||
.SetEase(Ease.OutQuad);
|
||
|
||
if (!_hasTriggered || _currentRotation < triggerAngle)
|
||
{
|
||
_failedAttemptCount++;
|
||
Debug.Log($"[CrankController] EndDrag 未触发:hasTriggered={_hasTriggered}, currentRotation={_currentRotation:F1}°, triggerAngle={triggerAngle}°", this);
|
||
|
||
if (!_failedAttemptHintShown && _failedAttemptCount >= Mathf.Max(1, failedAttemptHintThreshold))
|
||
{
|
||
_failedAttemptHintShown = true;
|
||
if (!string.IsNullOrEmpty(failedAttemptHintNode))
|
||
DialogController.Instance?.StartDialogNode(failedAttemptHintNode);
|
||
}
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 回位逻辑
|
||
|
||
private void HandleReturn()
|
||
{
|
||
if (_isCompleting) return;
|
||
|
||
if (!_isDragging && autoReturn && _currentRotation > 0f && !_isReturning && !_isSpinning)
|
||
{
|
||
_isReturning = true;
|
||
_returnSpeedPerSec = maxRotationAngle / Mathf.Max(0.01f, returnDuration);
|
||
}
|
||
|
||
if (_isReturning && !_isDragging)
|
||
{
|
||
float step = _returnSpeedPerSec * Time.deltaTime;
|
||
_currentRotation = Mathf.MoveTowards(_currentRotation, 0f, step);
|
||
ApplyCrankTransform(_currentRotation);
|
||
onCrankRotate?.Invoke(_currentRotation / maxRotationAngle);
|
||
|
||
if (_currentRotation <= 0f)
|
||
{
|
||
_currentRotation = 0f;
|
||
_isReturning = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 公共方法
|
||
|
||
public void ResetCrank()
|
||
{
|
||
if (_returnTween != null)
|
||
{
|
||
_returnTween.Kill();
|
||
_returnTween = null;
|
||
}
|
||
if (_spinTween != null)
|
||
{
|
||
_spinTween.Kill();
|
||
_spinTween = null;
|
||
}
|
||
if (_completeTween != null)
|
||
{
|
||
_completeTween.Kill();
|
||
_completeTween = null;
|
||
}
|
||
_spinAngleOffset = 0f;
|
||
_isSpinning = false;
|
||
_isReturning = false;
|
||
_isCompleting = false;
|
||
|
||
_isDragging = false;
|
||
_hasTriggered = false;
|
||
ResetFailedAttempts();
|
||
_currentRotation = 0f;
|
||
|
||
if (crankTransform != null)
|
||
{
|
||
ApplyCrankTransform(0f);
|
||
crankTransform.localScale = _initialScale;
|
||
}
|
||
}
|
||
|
||
private void ResetFailedAttempts()
|
||
{
|
||
_failedAttemptCount = 0;
|
||
_failedAttemptHintShown = false;
|
||
}
|
||
|
||
private void OnDestroy()
|
||
{
|
||
_scaleTween?.Kill();
|
||
_returnTween?.Kill();
|
||
_spinTween?.Kill();
|
||
_completeTween?.Kill();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 逆时针转一圈的表现(可被 Yarn 命令、事件等触发)
|
||
/// </summary>
|
||
/// <param name="duration">动画时长,不传则使用 spinOnceDuration</param>
|
||
public void PlaySpinCounterClockwise(float? duration = null)
|
||
{
|
||
float d = duration ?? spinOnceDuration;
|
||
if (d <= 0f) d = 0.5f;
|
||
|
||
if (_spinTween != null)
|
||
{
|
||
_spinTween.Kill();
|
||
_spinTween = null;
|
||
}
|
||
|
||
_spinAngleOffset = 0f;
|
||
_isSpinning = true;
|
||
|
||
_spinTween = DOTween.To(
|
||
() => _spinAngleOffset,
|
||
value =>
|
||
{
|
||
_spinAngleOffset = value;
|
||
ApplyCrankTransform(_currentRotation);
|
||
},
|
||
360f,
|
||
d
|
||
).SetEase(Ease.Linear).OnComplete(() =>
|
||
{
|
||
_spinAngleOffset = 0f;
|
||
_spinTween = null;
|
||
_isSpinning = false;
|
||
ApplyCrankTransform(_currentRotation);
|
||
onSpinOnceComplete?.Invoke();
|
||
});
|
||
}
|
||
|
||
/// <summary>
|
||
/// 手动旋转到指定角度(用于动画或程序化控制)
|
||
/// </summary>
|
||
public void RotateTo(float angle, float duration = 0.5f)
|
||
{
|
||
angle = Mathf.Clamp(angle, 0f, maxRotationAngle);
|
||
|
||
if (_returnTween != null)
|
||
{
|
||
_returnTween.Kill();
|
||
_returnTween = null;
|
||
}
|
||
|
||
DOTween.To(
|
||
() => _currentRotation,
|
||
value => {
|
||
_currentRotation = value;
|
||
ApplyCrankTransform(_currentRotation);
|
||
float normalizedRotation = _currentRotation / maxRotationAngle;
|
||
onCrankRotate?.Invoke(normalizedRotation);
|
||
},
|
||
angle,
|
||
duration
|
||
).SetEase(Ease.OutCubic);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前旋转角度(0-1标准化)
|
||
/// </summary>
|
||
public float GetNormalizedRotation()
|
||
{
|
||
return _currentRotation / maxRotationAngle;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前旋转角度(度)
|
||
/// </summary>
|
||
public float GetCurrentAngle()
|
||
{
|
||
return _currentRotation;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 是否正在拖拽中
|
||
/// </summary>
|
||
public bool IsDragging => _isDragging;
|
||
|
||
/// <summary>
|
||
/// 是否已达到触发角度
|
||
/// </summary>
|
||
public bool HasTriggered => _hasTriggered;
|
||
|
||
/// <summary>
|
||
/// 是否正在播放逆时针转圈
|
||
/// </summary>
|
||
public bool IsSpinning => _isSpinning;
|
||
|
||
/// <summary>
|
||
/// 回位时长(秒),供滑片等配合使用
|
||
/// </summary>
|
||
public float ReturnDuration => returnDuration;
|
||
|
||
/// <summary>
|
||
/// 设置回位时长(由 SalesManager 同步每次转动时间)
|
||
/// </summary>
|
||
public void SetReturnDuration(float duration)
|
||
{
|
||
returnDuration = Mathf.Max(0.01f, duration);
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region IInteraction(EventTriggerEx 统一交互)
|
||
|
||
public bool IsActive => true;
|
||
|
||
public bool IsAvailable => true;
|
||
|
||
public GameObject GetGameObject() => gameObject;
|
||
|
||
#endregion
|
||
}
|
||
|
||
/// <summary>
|
||
/// 发条交互桥接:挂在 Collider 所在子物体上,将 IInteraction 转交给父级 CrankController
|
||
/// </summary>
|
||
public class CrankInteractionBridge : MonoBehaviour, IInteraction
|
||
{
|
||
private CrankController _controller;
|
||
|
||
private void Awake()
|
||
{
|
||
_controller = GetComponentInParent<CrankController>();
|
||
}
|
||
|
||
public bool IsActive => _controller?.IsActive ?? true;
|
||
public bool IsAvailable => _controller?.IsAvailable ?? true;
|
||
public GameObject GetGameObject() => gameObject;
|
||
}
|
||
}
|