using System.Collections; using System.Linq; using AibisDream; using AibisDream.Framework; using AibisDream.FixSystem; using DG.Tweening; using UnityEngine; using UnityEngine.Events; using UnityEngine.EventSystems; using Yarn.Unity; namespace AibisDream.MiniGame.HuoShan { /// /// 处理器旋钮控制器 — 完整移植 Web_EmotionWave glow-tube 分支的交互体验 /// 交互通过 EventTriggerEx 统一管理,支持 EventSystemEx.isLocked 和 IInteraction 状态 /// 核心特性:累计旋转、输入阻力、噪声颤抖、回弹前蓄力预兆、底座发热变色 /// 保留原有公共接口(事件、Yarn 命令、功率查询) /// 在旋钮 GameObject 上添加 EventTriggerEx + Collider2D 即可接入;未添加则回退到 Input+Raycast /// public class KnobController : MonoBehaviour, IInteraction { #region Serialized Fields [Header("旋钮 Transform")] [Tooltip("旋钮旋转的 Transform")] [SerializeField] private Transform knobTransform; [Tooltip("旋钮底座 SpriteRenderer(发热变色)")] [SerializeField] private SpriteRenderer knobPlate; [Tooltip("功率指示器 Transform(旋钮上的标记线)")] [SerializeField] private Transform powerIndicator; [Tooltip("用于检测点击的 Collider2D")] [SerializeField] private Collider2D interactionCollider; [Header("旋转参数")] [Tooltip("最大累计圈数")] [SerializeField] private int maxTurns = 10; [Tooltip("旋转方向:勾选则顺时针拧=降功率(与 Web 原型一致)")] [SerializeField] private bool clockwiseDecreasesPower = true; [Tooltip("停手后到回弹的酝酿时间 (秒)")] [SerializeField] private float idleDelay = 0.8f; [Tooltip("回弹基础时长 (秒)")] [SerializeField] private float snapbackDuration = 0.8f; [Header("辉光管")] [Tooltip("辉光管粒子效果(不填则从 SaleView 下查找)")] [SerializeField] private GlowTubeEffect glowTubeEffect; [Header("信息面板")] [SerializeField] private GameObject infoPanel; [SerializeField] private UnityEngine.UI.Text infoText; [SerializeField] private string failMessage = "表达模块拒绝了未被处理的波形"; [SerializeField] private float panelShowDuration = 0.3f; [SerializeField] private float panelHideDuration = 0.2f; [Header("Yarn")] [Tooltip("单次失败时跳转的 Yarn 节点(Yarn 根据 $knobFailCount 决定:重试 reset_knob 或 lock_knob 后跳 failNode)")] [SerializeField] private string singleFailNode = "处理器单次调节失败"; [Header("失败判定")] [Tooltip("旋转到最大角度的百分比后判定为失败(0-1)")] [SerializeField] private float failThreshold = 0.3f; [Header("旋钮盖子")] [Tooltip("旋钮盖子 Transform(步进模式/旋钮/盖子),不填则尝试自动查找")] [SerializeField] private Transform knobLid; [Tooltip("打开盖子时的位移偏移(LocalSpace)")] [SerializeField] private Vector3 lidOpenOffset = new Vector3(0f, 2f, 0f); [Tooltip("盖子开合动画时长")] [SerializeField] private float lidAnimDuration = 0.4f; [Header("环形进度条")] [Tooltip("灯带进度条 SpriteRenderer;不填则尝试自动查找")] [SerializeField] private SpriteRenderer ringProgressRenderer; [Tooltip("自动查找的灯带节点名")] [SerializeField] private string ringProgressChildName = "灯带进度条"; [Tooltip("环形填充起始角度,0=右,90=上")] [SerializeField] private float ringProgressStartAngle = 90f; [Tooltip("勾选则按顺时针方向填充")] [SerializeField] private bool ringProgressClockwise = true; [Header("事件")] public UnityEvent onPowerAdjustmentStart; public UnityEvent onPowerChanged; public UnityEvent onPowerAdjustmentFail; public UnityEvent onPowerAdjustmentSuccess; [Tooltip("回弹完成时触发")] public UnityEvent onSnapbackComplete; #endregion #region Private State private float _maxAngle; private float _knobAngle; private float _knobValue; private float _intensity = 1f; private float _displayIntensity = 1f; private bool _isDragging; private bool _isSnappingBack; private bool _snapbackLocked; // Tremor private float _tremorAmount; private float _tremorTarget; private float _seedA; private float _seedB; // Idle / warning private float _idleStartTime; private bool _isIdle; private Coroutine _idleCoroutine; // Overshoot private float _overshootIntensity; private float _overshootDecay; // Resistance pulse private float _resistancePulse; private float _nextPulseTime; private float _animTime; private float _prevMouseAngle; private Camera _mainCamera; private bool _hasFailed; private bool _hasSucceeded; private float _currentPower = 100f; private int _failCount; private bool _isLocked; private Color _plateOriginalColor; private Vector3 _lidClosedPosition; private Tweener _lidTween; private EventTriggerEx _eventTrigger; private bool _useEventTrigger; private Material _ringProgressMaterial; private const string RingProgressShaderName = "AibisDream/SpriteRadialProgress"; private const string RingProgressShaderResourcePath = "Shader/SpriteRadialProgress"; private static readonly int MainTexId = Shader.PropertyToID("_MainTex"); private static readonly int ProgressId = Shader.PropertyToID("_Progress"); private static readonly int StartAngleId = Shader.PropertyToID("_StartAngle"); private static readonly int FillClockwiseId = Shader.PropertyToID("_FillClockwise"); #endregion #region Lifecycle private void Awake() { _mainCamera = Camera.main; _maxAngle = 360f * maxTurns; _seedA = Random.Range(0f, 1000f); _seedB = Random.Range(0f, 1000f); if (knobTransform == null) knobTransform = transform; if (interactionCollider == null && knobTransform != null) interactionCollider = knobTransform.GetComponent(); if (knobPlate != null) _plateOriginalColor = knobPlate.color; if (infoPanel != null) infoPanel.SetActive(false); // 事件目标:Collider 所在的对象(可能与 KnobController 不同,如旋钮 visuals 在子物体) var eventTargetGo = interactionCollider != null ? interactionCollider.gameObject : (knobTransform != null ? knobTransform.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); } EnsureKnobLid(); if (knobLid != null) _lidClosedPosition = knobLid.localPosition; EnsureRingProgress(); } /// /// 查找盖子 Transform(步进模式/旋钮/盖子),未配置时自动按路径查找 /// private void EnsureKnobLid() { if (knobLid != null) return; var t = transform; while (t != null) { if (t.name == "SaleView" || t.name == "SellView") { knobLid = t.Find("步进模式/旋钮/盖子"); if (knobLid != null) Debug.Log("[KnobController] 自动找到盖子:步进模式/旋钮/盖子", this); break; } t = t.parent; } } private void EnsureRingProgress() { if (ringProgressRenderer == null && knobTransform != null && !string.IsNullOrEmpty(ringProgressChildName)) { var ring = knobTransform.Find(ringProgressChildName); if (ring != null) ringProgressRenderer = ring.GetComponent(); } if (ringProgressRenderer == null) return; Shader shader = Resources.Load(RingProgressShaderResourcePath); if (shader == null) shader = Shader.Find(RingProgressShaderName); if (shader == null) { #if UNITY_EDITOR Debug.LogWarning($"[KnobController] 未找到环形进度条 Shader:{RingProgressShaderName}", this); #endif return; } _ringProgressMaterial = new Material(shader) { name = $"{ringProgressRenderer.gameObject.name}_RingProgress_Mat" }; if (ringProgressRenderer.sprite != null) _ringProgressMaterial.SetTexture(MainTexId, ringProgressRenderer.sprite.texture); ringProgressRenderer.material = _ringProgressMaterial; ApplyRingProgressMaterialProperties(); UpdateRingProgress(_knobValue); } private void ApplyRingProgressMaterialProperties() { if (_ringProgressMaterial == null) return; _ringProgressMaterial.SetFloat(StartAngleId, ringProgressStartAngle); _ringProgressMaterial.SetFloat(FillClockwiseId, ringProgressClockwise ? 1f : 0f); } private void UpdateRingProgress(float progress) { if (_ringProgressMaterial == null) return; if (ringProgressRenderer != null && ringProgressRenderer.sprite != null) _ringProgressMaterial.SetTexture(MainTexId, ringProgressRenderer.sprite.texture); _ringProgressMaterial.SetFloat(ProgressId, Mathf.Clamp01(progress)); } 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 Start() { UpdateFromKnobAngle(); } private void Update() { float dt = Time.deltaTime; _animTime += dt; // 强制使用 EventTriggerEx,不使用 Input+Raycast 回退 // if (!_useEventTrigger) // HandleMouseInputFallback(); UpdateResistancePulse(dt); UpdateKnobVisuals(dt); UpdateGlowTube(); } #endregion #region Input(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); } private void HandleMouseInputFallback() { if (Input.GetMouseButtonDown(0)) { if (_snapbackLocked) return; Ray ray = _mainCamera.ScreenPointToRay(Input.mousePosition); RaycastHit2D hit = Physics2D.GetRayIntersection(ray, float.MaxValue); if (hit.collider != null && IsValidHit(hit.collider)) { StartDrag(); } } if (_isDragging) { if (Input.GetMouseButton(0)) ContinueDrag(Input.mousePosition); else EndDrag(); } } #endregion #region IInteraction(EventTriggerEx 统一交互) public bool IsActive => !_isLocked; /// /// 不可用时(锁定、回弹中、已失败/成功)EventTriggerEx 将拦截交互 /// public bool IsAvailable => !_isLocked && !_snapbackLocked && !_hasFailed && !_hasSucceeded; public GameObject GetGameObject() => gameObject; #endregion private bool IsValidHit(Collider2D col) { if (col == interactionCollider) return true; if (col.gameObject == gameObject) return true; if (knobTransform != null && col.transform.IsChildOf(knobTransform)) return true; return false; } private float GetMouseAngleDeg(Vector3? screenPos = null) { Vector3 screenCenter = _mainCamera.WorldToScreenPoint(knobTransform.position); Vector3 mp = screenPos ?? Input.mousePosition; return Mathf.Atan2(mp.y - screenCenter.y, mp.x - screenCenter.x) * Mathf.Rad2Deg; } /// 输入阻力:转得越多,鼠标驱动力越弱 private float GetInputResistance() { return 1f - _knobValue * 0.88f; } #region Drag private void StartDrag() { if (_hasFailed || _hasSucceeded || _isLocked) return; _isDragging = true; _prevMouseAngle = GetMouseAngleDeg(Input.mousePosition); ClearIdleState(); onPowerAdjustmentStart?.Invoke(); } private void ContinueDrag(Vector3 screenPosition) { if (_snapbackLocked || _hasFailed || _hasSucceeded || _isLocked) return; float cur = GetMouseAngleDeg(screenPosition); float delta = cur - _prevMouseAngle; if (delta > 180f) delta -= 360f; if (delta < -180f) delta += 360f; _prevMouseAngle = cur; if (clockwiseDecreasesPower) delta = -delta; delta *= GetInputResistance(); _knobAngle = Mathf.Clamp(_knobAngle + delta, 0f, _maxAngle); UpdateFromKnobAngle(); ClearIdleState(); StartIdleCountdown(); } private void EndDrag() { if (!_isDragging) return; _isDragging = false; if (_knobAngle > 10f) StartIdleCountdown(); } #endregion #region Core State private void UpdateFromKnobAngle() { _knobAngle = Mathf.Clamp(_knobAngle, 0f, _maxAngle); _knobValue = _knobAngle / _maxAngle; UpdateRingProgress(_knobValue); // Intensity: cubic falloff _intensity = Mathf.Pow(1f - _knobValue, 3f); _intensity = Mathf.Max(0.03f, _intensity); _tremorTarget = _knobValue * _knobValue * 3.5f; _currentPower = _intensity * 100f; onPowerChanged?.Invoke(_currentPower); // Fail check if (_knobValue >= failThreshold && !_hasFailed && !_hasSucceeded) { TriggerFailure(); } } #endregion #region Idle / Warning private void ClearIdleState() { if (_idleCoroutine != null) { StopCoroutine(_idleCoroutine); _idleCoroutine = null; } _isIdle = false; _idleStartTime = 0f; } private void StartIdleCountdown() { ClearIdleState(); _isIdle = true; _idleStartTime = Time.time; _idleCoroutine = StartCoroutine(IdleSnapbackRoutine()); } private IEnumerator IdleSnapbackRoutine() { yield return new WaitForSeconds(idleDelay); TriggerSnapback(); } private float GetWarningLevel() { if (!_isIdle || _knobAngle < 10f) return 0f; float elapsed = Time.time - _idleStartTime; return Mathf.Pow(Mathf.Min(1f, elapsed / idleDelay), 2.8f); } #endregion #region Snapback private void TriggerSnapback() { if (_knobAngle < 5f) return; _isSnappingBack = true; _snapbackLocked = true; _isIdle = false; StartCoroutine(SnapbackRoutine()); } private IEnumerator SnapbackRoutine() { float snapFrom = _knobAngle; float turns = snapFrom / 360f; float dur = Mathf.Clamp(snapbackDuration * (turns / 3f), 0.5f, 1.2f); float t0 = Time.time; while (true) { float elapsed = Time.time - t0; float p = Mathf.Clamp01(elapsed / dur); // Charge-then-burst easing float eased; if (p < 0.08f) { eased = p * 0.02f; } else { float t = (p - 0.08f) / 0.92f; eased = 0.0016f + 0.9984f * (1f - Mathf.Pow(1f - t, 5f)); } _knobAngle = snapFrom * (1f - eased); UpdateFromKnobAngle(); _tremorTarget = 10f + turns * 3f; if (p >= 1f) break; yield return null; } _knobAngle = 0f; UpdateFromKnobAngle(); _overshootIntensity = Mathf.Min(0.55f, 0.2f + turns * 0.05f); _overshootDecay = 0f; _isSnappingBack = false; _tremorTarget = 0f; if (glowTubeEffect != null) glowTubeEffect.TriggerPulse(_overshootIntensity); yield return new WaitForSeconds(0.4f); _snapbackLocked = false; // 若是失败触发的回弹,回弹完成后再触发失败事件和 UI if (_hasFailed) { ShowInfoPanel(failMessage); onPowerAdjustmentFail?.Invoke(_currentPower); if (!string.IsNullOrEmpty(singleFailNode)) DialogController.Instance?.StartDialogNode(singleFailNode); Debug.Log($"[KnobController] Snapback complete, failure events triggered"); } onSnapbackComplete?.Invoke(); } #endregion #region Visuals private void UpdateKnobVisuals(float dt) { // Smooth tremor _tremorAmount += (_tremorTarget - _tremorAmount) * Mathf.Min(1f, dt * 14f); float warning = GetWarningLevel(); float warningTremor = warning * warning * 15f; float totalTremor = _tremorAmount + warningTremor; // Rotation + tremor offset float tremor = 0f; if (totalTremor > 0.05f) { tremor = (Mathf.PerlinNoise(_animTime * 50f + _seedA, 0f) * 2f - 1f) * 0.6f + (Mathf.PerlinNoise(_animTime * 120f + _seedB, 7f) * 2f - 1f) * 0.4f; tremor *= totalTremor; } knobTransform.localRotation = Quaternion.Euler(0f, 0f, -(_knobAngle + tremor)); // Plate heat coloring if (knobPlate != null) { float pressure = _knobValue; float warmth = Mathf.Min(1f, pressure + warning * 0.4f); if (warmth > 0.03f) { float r = Mathf.Lerp(_plateOriginalColor.r, 1f, warmth * 0.6f); float g = Mathf.Lerp(_plateOriginalColor.g, _plateOriginalColor.g * 0.7f, warmth * 0.5f); float b = Mathf.Lerp(_plateOriginalColor.b, _plateOriginalColor.b * 0.5f, warmth * 0.6f); float a = _plateOriginalColor.a; knobPlate.color = new Color(r, g, b, a); } else { knobPlate.color = _plateOriginalColor; } } } private void UpdateResistancePulse(float dt) { if (_intensity < 0.5f && _knobAngle > 100f && !_isSnappingBack) { float suppression = 1f - _intensity; if (_animTime > _nextPulseTime) { _resistancePulse = Mathf.Max(_resistancePulse, suppression * (0.25f + Random.value * 0.5f)); _nextPulseTime = _animTime + 0.2f + Random.value * (2.5f - suppression * 2f); } } float warnPulse = GetWarningLevel(); if (warnPulse > 0.2f) { float wp = warnPulse * 0.5f * (0.5f + 0.5f * Mathf.Sin(_animTime * 18f * warnPulse)); if (wp > 0f) _resistancePulse = Mathf.Max(_resistancePulse, wp); } _resistancePulse *= Mathf.Exp(-dt * 9f); if (_resistancePulse < 0.008f) _resistancePulse = 0f; } private void UpdateGlowTube() { _overshootDecay += Time.deltaTime * 3f; float overshoot = _overshootIntensity * Mathf.Exp(-_overshootDecay * 2.5f) * (1f + Mathf.Sin(_overshootDecay * 40f) * 0.5f * Mathf.Exp(-_overshootDecay * 1.5f)); _displayIntensity = _intensity + _resistancePulse + Mathf.Max(0f, overshoot); _displayIntensity = Mathf.Min(1.5f, _displayIntensity); if (glowTubeEffect != null) { glowTubeEffect.SetIntensity(_displayIntensity); if (_resistancePulse > 0.1f) glowTubeEffect.TriggerPulse(_resistancePulse * 0.5f); } } #endregion #region Failure / Info Panel private void TriggerFailure() { _failCount++; StorageSystem.Instance?.SetValue("$knobFailCount", (float)_failCount); _hasFailed = true; Debug.Log($"[KnobController] Failure #{_failCount} at {_currentPower:F1}%, waiting for snapback..."); // 不立即触发 UI/事件,等旋钮回弹完成后再触发 TriggerSnapback(); } private void ShowInfoPanel(string message) { if (infoPanel == null || infoText == null) return; infoText.text = message; infoPanel.SetActive(true); var rt = infoPanel.GetComponent(); if (rt != null) { Vector3 orig = rt.anchoredPosition; rt.anchoredPosition = orig + Vector3.up * 100f; rt.DOAnchorPos(orig, panelShowDuration).SetEase(Ease.OutCubic); } } private void HideInfoPanel() { if (infoPanel == null) return; var rt = infoPanel.GetComponent(); if (rt != null) { var end = rt.anchoredPosition + Vector2.up * 100f; rt.DOAnchorPos(end, panelHideDuration) .SetEase(Ease.InCubic) .OnComplete(() => infoPanel.SetActive(false)); } else { infoPanel.SetActive(false); } } #endregion #region Yarn Commands [YarnCommand("adjust_power")] public static IEnumerator AdjustPower(float targetPower) { var ctrl = FixSystemCenter.SystemDic.Get(); if (ctrl == null) ctrl = FindObjectOfType(); if (ctrl != null) { float targetAngle = (1f - Mathf.Clamp01(targetPower / 100f)) * ctrl._maxAngle; ctrl._knobAngle = targetAngle; ctrl.UpdateFromKnobAngle(); yield return new WaitForSeconds(1f); } } [YarnCommand("reset_knob")] public static void ResetKnob() { var ctrl = FixSystemCenter.SystemDic.Get(); if (ctrl == null) ctrl = FindObjectOfType(); ctrl?.Reset(clearFailCount: false); } /// /// 锁定旋钮(交互禁止)。由 Yarn 在达到允许重试次数后调用。 /// 用法: <> /// [YarnCommand("lock_knob")] public static void LockKnob() { var ctrl = FixSystemCenter.SystemDic.Get(); if (ctrl == null) ctrl = FindObjectOfType(); ctrl?.Lock(); } /// /// 解锁旋钮(交互解禁)。 /// 用法: <> /// [YarnCommand("unlock_knob")] public static void UnlockKnob() { var ctrl = FixSystemCenter.SystemDic.Get(); if (ctrl == null) ctrl = FindObjectOfType(); ctrl?.Unlock(); } #endregion #region Public API /// /// 打开旋钮盖子(DOTween 平移动画) /// public void OpenLid(float duration = 0f) { if (knobLid == null) return; float d = duration > 0f ? duration : lidAnimDuration; if (_lidTween != null) _lidTween.Kill(); Vector3 openPos = _lidClosedPosition + lidOpenOffset; _lidTween = knobLid.DOLocalMove(openPos, d).SetEase(Ease.OutCubic) .OnKill(() => _lidTween = null); } /// /// 盖上旋钮盖子(DOTween 平移回位) /// public void CloseLid(float duration = 0f) { if (knobLid == null) return; float d = duration > 0f ? duration : lidAnimDuration; if (_lidTween != null) _lidTween.Kill(); _lidTween = knobLid.DOLocalMove(_lidClosedPosition, d).SetEase(Ease.OutCubic) .OnKill(() => _lidTween = null); } public float GetCurrentPower() => _currentPower; public bool HasFailed() => _hasFailed; public float KnobValue => _knobValue; public float DisplayIntensity => _displayIntensity; /// /// 重置旋钮状态。clearFailCount=true 时同时清零失败次数(如进入 processor 模式时)。 /// public void Reset(bool clearFailCount = true) { ClearIdleState(); _hasFailed = false; _hasSucceeded = false; _isDragging = false; _isSnappingBack = false; _snapbackLocked = false; _knobAngle = 0f; _tremorAmount = 0f; _tremorTarget = 0f; _resistancePulse = 0f; _overshootIntensity = 0f; _isLocked = false; if (clearFailCount) _failCount = 0; UpdateFromKnobAngle(); if (knobPlate != null) knobPlate.color = _plateOriginalColor; HideInfoPanel(); Debug.Log("[KnobController] Reset"); } /// /// 交互禁止:锁定旋钮,禁止用户操作。由 Yarn 在达到允许重试次数后调用。 /// public void Lock() { _isLocked = true; Debug.Log("[KnobController] Locked"); } /// /// 交互解禁:解锁旋钮。通常由 Reset 内部调用,也可单独调用。 /// public void Unlock() { _isLocked = false; } public void SetMinAcceptablePower(float minPower) { failThreshold = 1f - Mathf.Clamp01(minPower / 100f); } public void ForceSuccess() { _hasSucceeded = true; onPowerAdjustmentSuccess?.Invoke(_currentPower); } private void OnDestroy() { if (_ringProgressMaterial == null) return; if (Application.isPlaying) Destroy(_ringProgressMaterial); else DestroyImmediate(_ringProgressMaterial); } #endregion } /// /// 旋钮交互桥接:挂在 Collider 所在子物体上,将 IInteraction 转交给父级 KnobController /// public class KnobInteractionBridge : MonoBehaviour, IInteraction { private KnobController _controller; private void Awake() { _controller = GetComponentInParent(); } public bool IsActive => _controller?.IsActive ?? true; public bool IsAvailable => _controller?.IsAvailable ?? true; public GameObject GetGameObject() => gameObject; } }