using System.Linq; using AibisDream.Framework; using UnityEngine; using UnityEngine.Events; using UnityEngine.EventSystems; using DG.Tweening; using System.Collections; namespace AibisDream.MiniGame.HuoShan { /// /// 发条控制器 - 实现发条的拖拽旋转交互 /// 功能:拖拽旋转,最大260°,缓慢回位,触发事件 /// 交互通过 EventTriggerEx 统一管理,支持 EventSystemEx.isLocked 和 IInteraction 状态 /// 在发条 GameObject 上添加 EventTriggerEx + Collider2D 即可接入;未添加则回退到 Input+Raycast /// 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 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(); #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(); if (_eventTrigger == null && eventTargetGo.GetComponent() != null) { if (eventTargetGo != gameObject) { var bridge = eventTargetGo.GetComponent(); if (bridge == null) eventTargetGo.AddComponent(); } _eventTrigger = eventTargetGo.AddComponent(); } 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() ?? crankTransform.GetComponentInChildren(true); } /// /// 控制发条交互提示高亮(材质 _ShineFade)。由外部脚本、Yarn 命令等在合适时机调用。 /// 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); } /// /// 确保 EventTrigger 有需要的触发类型(若使用 EventTriggerEx 管理交互) /// 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); } /// /// 回退:无 EventTriggerEx 时使用 Input + Raycast(如 prefab 未迁移场景) /// 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(); } } } /// /// 获取旋转中心(环中心优先,否则用发条父节点) /// 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; } /// /// 应用发条的位置和旋转。配置了 ringCenter 时沿环轨道移动,否则原地旋转。 /// 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); } } /// /// 检查点击目标是否有效(本物体、发条、交互碰撞体或其子物体) /// 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(); } /// /// 获取鼠标相对环中心的角度(0-360) /// 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(); } /// /// 逆时针转一圈的表现(可被 Yarn 命令、事件等触发) /// /// 动画时长,不传则使用 spinOnceDuration 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(); }); } /// /// 手动旋转到指定角度(用于动画或程序化控制) /// 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); } /// /// 获取当前旋转角度(0-1标准化) /// public float GetNormalizedRotation() { return _currentRotation / maxRotationAngle; } /// /// 获取当前旋转角度(度) /// public float GetCurrentAngle() { return _currentRotation; } /// /// 是否正在拖拽中 /// public bool IsDragging => _isDragging; /// /// 是否已达到触发角度 /// public bool HasTriggered => _hasTriggered; /// /// 是否正在播放逆时针转圈 /// public bool IsSpinning => _isSpinning; /// /// 回位时长(秒),供滑片等配合使用 /// public float ReturnDuration => returnDuration; /// /// 设置回位时长(由 SalesManager 同步每次转动时间) /// 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 } /// /// 发条交互桥接:挂在 Collider 所在子物体上,将 IInteraction 转交给父级 CrankController /// public class CrankInteractionBridge : MonoBehaviour, IInteraction { private CrankController _controller; private void Awake() { _controller = GetComponentInParent(); } public bool IsActive => _controller?.IsActive ?? true; public bool IsAvailable => _controller?.IsAvailable ?? true; public GameObject GetGameObject() => gameObject; } }