522 lines
15 KiB
C#
522 lines
15 KiB
C#
|
|
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
|
||
|
|
}
|
||
|
|
}
|