///
/// Interpolator and InertialInterpolator by Nothke
///
/// A utility struct for when a value needs to smoothly transition between 2 states.
///
/// Call Regress() to make the state advance towards 0,
/// or Progress() to make it advance towards 1.
/// Alternatively use Toggle().
/// SetTo(value) will set it to a state immediately.
///
/// Call Update() every frame or fixed frame for the transition to progress.
///
/// An Interpolator is a version that transits at fixed speed.
///
/// An InertialInterpolator is a version that uses inertia, it will accelerate from one state,
/// and slow down when approaching the end state.
/// InertialInterpolator can also advance to arbitrary point with AccelerateTo(value).
/// It can also "emergency-stop" by calling StartBraking().
///
/// It doesn't matter if you call the functions repeatedly or once.
///
/// Make sure to have a maxSpeed parameter higher than 0, otherwise they won't move.
///
/// To initialize the struct correctly in inspector (with maxSpeed > 0) use:
/// public Interpolator interpolator = Interpolator.Default();
///
/// ============================================================================
///
/// MIT License
///
/// Copyright(c) 2021 Ivan Notaroš
///
/// Permission is hereby granted, free of charge, to any person obtaining a copy
/// of this software and associated documentation files (the "Software"), to deal
/// in the Software without restriction, including without limitation the rights
/// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
/// copies of the Software, and to permit persons to whom the Software is
/// furnished to do so, subject to the following conditions:
///
/// The above copyright notice and this permission notice shall be included in all
/// copies or substantial portions of the Software.
///
/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
/// SOFTWARE.
///
/// ============================================================================
///
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace Nothke.Utils
{
public enum ProgressionState
{
AtStart,
Progressing,
AtEnd,
Regressing
};
public interface IInterpolator
{
void Update(float dt);
ProgressionState State { get; }
///
/// Advance value towards 1.
///
void Progress();
///
/// Advance value towards 0.
///
void Regress();
void SetTo(float value);
///
/// Toggle progression between progressing or regressing.
///
void Toggle();
}
[System.Serializable]
public struct Interpolator : IInterpolator
{
public float maxSpeed;
[HideInInspector]
public float progress, velocity;
ProgressionState state;
public ProgressionState State => state;
//public bool ProgressingOrAtEnd => state == DeploymentState.Progressing || state == DeploymentState.AtEnd;
public static Interpolator Default()
{
return new Interpolator()
{
maxSpeed = 1
};
}
public void Progress()
{
velocity = maxSpeed;
state = ProgressionState.Progressing;
}
public void Regress()
{
velocity = -maxSpeed;
state = ProgressionState.Regressing;
}
public void Toggle()
{
if (state == ProgressionState.AtEnd || state == ProgressionState.Progressing)
Regress();
else
Progress();
}
public void SetTo(float value)
{
value = Mathf.Clamp01(value);
progress = value;
velocity = 0;
if (value == 1)
state = ProgressionState.AtEnd;
else if (value == 0)
state = ProgressionState.AtStart;
else
state = ProgressionState.Progressing;
}
public void Update(float dt)
{
if (maxSpeed == 0)
return;
progress += velocity * dt;
if (velocity < 0 && progress < 0)
{
progress = 0;
if (state == ProgressionState.Regressing)
{
velocity = 0;
state = ProgressionState.AtStart;
}
}
else if (velocity > 0 && progress > 1)
{
progress = 1;
if (state == ProgressionState.Progressing)
{
velocity = 0;
state = ProgressionState.AtEnd;
}
}
}
}
[System.Serializable]
public struct InertialInterpolator : IInterpolator
{
public float maxSpeed;
public float acceleration;
public float brakingAcceleration;
[HideInInspector] public float progress;
[HideInInspector] public float velocity;
[HideInInspector] public float accel;
private ProgressionState state;
[HideInInspector] public bool braking;
private float endTarget;
private float beginTarget;
public ProgressionState State => state;
public bool Stopped => velocity == 0;
public static InertialInterpolator Default()
{
return new InertialInterpolator()
{
maxSpeed = 1f,
acceleration = 1f,
endTarget = 1f
};
}
public void Toggle()
{
if (state == ProgressionState.AtEnd || state == ProgressionState.Progressing)
Regress();
else
Progress();
}
private void ProgressTo(float to)
{
endTarget = to;
accel = acceleration;
state = ProgressionState.Progressing;
braking = velocity < 0;
if (braking)
accel = brakingAcceleration > 0 ? brakingAcceleration : acceleration;
}
public void Progress()
{
ProgressTo(1);
}
private void RegressTo(float to)
{
beginTarget = to;
accel = -acceleration;
state = ProgressionState.Regressing;
braking = velocity > 0;
if (braking)
accel = brakingAcceleration > 0 ? -brakingAcceleration : -acceleration;
}
public void Regress()
{
RegressTo(0);
}
///
/// Sets the target to start accelerating to. Note, the input should always be 0-1
///
///
public void AccelerateTo(float to)
{
if (progress < to)
ProgressTo(to);
else
RegressTo(to);
}
///
/// The interpolator will start decelerating with brakingAcceleration eventually slowly coming to a stop,
/// overriding any progression status.
///
public void StartBraking()
{
if (braking)
return;
braking = true;
switch (velocity)
{
case > 0:
ProgressTo(Mathf.Min(progress + StoppingDistance(velocity, -brakingAcceleration), 1));
return;
case < 0:
RegressTo(Mathf.Max(progress - -StoppingDistance(velocity, brakingAcceleration), 0));
return;
}
}
static float StoppingDistance(float curVelo, float accel)
{
return curVelo * curVelo / (2 * -accel);
}
public void Update(float dt)
{
if (maxSpeed == 0)
Debug.Log("MaxSpeed of a Deployer is 0, there will be no movement. Please set it before using");
// Limit velocity
if (velocity < -maxSpeed)
{
accel = 0;
velocity = -maxSpeed + Mathf.Epsilon;
}
else if (velocity > maxSpeed)
{
accel = 0;
velocity = maxSpeed - Mathf.Epsilon;
}
if (brakingAcceleration > 0 && !braking)
{
// TODO: Handle the case where braking force is so big that it immediately reverses the direction
// In that case we should skip braking state entirely
if (velocity > 0 && state == ProgressionState.Progressing)
{
float stopDist = StoppingDistance(velocity, -brakingAcceleration);
if (progress > endTarget - stopDist)
{
accel = -brakingAcceleration;
braking = true;
}
}
else if (velocity < 0 && state == ProgressionState.Regressing)
{
float stopDist = -StoppingDistance(velocity, brakingAcceleration);
if (progress < beginTarget + stopDist)
{
accel = brakingAcceleration;
braking = true;
}
}
}
if (braking)
{
if (accel < 0 && velocity < 0)
{
if (state == ProgressionState.Progressing)
SetTo(endTarget);
else
{
braking = false;
accel = -acceleration;
}
}
else if (accel > 0 && velocity > 0)
{
if (state == ProgressionState.Regressing)
SetTo(beginTarget);
else
{
braking = false;
accel = acceleration;
}
}
}
velocity += accel * dt;
progress += velocity * dt;
// Clamp between 0-1
if (velocity < 0 && progress < 0)
{
progress = 0;
velocity = 0;
if (state == ProgressionState.Regressing)
SetTo(0);
}
else if (velocity > 0 && progress > 1)
{
progress = 1;
velocity = 0;
if (state == ProgressionState.Progressing)
SetTo(1);
}
}
public void SetTo(float value)
{
value = Mathf.Clamp01(value);
progress = value;
velocity = 0;
accel = 0;
braking = false;
if (value == 1)
state = ProgressionState.AtEnd;
else if (value == 0)
state = ProgressionState.AtStart;
else
state = ProgressionState.Progressing;
}
}
}