Hello everyone,

I am not a very experienced developer and I am trying to make a physics based car game. I have read this as my inspiration and guide, but have been stuck for a too long of a time on an issue:

GDSim v0.4a - Autocross and Custom Setups | GTPlanet (archive.org)

Previously whenever I would start my game the angular velocity of all wheels would just accelerate infinitely into negative or positive without any input. I have since rewritten everything trying to search for mistake(s), but right now experiencing issues not much different. I have managed to make the car stable without any input, but once there is gas input, the car first accelerates forward, inverses direction and starts rotating in all sorts of different ways uncontrollably. From my understanding the slip ratio calculation must be at fault, as that is the input feeding into the simplified Pacejka formula. The angular velocity is just a variable of the wheel script, which is defined through

`angularvelocity += Time.fixedDeltaTime * netTorque / momentOfInertia`

every `Fixed Update`

. I am not calculating lateral slip at all yet, trying to understand with what is wrong with the longitudinal force first.

I would really appreciate somebody experienced in this who knows better than me to explain this. I am mostly interested in,

### is this issue just a mistake I made or is there an entire procedure/calculation I just didn’t even know about?

My code for the wheel script:

```
private void Start()
{
rb = transform.root.GetComponent<Rigidbody>();
minLength = restLength - travel;
maxLength = restLength + travel;
wheelMoment = 0.5f * wheelMass * wheelRadius * wheelRadius;
}
private void FixedUpdate()
{
SimulateSuspension();
if (grounded)
{
ApplyForces();
CalculateNetTorque();
SimulateTire();
slip.y = AlternateSlipRatio();
}
else
{
slip = Vector2.zero;
}
localVelo = transform.InverseTransformDirection(rb.velocity);
forward = transform.TransformDirection(transform.localRotation * Vector3.forward);
}
private void SimulateTire()
{
tireForce.x = Pacejka(slip.x, xShape);
tireForce.z = Pacejka(slip.y, yShape);
//slip.x = SlipAngle();
}
private void CalculateNetTorque()
{
netTorque = tireForce.z * wheelRadius;
netTorque += motorTorque;
if (Mathf.Abs(angularVelocity) < 5 && brakeTorque > Mathf.Abs(netTorque))
{
angularVelocity = 0;
}
else
{
netTorque -= brakeTorque * Mathf.Sign(angularVelocity);
netTorque -= rollingResistanceForce * Mathf.Sign(angularVelocity);
angularVelocity += Time.fixedDeltaTime * netTorque / wheelMoment;
}
}
float AlternateSlipRatio()
{
const float fullSlipVelo = 4.0f;
const float minVelocityThreshold = 0.1f; // Threshold for low-speed adjustments
const float minSpeedForSlip = 1e-5f; // Minimum speed to consider for slip calculation
// Calculate the component of the local velocity in the direction the wheel is facing
float wheelRoadVelo = Vector3.Dot(localVelo, transform.forward);
// Absolute value of road velocity
float absRoadVelo = Mathf.Abs(wheelRoadVelo);
// Calculate the linear velocity at the circumference of the wheel
float wheelTireVelo = angularVelocity * wheelRadius;
// Handle near-zero road velocity separately to avoid instability
if (absRoadVelo < minVelocityThreshold)
{
// Use a simplified slip calculation when the vehicle is nearly stationary
return Mathf.Clamp(wheelTireVelo / fullSlipVelo, -1.0f, 1.0f);
}
// Calculate the damping factor based on the road velocity
float damping = Mathf.Clamp01(absRoadVelo / fullSlipVelo);
// Calculate the slip ratio
float slipRatio = (wheelTireVelo - wheelRoadVelo) / absRoadVelo * damping;
// Clamp the slip ratio to avoid excessive values
return Mathf.Clamp(slipRatio, -1.0f, 1.0f);
}
private float SlipRatio()
{
float ySlip = 0;
if (Mathf.Abs(localVelo.z) > 0.00001f)
{
ySlip = angularVelocity * wheelRadius - localVelo.z / Mathf.Abs(localVelo.z);
Debug.Log("Local velocity on z axis is bigger than the theshold");
}
Debug.Log($"The localVelo.z = {localVelo.z}, localVelo.z > threshold = {localVelo.z > 1e-5f}");
Debug.Log(ySlip);
return ySlip;
}
float SlipAngle()
{
const float fullAngleVelo = 2.0f;
Vector3 wheelMotionDirection = localVelo;
wheelMotionDirection.y = 0;
if (wheelMotionDirection.sqrMagnitude < Mathf.Epsilon)
return 0;
float sinSlipAngle = wheelMotionDirection.normalized.x;
Mathf.Clamp(sinSlipAngle, -1, 1); // To avoid precision errors.
float damping = Mathf.Clamp01(localVelo.magnitude / fullAngleVelo);
return -Mathf.Asin(sinSlipAngle) * damping * damping;
}
private float Pacejka(float slip, float shape)
{
return suspensionForce * peak * Mathf.Sin(shape * Mathf.Atan(tireStiffness * slip - curve * (tireStiffness * slip - Mathf.Atan(tireStiffness * slip))));
}
private void SimulateSuspension()
{
if (Physics.Raycast(transform.position, -transform.up, out hitInfo, wheelRadius + maxLength))
{
grounded = true;
lastLength = length;
length = hitInfo.distance - wheelRadius;
length = Mathf.Clamp(length, minLength, maxLength);
compression = 1 - length / maxLength;
springVelo = (lastLength - length) / Time.fixedDeltaTime;
hitNormal = hitInfo.normal;
springForce = springStiffness * (restLength - length);
if (springVelo > 0) // If the spring is compressing, use the bump shock absorber stiffness
{
damperForce = bump * springVelo;
}
if (springVelo < 0) // If the spring is decompressing, use the rebound shock absorber stiffness
{
damperForce = rebound * springVelo;
}
suspensionForce = (springForce + damperForce);
}
else
{
grounded = false;
}
}
private void ApplyForces()
{
rb.AddForceAtPosition(suspensionForce * hitNormal, hitPoint);
rb.AddForceAtPosition(tireForce, hitPoint);
}
private void OnDrawGizmosSelected()
{
if (Physics.Raycast(transform.position, -transform.up, out RaycastHit hit, maxLength + wheelRadius))
{
Debug.DrawRay(transform.position, -transform.up, Color.red);
}
else
{
Debug.DrawRay(transform.position, -transform.up, Color.green);
}
}
```