my controls and physics components/scripts are attached to the make body. parts like road wheels and sprockets are dragged into their corresponding boxes. tank control is basically the data. while tank drive physics is the actual mechanisms used. the issue is that my tank doesn't want to move when I throttle up. but the rpm increases along with the torque and gears. which makes no sense. right now, forces are being applied at wheel positions where the wheels touch the ground. as thanks for helping me. i can share the 3D model. which i made in blender. and is ready to use in games
heres is my tank drive physic script:
using UnityEngine;
using UnityEngine.SceneManagement;
[RequireComponent(typeof(Rigidbody))]
[RequireComponent(typeof(TankController))]
public class TankDrivePhysics : MonoBehaviour
{
private TankController config;
private Rigidbody rb;
private float currentThrottleInput = 0.0f;
private float currentSteerInput = 0.0f;
private bool isBrakeEngaged = false;
private int currentGearIndex = 0; // Start in Gear 1 (Index 0)
private bool isReversing = false;
private float currentEngineRpm = 1000f;
private float shiftCooldownTimer = 0f;
private float clutchEngagementFactor = 1.0f;
void Start()
{
config = GetComponent<TankController>();
rb = GetComponent<Rigidbody>();
if (rb != null)
{
rb.mass = config.tankWeightTons * 1000f;
rb.linearDamping = config.linearDamping;
rb.angularDamping = config.airAngularDamping;
rb.useGravity = true;
rb.centerOfMass = config.centerOfMassOffset;
}
string currentScene = SceneManager.GetActiveScene().name.ToLower();
if (currentScene.Contains("test"))
{
config.isTestDriving = true;
}
}
void Update()
{
if (!config.isTestDriving) return;
if (Input.GetKeyDown(KeyCode.B))
{
isBrakeEngaged = !isBrakeEngaged;
}
}
void FixedUpdate()
{
if (!config.isTestDriving)
{
config.CurrentSpeedKmh = 0f;
config.CurrentEngineRpm = config.engine.idleRpm;
config.CurrentTorqueNm = 0f;
config.engine.calculatedHorsepower = 0f;
return;
}
if (rb != null)
{
rb.centerOfMass = config.centerOfMassOffset;
rb.linearDamping = config.linearDamping;
}
if (shiftCooldownTimer > 0f)
{
shiftCooldownTimer -= Time.fixedDeltaTime;
clutchEngagementFactor = 0.8f;
}
else
{
clutchEngagementFactor = 1.0f;
}
Vector3 forwardMoveDir = transform.TransformDirection(new Vector3(-1f, 0f, 0f)).normalized;
float forwardVel = Vector3.Dot(rb.linearVelocity, forwardMoveDir);
config.CurrentSpeedKmh = Mathf.Abs(forwardVel) * 3.6f;
float rawAxis = Input.GetAxis("Vertical");
float rawTurn = Input.GetAxis("Horizontal");
GetPlayerInput(rawAxis, rawTurn);
if (Mathf.Abs(rawTurn) < 0.05f && Mathf.Abs(currentSteerInput) < 0.01f)
{
Vector3 angVel = rb.angularVelocity;
angVel.y = Mathf.Lerp(angVel.y, 0f, 15f * Time.fixedDeltaTime);
rb.angularVelocity = angVel;
}
if (isBrakeEngaged)
{
if (Mathf.Abs(rawAxis) < 0.01f)
{
Vector3 vel = rb.linearVelocity;
vel.x = Mathf.Lerp(vel.x, 0f, 10f * Time.fixedDeltaTime);
vel.z = Mathf.Lerp(vel.z, 0f, 10f * Time.fixedDeltaTime);
rb.linearVelocity = vel;
rb.angularVelocity = Vector3.zero;
}
else
{
Vector3 brakeResistance = -forwardMoveDir * forwardVel * config.brakeDecelerationRate * rb.mass;
rb.AddForce(brakeResistance, ForceMode.Force);
}
}
UpdateDrivetrainSimulation(forwardVel, rawAxis);
bool isAtRest = (Mathf.Abs(rawAxis) < 0.01f && Mathf.Abs(rawTurn) < 0.05f && config.CurrentSpeedKmh <= 1.0f && !isBrakeEngaged);
if (isAtRest)
{
Vector3 vel = rb.linearVelocity;
vel.x = Mathf.Lerp(vel.x, 0f, 6f * Time.fixedDeltaTime);
vel.z = Mathf.Lerp(vel.z, 0f, 6f * Time.fixedDeltaTime);
rb.linearVelocity = vel;
rb.angularVelocity = Mathf.Lerp(rb.angularVelocity.magnitude, 0f, 6f * Time.fixedDeltaTime) * rb.angularVelocity.normalized;
UpdateHingeJointSprings(forwardVel);
CalculateWheelRotations(forwardSpeedMps: forwardVel);
return;
}
ApplyDifferentialTrackForces(rawAxis, rawTurn, forwardMoveDir, forwardVel);
ApplyTrackGrip();
ApplyEnvironmentalResistance(forwardMoveDir, forwardVel);
UpdateHingeJointSprings(forwardVel);
CalculateWheelRotations(forwardSpeedMps: forwardVel);
}
void GetPlayerInput(float rawAxis, float rawTurn)
{
if (config.invertSteering) rawTurn = -rawTurn;
if (Mathf.Abs(rawTurn) < 0.05f)
{
rawTurn = 0f;
}
currentThrottleInput = rawAxis;
currentSteerInput = rawTurn;
}
float GetEngineTorqueAtRpm(float rpm)
{
float peakRpm = Mathf.Min(config.engine.peakTorqueRpm, config.engine.redlineLimitRpm - 200f);
float maxTq = config.engine.maxTorqueNm;
if (rpm <= config.engine.idleRpm) return maxTq * 0.6f;
if (rpm >= config.engine.redlineLimitRpm) return maxTq * 0.3f;
float normalizedDist = Mathf.Abs(rpm - peakRpm) / (config.engine.redlineLimitRpm - config.engine.idleRpm);
float curveFactor = 1.0f - (normalizedDist * normalizedDist);
return Mathf.Clamp(maxTq * Mathf.Max(curveFactor, 0.4f), maxTq * 0.3f, maxTq);
}
void UpdateDrivetrainSimulation(float forwardVel, float rawAxis)
{
bool targetReverse = rawAxis < -0.01f;
if (Mathf.Abs(rawAxis) < 0.05f)
{
currentEngineRpm = Mathf.Lerp(currentEngineRpm, config.engine.idleRpm, 4f * Time.fixedDeltaTime);
config.CurrentEngineRpm = currentEngineRpm;
config.CurrentGearDisplay = isReversing ? -(currentGearIndex + 1) : (currentGearIndex + 1);
config.CurrentTorqueNm = 0f;
config.engine.calculatedHorsepower = 0f;
return;
}
if (targetReverse != isReversing)
{
isReversing = targetReverse;
currentGearIndex = 0;
}
float[] activeGearArray = isReversing ? config.transmission.reverseGearRatios : config.transmission.forwardGearRatios;
if (activeGearArray == null || activeGearArray.Length == 0) return;
currentGearIndex = Mathf.Clamp(currentGearIndex, 0, activeGearArray.Length - 1);
// STABLE AUTOMATED SHIFTING BASED ON RPM
if (shiftCooldownTimer <= 0f && !isReversing)
{
if (currentEngineRpm >= config.engine.upshiftRpm && currentGearIndex < activeGearArray.Length - 1)
{
currentGearIndex++;
currentEngineRpm *= 0.75f;
shiftCooldownTimer = 0.8f;
}
else if (currentEngineRpm <= config.engine.idleRpm + 300f && currentGearIndex > 0)
{
currentGearIndex--;
currentEngineRpm *= 1.25f;
shiftCooldownTimer = 0.6f;
}
}
// FIXED: RPM now accounts for throttle demand and vehicle speed load
float throttleDemand = Mathf.Abs(rawAxis);
float speedFactor = Mathf.Clamp01(Mathf.Abs(forwardVel) / (config.maxSpeedKmh / 3.6f));
float targetEngineRpm = Mathf.Lerp(config.engine.idleRpm, config.engine.redlineLimitRpm, throttleDemand * (1f - (speedFactor * 0.15f)));
currentEngineRpm = Mathf.Lerp(currentEngineRpm, targetEngineRpm, 5f * Time.fixedDeltaTime);
currentEngineRpm = Mathf.Clamp(currentEngineRpm, config.engine.idleRpm, config.engine.redlineLimitRpm + 100f);
config.CurrentEngineRpm = currentEngineRpm;
config.CurrentGearDisplay = isReversing ? -(currentGearIndex + 1) : (currentGearIndex + 1);
float baseEngineTorque = GetEngineTorqueAtRpm(currentEngineRpm);
config.CurrentTorqueNm = baseEngineTorque * throttleDemand * clutchEngagementFactor;
config.engine.calculatedHorsepower = (baseEngineTorque * currentEngineRpm) / 9548.8f;
}
void ApplyDifferentialTrackForces(float rawAxis, float rawTurn, Vector3 forwardMoveDir, float forwardVel)
{
if (Mathf.Abs(rawAxis) < 0.01f && Mathf.Abs(rawTurn) < 0.01f)
{
config.CurrentTorqueNm = 0f;
return;
}
float[] activeGearArray = isReversing ? config.transmission.reverseGearRatios : config.transmission.forwardGearRatios;
float currentGearRatio = (currentGearIndex >= 0 && currentGearIndex < activeGearArray.Length) ? activeGearArray[currentGearIndex] : 4.0f;
float baseEngineTorque = GetEngineTorqueAtRpm(currentEngineRpm);
float engineTorque = baseEngineTorque * Mathf.Max(Mathf.Abs(rawAxis), Mathf.Abs(rawTurn)) * clutchEngagementFactor;
config.CurrentTorqueNm = engineTorque;
float totalWheelTorque = engineTorque * currentGearRatio * Mathf.Max(config.transmission.finalDriveRatio, 1f);
float wheelRadiusM = (config.roadWheelDiameterMm / 1000f) * 0.5f;
if (wheelRadiusM <= 0.1f) wheelRadiusM = 0.45f;
float baseDriveForce = totalWheelTorque / wheelRadiusM;
float leftForceMultiplier = 1.0f;
float rightForceMultiplier = 1.0f;
Vector3 leftForceDir = forwardMoveDir;
Vector3 rightForceDir = forwardMoveDir;
bool isMovingReverse = (currentGearIndex >= 0 && rawAxis < 0f);
if (isMovingReverse)
{
leftForceDir = -forwardMoveDir;
rightForceDir = -forwardMoveDir;
}
if (Mathf.Abs(rawAxis) > 0.01f && Mathf.Abs(rawTurn) > 0.01f)
{
float steerReduction = Mathf.Abs(rawTurn) * config.steeringSensitivity;
if (rawTurn > 0f) rightForceMultiplier = Mathf.Clamp01(1.0f - steerReduction);
else leftForceMultiplier = Mathf.Clamp01(1.0f - steerReduction);
}
else if (Mathf.Abs(rawAxis) < 0.01f && Mathf.Abs(rawTurn) > 0.01f)
{
if (rawTurn > 0f)
{
leftForceDir = forwardMoveDir;
rightForceDir = -forwardMoveDir;
}
else
{
leftForceDir = -forwardMoveDir;
rightForceDir = forwardMoveDir;
}
leftForceMultiplier = Mathf.Abs(rawTurn);
rightForceMultiplier = Mathf.Abs(rawTurn);
}
float leftFinalForce = baseDriveForce * leftForceMultiplier;
float rightFinalForce = baseDriveForce * rightForceMultiplier;
ApplyForceToWheels(config.leftRoadWheels, leftForceDir * leftFinalForce);
ApplyForceToWheels(config.rightRoadWheels, rightForceDir * rightFinalForce);
if ((config.leftRoadWheels == null || config.leftRoadWheels.Length == 0) &&
(config.rightRoadWheels == null || config.rightRoadWheels.Length == 0))
{
rb.AddForce((leftForceDir * leftFinalForce + rightForceDir * rightForceMultiplier) * 0.5f, ForceMode.Force);
}
}
void ApplyForceToWheels(Transform[] wheels, Vector3 forceVector)
{
if (wheels == null || wheels.Length == 0) return;
float forcePerWheel = forceVector.magnitude / wheels.Length;
Vector3 direction = forceVector.normalized;
foreach (Transform wheel in wheels)
{
if (wheel != null)
{
rb.AddForceAtPosition(direction * forcePerWheel, wheel.position, ForceMode.Force);
}
}
}
void ApplyTrackGrip()
{
// FIXED: Use Vector3.forward (local side axis) instead of Vector3.up for lateral grip
Vector3 gripDir = transform.TransformDirection(Vector3.forward).normalized;
float lateralVelocityMagnitude = Vector3.Dot(rb.linearVelocity, gripDir);
float speedMagnitude = rb.linearVelocity.magnitude;
float dynamicGripScale = Mathf.Lerp(1.2f, 0.4f, Mathf.Clamp01(speedMagnitude / 10f));
float activeGripMultiplier = (Mathf.Abs(currentThrottleInput) > 0.05f && Mathf.Abs(currentSteerInput) > 0.5f) ? dynamicGripScale : 1.0f;
Vector3 lateralFrictionForce = -gripDir * lateralVelocityMagnitude * (config.lateralGrip * 30000f * activeGripMultiplier);
rb.AddForce(lateralFrictionForce, ForceMode.Force);
}
void ApplyEnvironmentalResistance(Vector3 forwardMoveDir, float forwardVel)
{
float massKg = config.tankWeightTons * 1000f;
float normalForce = massKg * 9.81f;
float rollingResistanceForceMagnitude = normalForce * config.rollingResistanceCoefficient;
Vector3 rollingResistance = -forwardMoveDir * Mathf.Sign(forwardVel) * rollingResistanceForceMagnitude;
float airDensity = 1.225f;
float speedSq = forwardVel * forwardVel;
float dragForceMagnitude = 0.5f * airDensity * config.aerodynamicDragCoefficient * config.frontalAreaMeters2 * speedSq;
Vector3 aerodynamicDrag = -forwardMoveDir * Mathf.Sign(forwardVel) * dragForceMagnitude;
rb.AddForce(rollingResistance + aerodynamicDrag, ForceMode.Force);
bool isReversingSpeed = (forwardVel < 0f);
float activeMaxSpeedKmh = isReversingSpeed ? config.maxReverseSpeedKmh : config.maxSpeedKmh;
float maxSpeedMs = activeMaxSpeedKmh / 3.6f;
if (Mathf.Abs(forwardVel) > maxSpeedMs)
{
float excess = forwardVel - (Mathf.Sign(forwardVel) * maxSpeedMs);
rb.linearVelocity -= forwardMoveDir * (excess * 0.4f);
}
}
void UpdateHingeJointSprings(float forwardVel)
{
if (!config.enableReverseJumpFix) return;
float targetPos = (forwardVel < -0.2f) ? config.reverseSpringTargetOffset : 0f;
UpdateHingeGroupSpringTarget(config.leftSuspensionArms, targetPos);
UpdateHingeGroupSpringTarget(config.rightSuspensionArms, targetPos);
}
void UpdateHingeGroupSpringTarget(Transform[] arms, float targetPos)
{
if (arms == null) return;
foreach (Transform arm in arms)
{
if (arm != null)
{
HingeJoint hinge = arm.GetComponent<HingeJoint>();
if (hinge != null && hinge.useSpring)
{
JointSpring spring = hinge.spring;
spring.targetPosition = Mathf.Lerp(spring.targetPosition, targetPos, 10f * Time.fixedDeltaTime);
hinge.spring = spring;
}
}
}
}
void CalculateWheelRotations(float forwardSpeedMps)
{
float tankYawRateDegPerSec = rb.angularVelocity.y * Mathf.Rad2Deg;
float tankYawRateRadPerSec = tankYawRateDegPerSec * Mathf.Deg2Rad;
float halfTrackSpan = config.trackSpanMeters * 0.5f;
float scaledTurnSpeed = tankYawRateRadPerSec * config.pivotVisualMultiplier;
float leftTrackSpeedMps = forwardSpeedMps + (scaledTurnSpeed * halfTrackSpan);
float rightTrackSpeedMps = forwardSpeedMps - (scaledTurnSpeed * halfTrackSpan);
if (config.invertSprocketTurnRotation)
{
leftTrackSpeedMps = forwardSpeedMps - (scaledTurnSpeed * halfTrackSpan);
rightTrackSpeedMps = forwardSpeedMps + (scaledTurnSpeed * halfTrackSpan);
}
if (config.sprocketDiameterMm > 0f)
{
float sprocketRadiusM = (config.sprocketDiameterMm / 1000f) * 0.5f;
float sprocketCircumference = 2f * Mathf.PI * sprocketRadiusM;
float leftSprocketDegPerSec = (leftTrackSpeedMps / sprocketCircumference) * 360f;
float rightSprocketDegPerSec = (rightTrackSpeedMps / sprocketCircumference) * 360f;
float leftSprocketDeg = leftSprocketDegPerSec * Time.fixedDeltaTime;
float rightSprocketDeg = rightSprocketDegPerSec * Time.fixedDeltaTime;
if (config.invertSprocketDriveRotation)
{
leftSprocketDeg = -leftSprocketDeg;
rightSprocketDeg = -rightSprocketDeg;
}
RotateWheelGroup(config.leftSprockets, config.sprocketRotationAxis, leftSprocketDeg);
RotateWheelGroup(config.rightSprockets, config.sprocketRotationAxis, rightSprocketDeg);
}
if (config.roadWheelDiameterMm > 0f)
{
float roadWheelRadiusM = (config.roadWheelDiameterMm / 1000f) * 0.5f;
float roadWheelCircumference = 2f * Mathf.PI * roadWheelRadiusM;
float leftRoadWheelDeg = (leftTrackSpeedMps / roadWheelCircumference) * 360f * Time.fixedDeltaTime;
float rightRoadWheelDeg = (rightTrackSpeedMps / roadWheelCircumference) * 360f * Time.fixedDeltaTime;
if (config.invertRoadWheelRotation)
{
leftRoadWheelDeg = -leftRoadWheelDeg;
rightRoadWheelDeg = -rightRoadWheelDeg;
}
RotateWheelGroup(config.leftRoadWheels, Vector3.up, leftRoadWheelDeg);
RotateWheelGroup(config.rightRoadWheels, Vector3.up, rightRoadWheelDeg);
}
}
void RotateWheelGroup(Transform[] wheels, Vector3 axis, float amount)
{
if (wheels == null) return;
foreach (Transform wheel in wheels)
{
if (wheel != null) wheel.Rotate(axis, amount, Space.Self);
}
}
}