Files

522 lines
15 KiB
C#
Raw Permalink Normal View History

2026-07-08 19:53:15 +08:00
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<Waypoint> waypoints = new List<Waypoint>();
[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<Waypoint> 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<int> 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<int> BuildTraversalIndices(PathMode mode, int count)
{
var list = new List<int>();
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<int> 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<int>(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
}
}