using System; using UnityEngine; using UnityEngine.Events; using DG.Tweening; namespace Interaction { public class ToggleRotateInteractable : MonoBehaviour, IInteractable { public enum ToggleState { Off = 0, On = 1 } public enum InitialStateMode { Auto = 0, ForceOff = 1, ForceOn = 2 } [Serializable] public class RotationTarget { public Transform target; [Header("轴选择")] public bool useX = true; public bool useY = true; public bool useZ = true; [Header("关闭状态 (Off)")] public Vector3 offEuler; [Header("开启状态 (On)")] public Vector3 onEuler; } [Header("目标设置")] [SerializeField] private RotationTarget[] targets; [Header("初始化")] [SerializeField] private InitialStateMode initialState = InitialStateMode.Auto; [SerializeField] private ToggleState state = ToggleState.Off; [Header("动画设置")] [SerializeField] private float duration = 1.0f; [SerializeField] private Ease animationEase = Ease.OutQuad; [SerializeField] private RotateMode rotateMode = RotateMode.Fast; [Header("交互提示")] [SerializeField] private string promptWhenOff = "打开"; [SerializeField] private string promptWhenOn = "关闭"; [Header("事件")] [SerializeField] private UnityEvent onTurnedOn; [SerializeField] private UnityEvent onTurnedOff; [SerializeField] private UnityEvent onStateChanged; public bool IsOn => state == ToggleState.On; private void Start() { if (targets == null || targets.Length == 0) { Debug.LogWarning($"[{nameof(ToggleRotateInteractable)}] 未绑定任何目标对象。"); return; } switch (initialState) { case InitialStateMode.ForceOn: state = ToggleState.On; break; case InitialStateMode.ForceOff: state = ToggleState.Off; break; default: state = DetectState(); break; } } public void Interact() { Toggle(true); } public void Toggle(bool animate = true) { SetState(IsOn ? ToggleState.Off : ToggleState.On, animate); } public void SetState(ToggleState newState, bool animate = true) { if (targets == null || targets.Length == 0) return; if (state == newState) return; state = newState; ApplyStateToTargets(animate); bool isOn = IsOn; onStateChanged?.Invoke(isOn); if (isOn) onTurnedOn?.Invoke(); else onTurnedOff?.Invoke(); } public string GetInteractPrompt() { return IsOn ? promptWhenOn : promptWhenOff; } private void ApplyStateToTargets(bool animate) { foreach (var t in targets) { if (t == null || t.target == null) continue; Vector3 desired = BuildDesiredEuler(t, IsOn ? t.onEuler : t.offEuler); t.target.DOKill(); if (!animate || duration <= 0f) { t.target.localEulerAngles = desired; continue; } t.target.DOLocalRotate(desired, duration, rotateMode) .SetEase(animationEase); } } private ToggleState DetectState() { float distOn = 0f; float distOff = 0f; bool hasAny = false; foreach (var t in targets) { if (t == null || t.target == null) continue; hasAny = true; Vector3 current = NormalizeEulerSigned(t.target.localEulerAngles); bool useX = t.useX; bool useY = t.useY; bool useZ = t.useZ; if (!useX && !useY && !useZ) { useX = true; useY = true; useZ = true; } if (useX) { distOn += Mathf.Abs(Mathf.DeltaAngle(current.x, t.onEuler.x)); distOff += Mathf.Abs(Mathf.DeltaAngle(current.x, t.offEuler.x)); } if (useY) { distOn += Mathf.Abs(Mathf.DeltaAngle(current.y, t.onEuler.y)); distOff += Mathf.Abs(Mathf.DeltaAngle(current.y, t.offEuler.y)); } if (useZ) { distOn += Mathf.Abs(Mathf.DeltaAngle(current.z, t.onEuler.z)); distOff += Mathf.Abs(Mathf.DeltaAngle(current.z, t.offEuler.z)); } } if (!hasAny) return ToggleState.Off; return distOn < distOff ? ToggleState.On : ToggleState.Off; } private Vector3 BuildDesiredEuler(RotationTarget t, Vector3 configured) { Vector3 current = NormalizeEulerSigned(t.target.localEulerAngles); bool useX = t.useX; bool useY = t.useY; bool useZ = t.useZ; if (!useX && !useY && !useZ) { useX = true; useY = true; useZ = true; } if (useX) current.x = configured.x; if (useY) current.y = configured.y; if (useZ) current.z = configured.z; return current; } private static Vector3 NormalizeEulerSigned(Vector3 euler) { euler.x = NormalizeAngleSigned(euler.x); euler.y = NormalizeAngleSigned(euler.y); euler.z = NormalizeAngleSigned(euler.z); return euler; } private static float NormalizeAngleSigned(float angle) { angle %= 360f; if (angle > 180f) angle -= 360f; if (angle < -180f) angle += 360f; return angle; } } }