Files
Echo/Assets/_Project/Scripts/Interaction/ToggleRotateInteractable.cs
T
2026-07-08 20:42:51 +08:00

219 lines
6.3 KiB
C#

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<bool> 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;
}
}
}