Hello, I recently created a new Unity project. I created a car object and gave it four children Objects with wheel colliders and one box collider child. The Car parent object itself has a Rigidbody of course.
I created a script that steers the wheels and adds torque to them.
I’d like you to look at this before I’m gonna explain further:
As you can see in the video, the car is veering. The reason for this is that one of the wheels (the front left in this case [the left one right in front of the camera]) somehow has no grip - while the other wheels slowly start to roll, the front left instantly has a massive rpm count. This isn’t a problem of the WheelCollider though, since all four share the same stats. I noticed that this wheelspin always happens with the first WheelCollider the motor torque gets applied to.
I tried finding the problem within my script, yet I couldn’t find anything that could be the reason for this.
Here is the class that applies the forces to the car. I know it’s a bit of a mess but I can’t find the reason why the first wheel the forces get applied to always does this wheelspin:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class WheelManager : MonoBehaviour
{
[Header("Stats:")]
[SerializeField] float maxMotorTorque = 100;
[SerializeField] float backwardsTorque = 50;
[SerializeField] float brakeForce = 90;
[SerializeField] float maxSteerAngle = 45f;
[SerializeField, Tooltip("The steering angle range at the top speed.")] float steerAngleMaxSpeed = 5;
[Header("Acceleration details:")]
[SerializeField] AnimationCurve curve;
[SerializeField, Tooltip("Needed to calculate velocity of the car.")] float wheelRadius;
[SerializeField, Tooltip("In meters per second (m/s).")] float topSpeed;
private float currentSpeed;
private float currentRpm;
[Header("Wheels")]
[SerializeField] Wheel[] wheels;
[Header("Input Variables:"), Tooltip("Only can be used to manuever the car if no input script is conrtolling it.")]
public bool forwards;
public bool backwards;
public bool braking;
[Range(-1, 1)] public float steerAngle;
void Start()
{
}
void FixedUpdate()
{
currentRpm = GetCurrentRpm(wheels);
currentSpeed = VelocityInMetersPerSecond(currentRpm, wheelRadius);
AccelerationManager();
SteeringAngles();
}
private void AccelerationManager()
{
// this state manager is built like a hierachy
// each if statement has return; at its end, so that e.g. braking overwrites accelerating and accelerating overwrites driving backwards
// this means that if the player hits the backwards and forwards key at the same time, the car will drive forwards, but if he brakes now, the car will brake instead of accelerating
foreach (Wheel wheel in wheels)
{
if (braking)
{
// brake, do not accelerate more
// if(braking) is put here so that it overwrites the acceleration
Brake(wheel);
return;
}
if(forwards && currentSpeed < topSpeed)
{
// drive forwards
// is put here so that braking can overwrite it, but that it still is handled above driving backwards
Accelerate(wheel);
return;
}
if(backwards)
{
// the last statement, will be overwritten by all other statements
BwAcceleration(wheel);
return;
}
wheel.wheelCol.motorTorque = 0f;
wheel.wheelCol.brakeTorque = 0f;
}
}
private void SteeringAngles()
{
foreach (Wheel wheel in wheels)
{
if (wheel.canSteer)
{
float normalizedSpeed = Mathf.Clamp01(Mathf.Abs(currentSpeed) / topSpeed);
float currentSteerRange = Mathf.Lerp(maxSteerAngle, steerAngleMaxSpeed, normalizedSpeed);
wheel.wheelCol.steerAngle = currentSteerRange * steerAngle;
}
}
}
#region Acceleration & Braking Functions
private void Accelerate(Wheel wheel)
{
wheel.wheelCol.brakeTorque = 0;
wheel.wheelCol.motorTorque = TorqueToAdd(currentSpeed, maxMotorTorque);
}
private void BwAcceleration(Wheel wheel)
{
wheel.wheelCol.brakeTorque = 0;
wheel.wheelCol.motorTorque = -TorqueToAdd(currentSpeed, backwardsTorque);
}
private void Brake(Wheel wheel)
{
wheel.wheelCol.motorTorque = 0;
wheel.wheelCol.brakeTorque = brakeForce;
}
#endregion
#region Miscellanios Functions
private float VelocityInMetersPerSecond(float tireRpm, float tireRadius)
{
return (((Mathf.PI * 2) * tireRadius) / 60) * tireRpm;
}
private float GetCurrentRpm(Wheel[] wheel)
{
float addedSpeeds = 0;
foreach (Wheel wheelItem in wheel)
{
addedSpeeds += wheelItem.wheelCol.rpm;
}
float averageSpeeds = addedSpeeds / wheels.Length;
return averageSpeeds;
}
private float TorqueToAdd(float currentSpeed, float maxTourque)
{
float normalizedSpeed = Mathf.Clamp01(Mathf.Abs(currentSpeed) / topSpeed);
return curve.Evaluate(normalizedSpeed) * maxTourque;
}
#endregion
}
The “Wheel” class referenced in the script is a class I made just for storing some variables:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[System.Serializable]
public class Wheel
{
public bool canSteer;
public WheelCollider wheelCol;
public GameObject wheelMesh;
}
As mentioned earlier, all WheelColliders share the same stats.
I am really clueless to what the issue is. I know some people advise to making your own car system, but I do not have the knowledge to make one myself, so if you could help that’d be awesome.
Thanks in advance and I’ll try to answer questions as good as I can.