using System; using System.Collections.Generic; using UnityEngine; #if DOTWEEN using DG.Tweening; #endif namespace Test_2022.CameraControl { [DisallowMultipleComponent] public sealed class CameraWaypointSwitcher : MonoBehaviour { public enum TransitionMode { Instant = 0, Tween = 1 } public enum PathMode { Loop = 0, PingPong = 1 } public enum ShakeMode { Off = 0, Handheld = 1, WhileMoving = 2 } [Serializable] public struct Waypoint { public Transform point; [Min(0f)] public float holdSeconds; } [Header("Target")] [SerializeField] Transform target; [SerializeField] bool autoStartOnEnable = true; [SerializeField] bool useUnscaledTime; [SerializeField] bool restoreOnDisable = true; [Header("Path")] [SerializeField] TransitionMode transitionMode = TransitionMode.Instant; [SerializeField] PathMode pathMode = PathMode.PingPong; [SerializeField] List waypoints = new List(); [SerializeField, Min(0f)] float moveDurationSeconds = 1.2f; [SerializeField, Min(0f)] float defaultHoldSeconds = 1.5f; [SerializeField] AnimationCurve moveEase = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f); [SerializeField] bool startFromNearestWaypoint = true; [Header("Shake")] [SerializeField] ShakeMode shakeMode = ShakeMode.Handheld; [SerializeField] bool createRigForShake = true; [SerializeField] bool shakePosition = true; [SerializeField] bool shakeRotation = true; [SerializeField, Min(0f)] float handheldPositionAmplitude = 0.015f; [SerializeField, Min(0f)] float handheldRotationAmplitude = 0.45f; [SerializeField, Min(0f)] float handheldFrequency = 0.35f; [SerializeField, Min(0f)] float handheldSmoothingSeconds = 0.12f; [SerializeField] bool handheldUseFixedSeed; [SerializeField] int handheldSeed = 12345; [Header("Shake (While Moving)")] [SerializeField, Min(0f)] float shakeStrengthPosition = 0.02f; [SerializeField, Min(0f)] float shakeStrengthRotation = 0.25f; [SerializeField, Min(1)] int shakeVibrato = 8; [SerializeField, Range(0f, 180f)] float shakeRandomness = 25f; Vector3 basePosition; Quaternion baseRotation; Transform rig; Transform shakeTransform; Vector3 shakeBaseLocalPosition; Quaternion shakeBaseLocalRotation; Vector3 shakePosOffset; Vector3 shakeRotOffset; float shakeSeedX; float shakeSeedY; float shakeSeedZ; float lastShakeTime; Coroutine cutRoutine; bool isPlaying; #if DOTWEEN Sequence sequence; Tween shakePosTween; Tween shakeRotTween; #endif public IList Waypoints => waypoints; void Reset() { var cam = Camera.main; if (cam != null) target = cam.transform; } void OnEnable() { if (autoStartOnEnable) StartPlayback(); } void OnDisable() { StopPlayback(restoreOnDisable); } public void StartPlayback() { if (target == null) return; if (waypoints == null || waypoints.Count == 0) return; StopPlayback(false); CacheBasePose(); SetupRigAndShakeTransforms(); CacheShakeLocalBase(); ResetHandheldState(); var moveTransform = rig != null ? rig : target; var indices = BuildTraversalIndices(pathMode, waypoints.Count); if (indices.Count == 0) return; if (startFromNearestWaypoint) RotateTraversalToNearest(indices, moveTransform.position); isPlaying = true; if (transitionMode == TransitionMode.Instant) { cutRoutine = StartCoroutine(RunCutLoop(moveTransform, indices)); return; } #if DOTWEEN sequence = DOTween.Sequence(); sequence.SetUpdate(useUnscaledTime); sequence.SetAutoKill(false); for (var i = 0; i < indices.Count; i++) { var waypointIndex = indices[i]; var wp = waypoints[waypointIndex].point; if (wp == null) continue; var move = moveTransform.DOMove(wp.position, moveDurationSeconds); move.SetEase(moveEase); var rot = moveTransform.DORotateQuaternion(wp.rotation, moveDurationSeconds); rot.SetEase(moveEase); sequence.Append(move); sequence.Join(rot); if (shakeMode == ShakeMode.WhileMoving) sequence.Join(CreateOneShotShakeTween(moveDurationSeconds)); sequence.AppendInterval(GetHoldSeconds(waypointIndex)); } sequence.SetLoops(-1, LoopType.Restart); sequence.Play(); #endif } public void StopPlayback(bool restore) { isPlaying = false; if (cutRoutine != null) { StopCoroutine(cutRoutine); cutRoutine = null; } #if DOTWEEN if (sequence != null) { sequence.Kill(false); sequence = null; } if (shakePosTween != null) { shakePosTween.Kill(false); shakePosTween = null; } if (shakeRotTween != null) { shakeRotTween.Kill(false); shakeRotTween = null; } #endif ResetShakeTransformLocal(); if (restore) RestoreBasePose(); TeardownRigIfCreated(); } public void ClearWaypoints() { waypoints.Clear(); } public bool AddWaypoint(Transform point, float holdSeconds = -1f) { if (point == null) return false; for (var i = 0; i < waypoints.Count; i++) { if (waypoints[i].point == point) return false; } waypoints.Add(new Waypoint { point = point, holdSeconds = holdSeconds }); return true; } public int RemoveNullWaypoints() { return waypoints.RemoveAll(w => w.point == null); } void CacheBasePose() { if (target == null) return; basePosition = target.position; baseRotation = target.rotation; } void RestoreBasePose() { if (target == null) return; target.position = basePosition; target.rotation = baseRotation; } void SetupRigAndShakeTransforms() { rig = null; shakeTransform = target; if (target == null) return; if (!createRigForShake || shakeMode == ShakeMode.Off) return; var rigGo = new GameObject(target.name + "_Rig"); rigGo.transform.SetPositionAndRotation(target.position, target.rotation); rigGo.transform.SetParent(target.parent, true); rig = rigGo.transform; target.SetParent(rig, true); shakeTransform = target; target.localPosition = Vector3.zero; target.localRotation = Quaternion.identity; } void CacheShakeLocalBase() { if (shakeTransform == null) return; shakeBaseLocalPosition = shakeTransform.localPosition; shakeBaseLocalRotation = shakeTransform.localRotation; } void ResetShakeTransformLocal() { if (shakeTransform == null) return; shakeTransform.localPosition = shakeBaseLocalPosition; shakeTransform.localRotation = shakeBaseLocalRotation; } void ResetHandheldState() { shakePosOffset = Vector3.zero; shakeRotOffset = Vector3.zero; lastShakeTime = useUnscaledTime ? Time.unscaledTime : Time.time; if (handheldUseFixedSeed) { shakeSeedX = handheldSeed * 0.0137f + 10.1f; shakeSeedY = handheldSeed * 0.0213f + 20.2f; shakeSeedZ = handheldSeed * 0.0341f + 30.3f; } else { shakeSeedX = UnityEngine.Random.value * 1000f; shakeSeedY = UnityEngine.Random.value * 1000f; shakeSeedZ = UnityEngine.Random.value * 1000f; } } void TeardownRigIfCreated() { if (rig == null || target == null) { rig = null; shakeTransform = null; return; } if (target.parent == rig) target.SetParent(rig.parent, true); if (Application.isPlaying) Destroy(rig.gameObject); else DestroyImmediate(rig.gameObject); rig = null; shakeTransform = null; } void LateUpdate() { if (!isPlaying) return; if (shakeMode != ShakeMode.Handheld) return; if (shakeTransform == null) return; if (!shakePosition && !shakeRotation) return; var now = useUnscaledTime ? Time.unscaledTime : Time.time; var dt = Mathf.Max(0f, now - lastShakeTime); lastShakeTime = now; var t = now * Mathf.Max(0f, handheldFrequency); var nx = Mathf.PerlinNoise(shakeSeedX, t) * 2f - 1f; var ny = Mathf.PerlinNoise(shakeSeedY, t + 11.1f) * 2f - 1f; var nz = Mathf.PerlinNoise(shakeSeedZ, t + 22.2f) * 2f - 1f; var targetPos = Vector3.zero; if (shakePosition && handheldPositionAmplitude > 0f) { targetPos = new Vector3(nx, ny * 0.7f, nz * 0.35f) * handheldPositionAmplitude; } var targetRot = Vector3.zero; if (shakeRotation && handheldRotationAmplitude > 0f) { targetRot = new Vector3(ny * 0.75f, nx * 0.5f, nz) * handheldRotationAmplitude; } var smooth = handheldSmoothingSeconds <= 0f ? 1f : 1f - Mathf.Exp(-dt / Mathf.Max(0.0001f, handheldSmoothingSeconds)); shakePosOffset = Vector3.Lerp(shakePosOffset, targetPos, smooth); shakeRotOffset = Vector3.Lerp(shakeRotOffset, targetRot, smooth); shakeTransform.localPosition = shakeBaseLocalPosition + shakePosOffset; shakeTransform.localRotation = shakeBaseLocalRotation * Quaternion.Euler(shakeRotOffset); } System.Collections.IEnumerator RunCutLoop(Transform moveTransform, List indices) { while (true) { for (var i = 0; i < indices.Count; i++) { var waypointIndex = indices[i]; if (waypointIndex < 0 || waypointIndex >= waypoints.Count) continue; var wp = waypoints[waypointIndex].point; if (wp == null) continue; moveTransform.SetPositionAndRotation(wp.position, wp.rotation); var hold = Mathf.Max(0f, GetHoldSeconds(waypointIndex)); if (hold > 0f) { if (useUnscaledTime) yield return new WaitForSecondsRealtime(hold); else yield return new WaitForSeconds(hold); } else yield return null; } } } float GetHoldSeconds(int waypointIndex) { if (waypointIndex < 0 || waypointIndex >= waypoints.Count) return defaultHoldSeconds; var h = waypoints[waypointIndex].holdSeconds; return h > 0f ? h : defaultHoldSeconds; } static List BuildTraversalIndices(PathMode mode, int count) { var list = new List(); if (count <= 0) return list; if (count == 1) { list.Add(0); return list; } if (mode == PathMode.Loop) { for (var i = 0; i < count; i++) list.Add(i); return list; } for (var i = 0; i < count; i++) list.Add(i); for (var i = count - 2; i >= 1; i--) list.Add(i); return list; } void RotateTraversalToNearest(List indices, Vector3 currentPosition) { if (indices == null || indices.Count == 0) return; var best = 0; var bestDist = float.PositiveInfinity; for (var i = 0; i < indices.Count; i++) { var wi = indices[i]; if (wi < 0 || wi >= waypoints.Count) continue; var p = waypoints[wi].point; if (p == null) continue; var d = (p.position - currentPosition).sqrMagnitude; if (d < bestDist) { bestDist = d; best = i; } } if (best <= 0) return; var rotated = new List(indices.Count); for (var i = best; i < indices.Count; i++) rotated.Add(indices[i]); for (var i = 0; i < best; i++) rotated.Add(indices[i]); indices.Clear(); indices.AddRange(rotated); } #if DOTWEEN Tween CreateOneShotShakeTween(float duration) { if (shakeTransform == null) return null; var seq = DOTween.Sequence(); seq.SetUpdate(useUnscaledTime); if (shakePosition && shakeStrengthPosition > 0f) { var t = shakeTransform.DOShakePosition( duration, shakeStrengthPosition, shakeVibrato, shakeRandomness, false, true ); seq.Join(t); } if (shakeRotation && shakeStrengthRotation > 0f) { var t = shakeTransform.DOShakeRotation( duration, shakeStrengthRotation, shakeVibrato, shakeRandomness, true ); seq.Join(t); } return seq; } #endif } }