219 lines
6.3 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|