Vehicle/VehicleController.Wheels.cs
using System.Linq;
using Sandbox;
namespace Bugges.VehicleController;
partial class VehicleController : Component
{
public const string WHEELS = "Wheels";
public enum WheelCastType { Ray, Sphere, Cylinder }
[Order( -8 )]
[Property( Name = "Cast Type" ), Feature( WHEELS )]
private WheelCastType Wheel_CastType { get; set; } = WheelCastType.Cylinder;
[Property( Name = "Max Turn Angle" ), Feature( WHEELS )]
private float Wheel_MaxTurnAngle { get; set; } = 30.0f;
[Property, Feature( WHEELS )]
private bool Wheel_VisualizeTrace { get; set; } = false;
[Feature( WHEELS ), InlineEditor, WideMode, Property]
private Wheel[] Wheels { get; set; }
private float GetAverageDrivenWheelRPS( out bool anyGrounded )
{
anyGrounded = false;
float totalAngularVelocity = 0f;
int motorCount = 0;
foreach ( var wheel in Wheels )
{
if ( !wheel.IsMotor ) continue;
motorCount++;
totalAngularVelocity += wheel.SpinSpeed / (2f * float.Pi);
if ( wheel.IsGrounded )
anyGrounded = true;
}
return motorCount > 0 ? totalAngularVelocity / motorCount : 0f;
}
private void ApplyTurn( Wheel wheel )
{
if ( !wheel.IsTurnable ) return;
float speed = Vector3.Dot( WorldTransform.Forward, Body.Velocity );
float normalizedSpeed = float.Clamp( float.Abs( speed ) / Engine_TopSpeed, 0.0f, 1.0f );
float turnMultiplier = float.Pow( 1.0f - normalizedSpeed, 2 );
float rawInput = TurnInput;
float turnInput = float.Sign( rawInput ) * rawInput * rawInput;
wheel.TurnAngle = turnInput * Wheel_MaxTurnAngle * turnMultiplier;
}
private void UpdateWheelSpin( Wheel wheel, SceneTraceResult trace )
{
if ( wheel.IsBraking )
{
wheel.SpinSpeed += (0f - wheel.SpinSpeed) * Time.Delta * 10f;
return;
}
if ( trace.Hit )
{
Vector3 contactPoint = GetWheelContactPoint( wheel, trace );
Vector3 tireWorldVel = Body.GetVelocityAtPoint( contactPoint );
float forwardVelocity = Vector3.Dot( wheel.Forward, tireWorldVel );
// Convert linear speed to angular velocity (radians per second)
wheel.SpinSpeed = forwardVelocity / wheel.Radius;
return;
}
if ( wheel.IsMotor && IsEngineOn && (Gear_Current == Gears.Drive || Gear_Current == Gears.Reverse) )
{
float gearMult = Gear_CurrentRatio * Gear_FinalDriveRatio;
if ( Gear_Current == Gears.Reverse ) gearMult *= -1f;
float targetSpinRpm = gearMult != 0f ? CurrentRPM / gearMult : 0f;
float targetSpinAngularVel = targetSpinRpm / 60f * (2f * float.Pi);
wheel.SpinSpeed += (targetSpinAngularVel - wheel.SpinSpeed) * Time.Delta * 5f;
return;
}
wheel.SpinSpeed += (0f - wheel.SpinSpeed) * Time.Delta * 1f;
}
private SceneTraceResult Trace( Wheel wheel )
{
SceneTraceResult trace = Wheel_CastType switch
{
WheelCastType.Cylinder => CylinderTrace( wheel ),
WheelCastType.Sphere => SphereTrace( wheel ),
_ => RayTrace( wheel )
};
return trace;
}
private SceneTraceResult RayTrace( Wheel wheel )
{
Vector3 springDown = wheel.Spring.WorldTransform.Down;
Vector3 springPos = wheel.Spring.WorldPosition;
var wheelRay = new Ray( springPos, springDown );
return Scene.Trace
.Ray( wheelRay, Spring_MaxDistance + wheel.Radius )
.IgnoreGameObject( GameObject )
.IgnoreGameObject( wheel.Visual )
.Run();
}
private SceneTraceResult SphereTrace( Wheel wheel )
{
Vector3 springDown = wheel.Spring.WorldTransform.Down;
Vector3 springPos = wheel.Spring.WorldPosition + springDown * wheel.Radius;
var wheelRay = new Ray( springPos, springDown );
return Scene.Trace
.Sphere( wheel.Radius, wheelRay, Spring_MaxDistance - wheel.Radius )
.IgnoreGameObject( GameObject )
.IgnoreGameObject( wheel.Visual )
.Run();
}
private SceneTraceResult CylinderTrace( Wheel wheel )
{
Vector3 springDown = wheel.Spring.WorldTransform.Down;
Vector3 springPos = wheel.Spring.WorldPosition + springDown * wheel.Radius;
var wheelRay = new Ray( springPos, springDown );
Rotation cylinderRotation = wheel.SteerRotation * Rotation.FromAxis( Vector3.Forward, 90f );
return Scene.Trace
.Cylinder( wheel.Width, wheel.Radius, wheelRay, Spring_MaxDistance - wheel.Radius )
.Rotated( cylinderRotation )
.IgnoreGameObject( GameObject )
.IgnoreGameObject( wheel.Visual )
.Run();
}
private float GetTraceDistance( Wheel wheel, SceneTraceResult trace )
{
return Wheel_CastType == WheelCastType.Ray
? float.Max( trace.Distance - wheel.Radius, 0.0f )
: trace.Distance + wheel.Radius;
}
private Vector3 GetWheelContactPoint( Wheel wheel, SceneTraceResult trace )
{
float traceDistance = GetTraceDistance( wheel, trace );
return wheel.Spring.WorldPosition + wheel.Down * traceDistance;
}
}