255 lines
8.9 KiB
C#
255 lines
8.9 KiB
C#
|
|
using UnityEngine;
|
||
|
|
using DG.Tweening;
|
||
|
|
using UnityEngine.SceneManagement;
|
||
|
|
|
||
|
|
namespace Player
|
||
|
|
{
|
||
|
|
public class CameraLookAtTarget : MonoBehaviour
|
||
|
|
{
|
||
|
|
public enum RotationSpace
|
||
|
|
{
|
||
|
|
Local = 0,
|
||
|
|
World = 1
|
||
|
|
}
|
||
|
|
|
||
|
|
[Header("目标")]
|
||
|
|
[Tooltip("需要被追踪/看向的目标物体。留空可使用“自动按 Tag 寻找”。")]
|
||
|
|
[SerializeField] private GameObject target;
|
||
|
|
[Tooltip("当 Target 为空时,自动在场景中按 Tag 寻找目标(适合预设体跨场景复用)。")]
|
||
|
|
[SerializeField] private bool autoFindTargetByTag = true;
|
||
|
|
[Tooltip("用于自动寻找的 Tag(例如 Player)。要求目标物体在场景里正确设置 Tag。")]
|
||
|
|
[SerializeField] private string targetTag = "Player";
|
||
|
|
[Tooltip("自动寻找的间隔秒数,避免每帧 Find。")]
|
||
|
|
[SerializeField] private float findInterval = 0.5f;
|
||
|
|
|
||
|
|
[Header("旋转对象")]
|
||
|
|
[Tooltip("实际需要旋转的子物体(例如相机 Pivot)。其 Left(-X) 方向将对准目标。不填则默认使用当前脚本所在物体。")]
|
||
|
|
[SerializeField] private Transform rotationTarget;
|
||
|
|
|
||
|
|
[Header("距离触发")]
|
||
|
|
[Tooltip("开启后:只有在 Max Distance 距离内才会朝向目标。")]
|
||
|
|
[SerializeField] private bool useDistance = true;
|
||
|
|
[Tooltip("距离阈值(单位:米)。")]
|
||
|
|
[SerializeField] private float maxDistance = 5f;
|
||
|
|
|
||
|
|
[Header("更新与空间")]
|
||
|
|
[Tooltip("旋转使用本地(Local)还是世界(World)空间。相机/子物体在父物体下面时通常用 Local。")]
|
||
|
|
[SerializeField] private RotationSpace rotationSpace = RotationSpace.Local;
|
||
|
|
[Tooltip("更新间隔秒数。0=每帧 LateUpdate;例如 0.05=每秒20次。")]
|
||
|
|
[SerializeField] private float updateInterval = 0f;
|
||
|
|
[Tooltip("世界 Up 方向,通常保持 Vector3.up。")]
|
||
|
|
[SerializeField] private Vector3 worldUp = Vector3.up;
|
||
|
|
|
||
|
|
[Header("平滑旋转 (DOTween)")]
|
||
|
|
[Tooltip("看向目标时的旋转过渡时间(秒)。0=瞬间转过去。")]
|
||
|
|
[SerializeField] private float tweenDuration = 0.15f;
|
||
|
|
[Tooltip("看向目标时的缓动曲线。")]
|
||
|
|
[SerializeField] private Ease tweenEase = Ease.OutSine;
|
||
|
|
[Tooltip("角度小于该值(度)则不触发新的 tween,减少抖动和频繁 Kill。")]
|
||
|
|
[SerializeField] private float minAngleToTween = 0.2f;
|
||
|
|
[Tooltip("是否使用不受 Time.timeScale 影响的时间(暂停/慢放时有用)。同时也会影响 Update Interval 与自动寻找的计时。")]
|
||
|
|
[SerializeField] private bool useUnscaledTime = false;
|
||
|
|
|
||
|
|
private Tween currentTween;
|
||
|
|
private float nextUpdateTime;
|
||
|
|
private float nextFindTime;
|
||
|
|
|
||
|
|
public GameObject Target
|
||
|
|
{
|
||
|
|
get => target;
|
||
|
|
set => target = value;
|
||
|
|
}
|
||
|
|
|
||
|
|
public Transform RotationTarget
|
||
|
|
{
|
||
|
|
get => rotationTarget;
|
||
|
|
set => rotationTarget = value;
|
||
|
|
}
|
||
|
|
|
||
|
|
private void Awake()
|
||
|
|
{
|
||
|
|
if (rotationTarget == null)
|
||
|
|
rotationTarget = transform;
|
||
|
|
}
|
||
|
|
|
||
|
|
private void OnEnable()
|
||
|
|
{
|
||
|
|
SceneManager.sceneLoaded += OnSceneLoaded;
|
||
|
|
TryAutoResolveTarget(force: true);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void OnDisable()
|
||
|
|
{
|
||
|
|
SceneManager.sceneLoaded -= OnSceneLoaded;
|
||
|
|
|
||
|
|
if (currentTween != null && currentTween.IsActive())
|
||
|
|
currentTween.Kill();
|
||
|
|
}
|
||
|
|
|
||
|
|
private void OnValidate()
|
||
|
|
{
|
||
|
|
if (maxDistance < 0f) maxDistance = 0f;
|
||
|
|
if (findInterval < 0f) findInterval = 0f;
|
||
|
|
if (updateInterval < 0f) updateInterval = 0f;
|
||
|
|
if (tweenDuration < 0f) tweenDuration = 0f;
|
||
|
|
if (minAngleToTween < 0f) minAngleToTween = 0f;
|
||
|
|
}
|
||
|
|
|
||
|
|
private void LateUpdate()
|
||
|
|
{
|
||
|
|
TryAutoResolveTarget(force: false);
|
||
|
|
|
||
|
|
if (updateInterval > 0f)
|
||
|
|
{
|
||
|
|
float now = useUnscaledTime ? Time.unscaledTime : Time.time;
|
||
|
|
if (now < nextUpdateTime) return;
|
||
|
|
nextUpdateTime = now + updateInterval;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool inRange = target != null;
|
||
|
|
if (inRange && useDistance)
|
||
|
|
{
|
||
|
|
Vector3 origin = rotationTarget != null ? rotationTarget.position : transform.position;
|
||
|
|
float sqr = (target.transform.position - origin).sqrMagnitude;
|
||
|
|
float maxSqr = maxDistance * maxDistance;
|
||
|
|
inRange = sqr <= maxSqr;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (inRange)
|
||
|
|
{
|
||
|
|
Quaternion desired = GetDesiredRotationToTarget(target.transform);
|
||
|
|
TweenToRotation(desired, tweenDuration, tweenEase);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private Quaternion GetDesiredRotationToTarget(Transform t)
|
||
|
|
{
|
||
|
|
Vector3 origin = rotationTarget != null ? rotationTarget.position : transform.position;
|
||
|
|
Vector3 dir = t.position - origin;
|
||
|
|
if (dir.sqrMagnitude < 0.000001f)
|
||
|
|
return rotationSpace == RotationSpace.Local ? rotationTarget.localRotation : rotationTarget.rotation;
|
||
|
|
|
||
|
|
Quaternion worldRot = GetWorldRotation(dir, worldUp);
|
||
|
|
if (rotationSpace == RotationSpace.World)
|
||
|
|
return worldRot;
|
||
|
|
|
||
|
|
if (rotationTarget.parent == null)
|
||
|
|
return worldRot;
|
||
|
|
|
||
|
|
return Quaternion.Inverse(rotationTarget.parent.rotation) * worldRot;
|
||
|
|
}
|
||
|
|
|
||
|
|
private void TryAutoResolveTarget(bool force)
|
||
|
|
{
|
||
|
|
if (!autoFindTargetByTag) return;
|
||
|
|
|
||
|
|
if (target != null)
|
||
|
|
{
|
||
|
|
if (!target.scene.IsValid() || !target.scene.isLoaded)
|
||
|
|
{
|
||
|
|
target = null;
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
float now = Time.realtimeSinceStartup;
|
||
|
|
if (!force)
|
||
|
|
{
|
||
|
|
if (now < nextFindTime) return;
|
||
|
|
}
|
||
|
|
|
||
|
|
nextFindTime = now + findInterval;
|
||
|
|
target = ResolveByTag();
|
||
|
|
}
|
||
|
|
|
||
|
|
private GameObject ResolveByTag()
|
||
|
|
{
|
||
|
|
if (string.IsNullOrWhiteSpace(targetTag))
|
||
|
|
return null;
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
GameObject go = GameObject.FindGameObjectWithTag(targetTag);
|
||
|
|
if (go != null) return go;
|
||
|
|
}
|
||
|
|
catch
|
||
|
|
{
|
||
|
|
}
|
||
|
|
|
||
|
|
Transform[] all = FindObjectsOfType<Transform>(true);
|
||
|
|
for (int i = 0; i < all.Length; i++)
|
||
|
|
{
|
||
|
|
GameObject go = all[i].gameObject;
|
||
|
|
if (!go.scene.IsValid() || !go.scene.isLoaded) continue;
|
||
|
|
if (go.CompareTag(targetTag))
|
||
|
|
return go;
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
private Quaternion GetWorldRotation(Vector3 dir, Vector3 up)
|
||
|
|
{
|
||
|
|
Vector3 left = dir.normalized;
|
||
|
|
Vector3 right = -left;
|
||
|
|
|
||
|
|
Vector3 upN = up.sqrMagnitude < 0.000001f ? Vector3.up : up.normalized;
|
||
|
|
Vector3 forward = Vector3.Cross(upN, right);
|
||
|
|
if (forward.sqrMagnitude < 0.000001f)
|
||
|
|
{
|
||
|
|
upN = Vector3.Cross(right, Vector3.forward).sqrMagnitude > 0.000001f ? Vector3.forward : Vector3.up;
|
||
|
|
forward = Vector3.Cross(upN, right);
|
||
|
|
}
|
||
|
|
|
||
|
|
forward.Normalize();
|
||
|
|
Vector3 orthoUp = Vector3.Cross(right, forward);
|
||
|
|
return Quaternion.LookRotation(forward, orthoUp);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
|
||
|
|
{
|
||
|
|
if (!autoFindTargetByTag) return;
|
||
|
|
|
||
|
|
target = null;
|
||
|
|
TryAutoResolveTarget(force: true);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void TweenToRotation(Quaternion desired, float duration, Ease ease)
|
||
|
|
{
|
||
|
|
if (rotationTarget == null)
|
||
|
|
rotationTarget = transform;
|
||
|
|
|
||
|
|
Quaternion current = rotationSpace == RotationSpace.Local ? rotationTarget.localRotation : rotationTarget.rotation;
|
||
|
|
if (Quaternion.Angle(current, desired) < minAngleToTween)
|
||
|
|
return;
|
||
|
|
|
||
|
|
if (currentTween != null && currentTween.IsActive())
|
||
|
|
currentTween.Kill();
|
||
|
|
|
||
|
|
if (duration <= 0f)
|
||
|
|
{
|
||
|
|
SetRotation(desired);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
currentTween = rotationSpace == RotationSpace.Local
|
||
|
|
? rotationTarget.DOLocalRotateQuaternion(desired, duration)
|
||
|
|
: rotationTarget.DORotateQuaternion(desired, duration);
|
||
|
|
|
||
|
|
currentTween.SetEase(ease);
|
||
|
|
currentTween.SetUpdate(useUnscaledTime);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void SetRotation(Quaternion rot)
|
||
|
|
{
|
||
|
|
if (rotationSpace == RotationSpace.Local)
|
||
|
|
rotationTarget.localRotation = rot;
|
||
|
|
else
|
||
|
|
rotationTarget.rotation = rot;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|