using UnityEngine; #if UNITY_EDITOR using UnityEditor; #endif [ExecuteAlways] public class ElasticTracker : MonoBehaviour { // ── Follow ── public bool enableFollow = true; public Transform[] followTargets = new Transform[1]; public Vector3 positionOffset; [Min(0f)] public float followSmoothSpeed = 5f; [Min(0f)] public float followDistanceElasticity = 1.5f; [Min(1)] public int followFrameInterval = 1; public bool followX = true; public bool followY = true; public bool followZ = true; [Range(0f, 100f)] public float moveRatioX = 100f; [Range(0f, 100f)] public float moveRatioY = 100f; [Range(0f, 100f)] public float moveRatioZ = 100f; // ── LookAt ── public bool enableLookAt = true; public Transform[] lookAtTargets = new Transform[1]; public Vector3 lookAtOffset; [Min(0f)] public float lookAtSmoothSpeed = 5f; public Vector3 worldUp = Vector3.up; [Min(1)] public int lookAtFrameInterval = 1; public bool rotateX = true; public bool rotateY = true; public bool rotateZ = true; [Range(0f, 100f)] public float rotateRatioX = 100f; [Range(0f, 100f)] public float rotateRatioY = 100f; [Range(0f, 100f)] public float rotateRatioZ = 100f; // ── Orbital ── public bool enableOrbital = false; public Transform[] orbitCenters = new Transform[0]; [Min(0f)] public float orbitHorizontalRadius = 5f; public float orbitHorizontalSpeed = 15f; public float orbitHorizontalPhaseOffset = 0f; [Min(0f)] public float orbitVerticalRadius = 1f; public float orbitVerticalSpeed = 8f; public float orbitVerticalPhaseOffset = 0f; public float orbitVerticalAngleMin = -20f; public float orbitVerticalAngleMax = 40f; public float orbitHeightOffset = 2f; // ── Noise (Hand-held) ── public bool enableNoise = false; [Min(0f)] public float posNoiseAmplitude = 0.003f; [Min(0f)] public float posNoiseFrequency = 0.4f; public bool posNoiseX = true; public bool posNoiseY = true; public bool posNoiseZ = true; [Min(0f)] public float rotNoiseAmplitude = 0.25f; [Min(0f)] public float rotNoiseFrequency = 0.3f; public bool rotNoiseX = true; public bool rotNoiseY = true; public bool rotNoiseZ = false; // ── Editor ── public bool updateInEditMode = true; private int _followFrameCounter; private float _followAccDelta; private int _lookAtFrameCounter; private float _lookAtAccDelta; private float _orbitalTime; private float _noiseTime; private float _noiseSeedX, _noiseSeedY, _noiseSeedZ; private float _noiseSeedRX, _noiseSeedRY, _noiseSeedRZ; #if UNITY_EDITOR private double _lastEditorTime; #endif private void OnEnable() { _followFrameCounter = 0; _followAccDelta = 0f; _lookAtFrameCounter = 0; _lookAtAccDelta = 0f; _orbitalTime = 0f; _noiseTime = 0f; _noiseSeedX = Random.Range(0f, 1000f); _noiseSeedY = Random.Range(0f, 1000f); _noiseSeedZ = Random.Range(0f, 1000f); _noiseSeedRX = Random.Range(0f, 1000f); _noiseSeedRY = Random.Range(0f, 1000f); _noiseSeedRZ = Random.Range(0f, 1000f); #if UNITY_EDITOR _lastEditorTime = EditorApplication.timeSinceStartup; EditorApplication.update -= EditorTick; EditorApplication.update += EditorTick; #endif } private void OnDisable() { #if UNITY_EDITOR EditorApplication.update -= EditorTick; #endif } private void LateUpdate() { if (!Application.isPlaying) return; float dt = Time.deltaTime; if (enableFollow) { _followAccDelta += dt; _followFrameCounter++; if (_followFrameCounter >= followFrameInterval) { ApplyFollow(_followAccDelta); _followFrameCounter = 0; _followAccDelta = 0f; } } if (enableOrbital) { ApplyOrbital(dt); } if (enableLookAt) { _lookAtAccDelta += dt; _lookAtFrameCounter++; if (_lookAtFrameCounter >= lookAtFrameInterval) { ApplyLookAt(_lookAtAccDelta); _lookAtFrameCounter = 0; _lookAtAccDelta = 0f; } } if (enableNoise) { ApplyNoise(dt); } } #if UNITY_EDITOR private void EditorTick() { if (Application.isPlaying || !updateInEditMode || this == null || !isActiveAndEnabled) return; double now = EditorApplication.timeSinceStartup; float deltaTime = Mathf.Max(0.0001f, (float)(now - _lastEditorTime)); _lastEditorTime = now; if (enableFollow) ApplyFollow(deltaTime); if (enableOrbital) ApplyOrbital(deltaTime); if (enableLookAt) ApplyLookAt(deltaTime); if (enableNoise) ApplyNoise(deltaTime); EditorApplication.QueuePlayerLoopUpdate(); } #endif private Vector3 GetCenterPosition(Transform[] targets) { Vector3 sum = Vector3.zero; int count = 0; for (int i = 0; i < targets.Length; i++) { if (targets[i] != null) { sum += targets[i].position; count++; } } if (count == 0) return transform.position; return sum / count; } private void ApplyFollow(float deltaTime) { Vector3 centerPos = GetCenterPosition(followTargets); if (centerPos == transform.position && followTargets.Length > 0) return; Vector3 targetPos = centerPos + positionOffset; Vector3 currentPos = transform.position; float distance = Vector3.Distance(currentPos, targetPos); float speedMultiplier = 1f + (distance * followDistanceElasticity); float t = 1f - Mathf.Exp(-followSmoothSpeed * speedMultiplier * deltaTime); Vector3 fullNextPos = Vector3.Lerp(currentPos, targetPos, t); Vector3 nextPos = currentPos; if (followX) nextPos.x = Mathf.Lerp(currentPos.x, fullNextPos.x, moveRatioX * 0.01f); if (followY) nextPos.y = Mathf.Lerp(currentPos.y, fullNextPos.y, moveRatioY * 0.01f); if (followZ) nextPos.z = Mathf.Lerp(currentPos.z, fullNextPos.z, moveRatioZ * 0.01f); transform.position = nextPos; } private void ApplyOrbital(float deltaTime) { // orbitCenters가 비어있으면 followTargets를 폴백으로 사용 Transform[] centers = (orbitCenters != null && orbitCenters.Length > 0) ? orbitCenters : followTargets; Vector3 centerPos = GetCenterPosition(centers); _orbitalTime += deltaTime; // Horizontal: continuous 360° loop float hAngleDeg = (orbitHorizontalPhaseOffset + orbitHorizontalSpeed * _orbitalTime) % 360f; float hAngleRad = hAngleDeg * Mathf.Deg2Rad; // Vertical: ping-pong with sine easing for smooth turnaround float vCycle = orbitVerticalSpeed * _orbitalTime + orbitVerticalPhaseOffset; float vNormalized = (Mathf.Sin(vCycle * Mathf.Deg2Rad) + 1f) * 0.5f; float vAngleDeg = Mathf.Lerp(orbitVerticalAngleMin, orbitVerticalAngleMax, vNormalized); float vAngleRad = vAngleDeg * Mathf.Deg2Rad; // Spherical to Cartesian offset float cosV = Mathf.Cos(vAngleRad); Vector3 orbitOffset = new Vector3( Mathf.Sin(hAngleRad) * orbitHorizontalRadius * cosV, Mathf.Sin(vAngleRad) * orbitVerticalRadius + orbitHeightOffset, Mathf.Cos(hAngleRad) * orbitHorizontalRadius * cosV ); transform.position = centerPos + orbitOffset; } private void ApplyLookAt(float deltaTime) { Vector3 centerPos = GetCenterPosition(lookAtTargets); Vector3 lookPoint = centerPos + lookAtOffset; Vector3 direction = lookPoint - transform.position; if (direction.sqrMagnitude < 0.0001f) return; Quaternion targetRot = Quaternion.LookRotation(direction.normalized, worldUp); float t = 1f - Mathf.Exp(-lookAtSmoothSpeed * deltaTime); Quaternion fullRot = Quaternion.Slerp(transform.rotation, targetRot, t); Vector3 currentEuler = transform.rotation.eulerAngles; Vector3 fullEuler = fullRot.eulerAngles; Vector3 resultEuler = currentEuler; if (rotateX) resultEuler.x = Mathf.LerpAngle(currentEuler.x, fullEuler.x, rotateRatioX * 0.01f); if (rotateY) resultEuler.y = Mathf.LerpAngle(currentEuler.y, fullEuler.y, rotateRatioY * 0.01f); if (rotateZ) resultEuler.z = Mathf.LerpAngle(currentEuler.z, fullEuler.z, rotateRatioZ * 0.01f); transform.rotation = Quaternion.Euler(resultEuler); } private void ApplyNoise(float deltaTime) { _noiseTime += deltaTime; // Position noise if (posNoiseAmplitude > 0f) { float pt = _noiseTime * posNoiseFrequency; float nx = posNoiseX ? (Mathf.PerlinNoise(_noiseSeedX + pt, 0f) - 0.5f) * 2f * posNoiseAmplitude : 0f; float ny = posNoiseY ? (Mathf.PerlinNoise(_noiseSeedY + pt, 0f) - 0.5f) * 2f * posNoiseAmplitude : 0f; float nz = posNoiseZ ? (Mathf.PerlinNoise(_noiseSeedZ + pt, 0f) - 0.5f) * 2f * posNoiseAmplitude : 0f; transform.position += transform.rotation * new Vector3(nx, ny, nz); } // Rotation noise if (rotNoiseAmplitude > 0f) { float rt = _noiseTime * rotNoiseFrequency; float rx = rotNoiseX ? (Mathf.PerlinNoise(_noiseSeedRX + rt, 0f) - 0.5f) * 2f * rotNoiseAmplitude : 0f; float ry = rotNoiseY ? (Mathf.PerlinNoise(_noiseSeedRY + rt, 0f) - 0.5f) * 2f * rotNoiseAmplitude : 0f; float rz = rotNoiseZ ? (Mathf.PerlinNoise(_noiseSeedRZ + rt, 0f) - 0.5f) * 2f * rotNoiseAmplitude : 0f; transform.rotation *= Quaternion.Euler(rx, ry, rz); } } }