Dynamics/SecondOrderDynamics.cs
using System;
namespace Andicraft.SecondOrderDynamics;
// based on https://www.youtube.com/watch?v=KPoeNZZ6H4s
public abstract class SecondOrderDynamics<T>
{
public T CurrentValue => _currentValue;
protected T _currentValue;
private Vector3 _parameters;
protected T _previousValue;
protected T _velocity;
// yeah idk about these three, complicated and weird math, watch the video
protected float k1;
protected float k2;
protected float k3;
/// <summary>
/// Creates a dynamics system with default parameters.
/// </summary>
protected SecondOrderDynamics(T startingValue) : this(3f, .5f, 1f, startingValue)
{
}
/// <summary>
/// Creates a dynamics system with assigned parameters.
/// </summary>
protected SecondOrderDynamics( DynamicsParameters p, T startingValue ) : this(p.Frequency, p.Damping, p.Response, startingValue)
{
}
/// <summary>
/// Creates a dynamics system with assigned parameters.
/// </summary>
protected SecondOrderDynamics(Vector3 parameters, T startingValue) : this(parameters.x, parameters.y, parameters.z, startingValue)
{
}
/// <summary>
/// Creates and initializes the dynamics system.
/// </summary>
/// <param name="frequency">Frequency of the system in hertz - the higher this is the faster things move</param>
/// <param name="damping">Damping. At 0 it will never stop vibrating, at 1 you get critical damping. Between 0 and 1 you get springy motion, above 1 the value approaches very slowly.</param>
/// <param name="response">Response. Less than one means slow to respond, at exactly one the system will follow the target value immediately. Above 1, the system will overshoot.</param>
/// <param name="startingValue">Starting value.</param>
protected SecondOrderDynamics(float frequency, float damping, float response, T startingValue)
{
_parameters = new Vector3(frequency, damping, response);
_previousValue = startingValue;
_currentValue = startingValue;
CalculateParameters();
}
/// <summary>
/// Lets you refresh parameters mid-simulation.
/// </summary>
/// <param name="v">Parameters in Vector3 format. Implicit conversion allows you to pass in a DynamicsParameters object.</param>
public void UpdateParameters(Vector3 v)
{
_parameters = v;
CalculateParameters();
}
/// <summary>
/// Lets you refresh parameters mid-simulation.
/// </summary>
public void UpdateParameters(float frequency, float damping, float response)
{
_parameters.x = frequency;
_parameters.y = damping;
_parameters.z = response;
CalculateParameters();
}
/// <summary>
/// Lets you change the frequency of the system mid-simulation.
/// </summary>
public void SetFrequency(float frequency)
{
_parameters.x = frequency;
CalculateParameters();
}
/// <summary>
/// Lets you change the damping of the system mid-simulation.
/// </summary>
public void SetDamping(float damping)
{
_parameters.y = damping;
CalculateParameters();
}
/// <summary>
/// Lets you change the response of the system mid-simulation.
/// </summary>
public void SetResponse(float response)
{
_parameters.z = response;
CalculateParameters();
}
private void CalculateParameters()
{
var f = _parameters.x;
var z = _parameters.y;
var r = _parameters.z;
k1 = z / (MathF.PI * f);
k2 = 1 / (2 * MathF.PI * f * (2 * MathF.PI * f));
k3 = r * z / (2 * MathF.PI * f);
}
/// <summary>
/// Sets system to a specified value, along with setting the velocity to zero.
/// </summary>
public abstract void Reset(T value);
/// <summary>
/// Update the system and retreive the new value.
/// </summary>
/// <param name="deltaTime">Time since last update, in seconds.</param>
/// <param name="target">The value you want to approach.</param>
/// <param name="setVelocity">If <c>true</c>, allows you to specify the velocity - useful to kick the system in one direction without changing the target.</param>
/// <param name="velocity">If <c>setVelocity</c> is <c>true</c>, this is the velocity to set.</param>
/// <returns></returns>
public abstract T Update(float deltaTime, T target, bool setVelocity = false, T velocity = default);
// If we had access to Microsoft.CSharp this would also contain the code to update the systems, using dynamic variables
// to allow me to do math with generic types - massively reducing code duplication. Oh well!
}
/// <summary>
/// Defines the behavior of a Second Order Dynamics system with simple and easy to control parameters.
/// </summary>
[Serializable]
public class DynamicsParameters
{
/// <summary>
/// The frequency of the system in Hz. The higher this is, the faster the system moves.
/// </summary>
public float Frequency { get; set; } = 1;
/// <summary>
/// Damping strength of the system.
/// <list type="bullet">
/// <item>At exactly 0, the system will never stop vibrating.</item>
/// <item>Between 0 and 1, you'll get a slowly settling spring motion.</item>
/// <item>1 is critical damping (like <c>MathX.SmoothDamp</c>)</item>
/// <item>Above 1 things start approaching slower and slower.</item>
/// </list>
/// </summary>
public float Damping { get; set; } = 1;
/// <summary>
/// Determines how the system responds to changing values.
/// <list type="bullet">
/// <item>Values < 0 will anticipate (go backwards before going forward - it's weird but cool!)</item>
/// <item>Values between 0 and 1 will start moving slowly.</item>
/// <item>If exactly 1, the system will start following the target value immediately.</item>
/// <item>Values above 1 will cause the system to overshoot - this is where the really cool motion happens!</item>
/// </list>
/// </summary>
public float Response { get; set; } = 1;
public DynamicsParameters(){}
public DynamicsParameters( float frequency, float response, float damping )
{
Frequency = frequency;
Response = response;
Damping = damping;
}
public static implicit operator Vector3(DynamicsParameters p) =>
new Vector3(p.Frequency, p.Damping, p.Response);
public static implicit operator DynamicsParameters(Vector3 p)
{
return new DynamicsParameters()
{
Frequency = p.x,
Damping = p.y,
Response = p.z
};
}
}