using UnityEngine;
///
/// Procedural platform animation supporting translation, rotation, and scaling.
/// Supports multiple animation types (Constant, PingPong, Loop, Single) with easing.
///
[DefaultExecutionOrder(0)]
public class ProceduralAnimator : MonoBehaviour
{
public enum AnimationSpace
{
Local,
World
}
public enum AnimationType
{
Constant, // Continuous movement without return
PingPong, // Back and forth (A → B → A)
Loop, // Loop with teleport (A → B, teleport to A)
Single // Play once then stop
}
public enum AnimationBalance
{
Centered,
Forward,
Backward
}
[System.Serializable]
public class Settings
{
[Tooltip("Animation space")]
public AnimationSpace Space = AnimationSpace.Local;
[Tooltip("Type of animation loop")]
public AnimationType Type = AnimationType.PingPong;
[Tooltip("TCenter of animation")]
public AnimationBalance Balance = AnimationBalance.Centered;
[Tooltip("Custom animation curve")]
public AnimationCurve Curve = AnimationCurve.EaseInOut(0, 0, 1, 1);
[Tooltip("Play on Awake")]
public bool PlayOnAwake = true;
[Tooltip("Animation speed multiplier")]
public float Duration = 4;
[Tooltip("Time offset to desynchronize multiple animations")]
public float TimeOffset = 0;
[Tooltip("Translation amount in local space (meters)")]
public Vector3 Translation = Vector3.zero;
[Tooltip("Rotation amount in degrees")]
public Vector3 Rotation = Vector3.zero;
[Tooltip("Scale amount relative to initial scale")]
public Vector3 Scale = Vector3.zero;
}
[System.Serializable]
public class State
{
[Tooltip("Current animation time")]
public float CurrentTime;
[Tooltip("Is animation finished? (Single only)")]
public bool IsFinished;
[HideInInspector] public Vector3 InitialPosition;
[HideInInspector] public Quaternion InitialRotation;
[HideInInspector] public Vector3 InitialScale;
}
[SerializeField] private Settings _settings;
[SerializeField] private State _state;
private bool _started;
private float _timeOffset;
void OnDrawGizmosSelected()
{
}
void OnEnable()
{
_timeOffset = Time.timeSinceLevelLoad;
_started = _settings.PlayOnAwake;
}
void Start()
{
_state.InitialPosition = transform.localPosition;
_state.InitialRotation = transform.localRotation;
_state.InitialScale = transform.localScale;
_state.CurrentTime = _settings.TimeOffset;
_state.IsFinished = false;
}
void Update()
{
if (_state.IsFinished || !_started)
return;
_state.CurrentTime = Time.timeSinceLevelLoad + _settings.TimeOffset * _settings.Duration - _timeOffset;
float normalizedTime = GetNormalizedTime(_state.CurrentTime);
float easedTime = ApplyEasing(normalizedTime);
float balanceTime = ApplyBalance(easedTime);
ApplyTranslation(balanceTime);
ApplyRotation(balanceTime);
ApplyScale(balanceTime);
}
///
/// Play animation from initial state.
///
[ContextMenu("PlayAnimation")]
public void PlayAnimation()
{
ResetAnimation();
_timeOffset = Time.timeSinceLevelLoad;
_started = true;
}
///
/// Resets animation to initial state.
///
public void ResetAnimation()
{
_state.CurrentTime = _settings.TimeOffset;
_state.IsFinished = false;
transform.localPosition = _state.InitialPosition;
transform.localRotation = _state.InitialRotation;
transform.localScale = _state.InitialScale;
}
///
/// Pauses or resumes animation.
///
public void SetPaused(bool paused)
{
enabled = !paused;
}
///
/// Gets normalized time [0,1] based on animation type.
///
private float GetNormalizedTime(float time)
{
if (_settings.Duration == 0)
return 0;
switch (_settings.Type)
{
case AnimationType.Constant:
return time / _settings.Duration;
case AnimationType.PingPong:
return Mathf.PingPong(time / _settings.Duration, 1);
case AnimationType.Loop:
return Mathf.Repeat(time / _settings.Duration, 1f);
case AnimationType.Single:
if (time >= _settings.Duration)
{
_state.IsFinished = true;
return 1;
}
return time / _settings.Duration;
default:
return 0f;
}
}
private float ApplyBalance(float time)
{
switch (_settings.Balance)
{
case AnimationBalance.Centered:
return time - .5f;
case AnimationBalance.Backward:
return -time;
default:
return time;
}
}
///
/// Applies easing function to normalized time.
///
private float ApplyEasing(float time)
{
switch (_settings.Type)
{
default:
return _settings.Curve.Evaluate(time); ;
case AnimationType.Constant:
return time;
}
}
///
/// Applies translation based on eased time.
///
private void ApplyTranslation(float time)
{
if (_settings.Translation == Vector3.zero)
return;
Vector3 offset = _settings.Translation * time;
if (_settings.Space == AnimationSpace.Local)
offset = _state.InitialRotation * offset;
transform.localPosition = _state.InitialPosition + offset;
}
///
/// Applies rotation based on eased time.
///
private void ApplyRotation(float time)
{
if (_settings.Rotation == Vector3.zero)
return;
Vector3 rotation = _settings.Rotation * time;
Quaternion rotationOffset = Quaternion.Euler(rotation);
if (_settings.Space == AnimationSpace.Local)
rotationOffset = _state.InitialRotation * rotationOffset;
else
rotationOffset = rotationOffset * _state.InitialRotation;
transform.localRotation = rotationOffset;
}
///
/// Applies scale based on eased time.
///
private void ApplyScale(float t)
{
if (_settings.Scale == Vector3.zero)
return;
Vector3 scale = Vector3.one + _settings.Scale * t;
transform.localScale = Vector3.Scale(_state.InitialScale, scale);
}
}