using UnityEngine;
public class Suspension : MonoBehaviour
{
#region Variables
[Header("References")]
[SerializeField] private Rigidbody _rb;
[SerializeField] private Transform _wheel;
[Header("Details")]
[SerializeField] private float _offset = 0.1f;
[SerializeField] private float _maximumTravel = 0.5f;
[SerializeField] private float _maxForce = 20000f;
[SerializeField] private float _dampingForce = 500f;
[Curve(0f, 0f, 1f, 1f, true)]
[SerializeField] private AnimationCurve _forceCurve;
[HideInInspector] public bool isGrounded;
[HideInInspector] public float force;
private float _highestPoint;
private float _lowestPoint;
private float _lastUpdateProgression;
private float _wheelRadius;
#endregion
#region Unity functions
private void Start()
{
CalculateTravel();
CalculateWheelRadius();
}
private void Update()
{
ClampWheel();
}
private void FixedUpdate()
{
CalculateSuspension();
}
#endregion
#region Wheel
/// <summary>
/// Calculates the radius of the attached wheel
/// </summary>
private void CalculateWheelRadius()
{
_wheelRadius = _wheel.GetComponent<Renderer>().bounds.size.y;
}
/// <summary>
/// Update the position of the wheel to the ground
/// </summary>
private void UpdateWheelPosition(float progression)
{
var wheelPosition = _highestPoint - _maximumTravel * (1 - progression) + _wheelRadius / 2;
_wheel.localPosition = new Vector3(_wheel.localPosition.x, wheelPosition, _wheel.localPosition.z);
}
/// <summary>
/// Clamp the wheel within the suspension travel
/// </summary>
private void ClampWheel()
{
var clamp = Mathf.Clamp(_wheel.localPosition.y, _lowestPoint, _highestPoint);
_wheel.localPosition = new Vector3(_wheel.localPosition.x, clamp, _wheel.localPosition.z);
}
#endregion
#region Suspension
/// <summary>
/// Calculates the top and bottom point of the suspension
/// </summary>
private void CalculateTravel()
{
_highestPoint = _wheel.localPosition.y + _offset;
_lowestPoint = _highestPoint - _maximumTravel;
}
/// <summary>
/// Calculates and applies all forces of the suspension
/// </summary>
private void CalculateSuspension()
{
var (progression, hasContact) = CalculateProgression();
if (hasContact)
{
force = CalculateForce(progression);
ApplyForce(force);
isGrounded = true;
}
else
{
force = 0;
isGrounded = false;
}
UpdateWheelPosition(progression);
}
/// <summary>
/// Calculate the progression of the spring
/// </summary>
/// <returns> The progression </returns>
private (float, bool) CalculateProgression()
{
var suspensionWorldPosition = _rb.transform.TransformPoint(new Vector3(_wheel.localPosition.x, _highestPoint, _wheel.localPosition.z));
var raycastLength = _maximumTravel;
if (Physics.Raycast(suspensionWorldPosition, -_rb.transform.up, out var hit, raycastLength))
{
var progression = Vector3.Distance(suspensionWorldPosition, hit.point);
return (1 - progression / raycastLength, true);
}
return (0, false);
}
/// <summary>
/// Calculates the force that the spring pushes
/// </summary>
/// <param name="progression"> The progression within the spring </param>
/// <returns> The force it pushes with </returns>
private float CalculateForce(float progression)
{
var forceFactor = _forceCurve.Evaluate(progression);
var velocity = CalculateVelocity(progression);
return forceFactor * _maxForce + velocity * _dampingForce;
}
/// <summary>
/// Calculate the velocity of the tyre within the suspension
/// </summary>
/// <param name="progression"> The progression of the spring </param>
/// <returns> The velocity </returns>
private float CalculateVelocity(float progression)
{
var velocity = (progression - _lastUpdateProgression) / Time.fixedDeltaTime;
_lastUpdateProgression = progression;
return velocity;
}
/// <summary>
/// Applies the forces to the car
/// </summary>
/// <param name="force"> The force that should be applied </param>
private void ApplyForce(float force)
{
var localSuspensionPoint = new Vector3(_wheel.localPosition.x, _highestPoint, _wheel.localPosition.z);
var forcePoint = _rb.transform.TransformPoint(localSuspensionPoint);
_rb.AddForceAtPosition(_rb.transform.up * force, forcePoint);
}
#endregion
}