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