Car Suspension

Suspension

The idea

Create a simple suspension system that adjusts to the terrain with the help of a PID (proportional, integral, derivative) controller.

Engine

Unity


Language

C#


Duration

1 Week

 
using UnityEngine;

public class Movement : MonoBehaviour {
    private Transform transform;
    [SerializeField] private float moveSpeed = 1f;
	
    private void Start() {
        transform = GetComponent<Transform>();
    }

    private void Update() {
        if (Input.GetKey(KeyCode.W))
            transform.Translate(Time.deltaTime * moveSpeed * Vector3.forward);
        if (Input.GetKey(KeyCode.S))
            transform.Translate(Time.deltaTime * moveSpeed * -Vector3.forward);
        if (Input.GetKey(KeyCode.A))
            transform.Translate(Time.deltaTime * moveSpeed * -Vector3.right);
        if (Input.GetKey(KeyCode.D))
            transform.Translate(Time.deltaTime * moveSpeed * Vector3.right);
    }
}
using UnityEngine;

public class SphereFollow : MonoBehaviour {
    [SerializeField]private GameObject cube;

    private Transform cubeTransform;

    private void Start() {
        cubeTransform = cube.transform;
    }

    void Update() {
        Vector3 position = cubeTransform.position;
        position.y = transform.position.y;
        transform.position = position;
    }
}
	
using UnityEngine;

public class Suspension : MonoBehaviour {
    [SerializeField]private float freeLength = 1.2f;

    [SerializeField] private Transform contactTransform = null;
    private Rigidbody rb;
  
    [SerializeField]private float proportional = 1f;
    [SerializeField]private float integral = 1f;
    [SerializeField]private float derivative = 1f;
    private PIDController pidController = null;
    
    void Start() {
        rb = GetComponent<Rigidbody>();
        pidController = new PIDController(proportional, integral, derivative);
    }

    private void FixedUpdate() {
        rb.AddForce(TotalForce(), ForceMode.Force);
    }

    float CalculateLength() {
        return transform.position.y - contactTransform.position.y;
    }

    float LengthError() {
        return CalculateLength() - freeLength;
    }

    Vector3 TotalForce() {
        return -pidController.OutputValue(LengthError()) * Vector3.up;
    }
}
	
using UnityEngine;

public class PIDController {
    private float proportional = 1f;
    private float integral = 1f;
    private float derative = 1f;

    private float sumOfPastErrors = 0f;
    private float oldError = 0f;
    
    public PIDController(float p, float i, float d) {
        proportional = p;
        integral = i;
        derative = d;

        sumOfPastErrors = 0f;
    }

    public float OutputValue(float error) {
        float toReturn = 0f;

        toReturn += proportional * error;

        sumOfPastErrors += error * Time.fixedDeltaTime;
        toReturn += integral * sumOfPastErrors;

        float derativeError = (error - oldError) / Time.fixedDeltaTime;

        toReturn += derative * derativeError;
        oldError = error;
        
        return toReturn;
    }
}
	
using UnityEngine;

public class Body : MonoBehaviour {
    [SerializeField] private Transform backLeftSuspension, backRightSuspension, frontLeftSuspension, frontRightSuspension;

    private void Update() {
        Vector3 position = new Vector3();

        position = (backLeftSuspension.position + backRightSuspension.position + frontLeftSuspension.position + frontRightSuspension.position) / 4f;
        transform.position = position;
        transform.up = CalculateNormals();
    }

    Vector3 CalculateNormals() {
        Vector3 backLeftToBackRight = backLeftSuspension.position - backRightSuspension.position; 
        Vector3 frontRightToBackRight = frontRightSuspension.position - backRightSuspension.position;
        Vector3 frontRightToFrontLeft = frontRightSuspension.position - frontLeftSuspension.position;
        Vector3 backLeftToFrontLeft = backLeftSuspension.position - frontLeftSuspension.position;
        
        Vector3 cross = Vector3.Cross(backLeftToBackRight, frontRightToBackRight);
        Vector3 cross2 = Vector3.Cross(frontRightToFrontLeft, backLeftToFrontLeft);

        return ((cross + cross2)).normalized;
    }
}
	

Getting started

Starting up I set up simple movement to a cube and attached a sphere to follow the cube on the ground using gravity to follow the terrain.

Setting up the suspension

When the sphere follows the ball it was time to set up the suspension, the cube has a desired height it wants to keep from the sphere. When the cube is not at its desired height it adds a force to correct its position.



By tweaking the values sent into the PID controller you can controll the smoothness of the cubes movement.

There is also a possibility to add stiffness and dampening coefficient calculations to tweak the smoothness more.

Creating a body

Making four sets of suspension cubes and spheres as tires.

Adding a fifth larger cube that follows the positioning of the suspensions to act as a body.

Final Result

By crossing the four different suspenisons and setting the bodys up to the result the body follows the positioning of the suspensions and it looks like a suspension for a car.