I created a custom car physics wich has suspension, torque forces, steering rotation and steering forces.
First Scenario:
The steering force method iIS NOT USED (line that calls HandleSteerPhysics commented), and the car drives with 0 grip, steering around by himself and when i drive him.
Second Scenario:
The steering force method IS USED, and the car becomes crazy bouncing in the world space.
- At this point i am definitly not sure where the problem is.
- Is it my car structure? the way i created the game objects hierarchy
- Is it the actual force?
- I’ll let references down here to show how is my code and how is my gameobjects configured
Tires and springs:
All springs: Strength 2000, Damping 150, rest distance 0.2, max distance 0.3
Front Tires: Grip 1, Max steer angle 35
Back Tires: Grip 1, Max steer angle 0
Images:
Code:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CarController : Mechanic
{
[Header("[Car Parts]")]
[SerializeField] List<WheelAssembly> wheelAssemblies;
[Header("[Car Movement]")]
[SerializeField] float acceleration;
[SerializeField] float maxTorque;
[SerializeField] float wheelBase;
[SerializeField] float trackWidth;
[SerializeField] LayerMask layerMask;
[SerializeField] bool isGrounded;
private Rigidbody rb;
void Start() {
rb = GetComponent<Rigidbody>();
}
void Update() {
HandleSteering();
}
void FixedUpdate() {
HandlePhysics();
}
float SuspensionMaxDist(SpringTip springTip, Tire tire) {
return springTip.maxDist + tire.mesh.bounds.size.y;
}
float SuspensionRestDist(SpringTip springTip, Tire tire) {
return springTip.restDist + tire.mesh.bounds.size.y;
}
void HandlePhysics() {
foreach (WheelAssembly WA in wheelAssemblies) {
SpringTip springTip = WA.springTip;
Tire tire = WA.tire;
float rayDistance = SuspensionMaxDist(springTip, tire);
Debug.DrawRay(springTip.transform.position, Vector3.down * rayDistance, Color.green);
if (Physics.Raycast(springTip.transform.position, -springTip.transform.up, out RaycastHit hit, rayDistance, layerMask)) {
isGrounded = true;
HandleSuspensionPhysics(springTip, tire, hit);
HandleTorquePhysics(tire, hit);
HandleSteerPhysics(tire, hit);
} else {
isGrounded = false;
}
}
}
void HandleSuspensionPhysics(SpringTip springTip, Tire tire, RaycastHit hit) {
Vector3 springVel = rb.GetPointVelocity(springTip.transform.position);
float projectedVelocity = Vector3.Dot(springTip.transform.up, springVel);
float springOffset = SuspensionRestDist(springTip, tire) - hit.distance;
float force = (springOffset * springTip.strength) - (projectedVelocity * springTip.damping);
rb.AddForceAtPosition(springTip.transform.up * force, springTip.transform.position);
}
// Basic newton force law
void HandleTorquePhysics(Tire tire, RaycastHit _) {
if (Mathf.Abs(rb.velocity.magnitude) < maxTorque) {
float accelerationInput = Input.GetAxis("Vertical");
float accelerationFactor = accelerationInput * acceleration;
float torque = rb.mass / wheelAssemblies.Count * accelerationFactor;
rb.AddForceAtPosition(tire.transform.forward * torque, tire.transform.position);
} else {
rb.velocity = rb.velocity.normalized * maxTorque;
}
}
void HandleSteerPhysics(Tire tire, RaycastHit _) {
Vector3 steerDirection = tire.transform.right;
Vector3 tireVelocity = rb.GetPointVelocity(tire.transform.position);
float steeringVelocity = Vector3.Dot(steerDirection, tireVelocity);
float desiredChangeVelocity = -steeringVelocity * tire.grip / Time.fixedDeltaTime;
Vector3 force = steerDirection * (rb.mass / wheelAssemblies.Count) * desiredChangeVelocity;
rb.AddForceAtPosition(force, tire.transform.position);
}
// Kinematics Ackerman Geometry
void HandleSteering() {
foreach (WheelAssembly WA in wheelAssemblies) {
Tire tire = WA.tire;
if (tire.position == Tire.Position.FL || tire.position == Tire.Position.FR) {
float deltaSin = Input.GetAxis("Horizontal");
float deltaAck = deltaSin * tire.maxSteerAngle * Mathf.Deg2Rad;
float tanDeltaAck = Mathf.Tan(deltaAck);
float deltaRight = Mathf.Atan( wheelBase * tanDeltaAck / (wheelBase - 0.5f * trackWidth * tanDeltaAck) ) * Mathf.Rad2Deg;
float deltaLeft = Mathf.Atan( wheelBase * tanDeltaAck / (wheelBase + 0.5f * trackWidth * tanDeltaAck) ) * Mathf.Rad2Deg;
if (tire.position == Tire.Position.FL) {
tire.transform.localRotation = Quaternion.Lerp(tire.transform.localRotation, Quaternion.Euler(tire.transform.localEulerAngles.x, deltaLeft, tire.transform.localEulerAngles.z), Time.deltaTime * 5);
} else if (tire.position == Tire.Position.FR) {
tire.transform.localRotation = Quaternion.Lerp(tire.transform.localRotation, Quaternion.Euler(tire.transform.localEulerAngles.x, deltaRight, tire.transform.localEulerAngles.z), Time.deltaTime * 5);
}
}
}
}
}
using System;
using UnityEngine;
[Serializable]
public struct WheelAssembly {
public SpringTip springTip;
public Tire tire;
}
[Serializable]
public struct SpringTip {
[SerializeField] public Transform transform;
[SerializeField] public float strength;
[SerializeField] public float damping;
[SerializeField] public float restDist;
[SerializeField] public float maxDist;
}
[Serializable]
public struct Tire {
[SerializeField] public Transform transform;
[SerializeField] public MeshRenderer mesh;
[SerializeField] public Position position;
[SerializeField] public float maxSteerAngle;
[SerializeField] [Range(0, 1)] public float grip;
public enum Position {
FL,
FR,
BL,
BR
}
}

