Code/Vehicle/VehicleController.cs
using System;
using System.Collections.Generic;
using Sandbox;
using Sandbox.VR;
namespace Meteor.VehicleTool.Vehicle;
[Category( "Physics" )]
[Title( "Vehicle Controller" )]
[EditorHandle( Icon = "directions_car" )]
[Icon( "directions_car" )]
public partial class VehicleController : Component
{
[Property]
[Hide]
[RequireComponent]
public Rigidbody Body { get; set; }
public float CurrentSpeed { get; private set; }
public Vector3 LocalVelocity { get; private set; }
private bool _showRigidBodyComponent;
[Property]
[Group( "Components" )]
[Title( "Show Rigidbody" )]
public bool ShowRigidbodyComponent
{
get => _showRigidBodyComponent;
set
{
_showRigidBodyComponent = value;
if ( Body.IsValid() )
Body.Flags = Body.Flags.WithFlag( ComponentFlags.Hidden, !value );
}
}
protected override void OnDisabled()
{
VerticalInput = 0;
Handbrake = 0;
SteeringAngle = 0;
CurrentSteerAngle = 0;
Throttle = 0;
Brakes = 0;
if ( UseSteering )
SetSteerAngle( 0 );
foreach ( var item in Wheels )
{
item.BrakeTorque = 200f;
item.MotorTorque = 0;
}
}
protected override void OnDestroy()
{
RemoveSounds();
}
protected override void OnAwake()
{
LoadSoundsAsync();
EnsureComponentsCreated();
Transmission.OnGearUpShift += OnGearUp;
Transmission.OnGearDownShift += OnGearDown;
}
protected override void OnStart()
{
base.OnStart();
FindWheels();
}
protected override void OnUpdate()
{
if ( UseAudio )
UpdateSound();
if ( IsProxy )
return;
if ( UseCameraControls )
UpdateCameraPosition();
if ( UseLookControls )
UpdateEyeAngles();
}
protected override void OnFixedUpdate()
{
if ( !IsProxy && UseInputControls )
UpdateInput();
LocalVelocity = WorldTransform.PointToLocal( Body.GetVelocityAtPoint( WorldPosition ) + WorldPosition );
CurrentSpeed = Body.Velocity.Length.InchToMeter();
if ( UseSteering )
UpdateSteerAngle();
if ( UsePowertrain )
UpdateBrakes();
if ( !IsOnGround )
AirControl();
SimulateAerodinamics();
}
[Property, Group( "Air Control" )] public float YawTorque { get; set; } = 10000;
[Property, Group( "Air Control" )] public float PitchTorque { get; set; } = 20000;
private void AirControl()
{
Vector3 torque = Vector3.Zero;
torque.y = VerticalInput * PitchTorque / Time.Delta;
torque.x = -SteeringAngle * YawTorque / Time.Delta;
Body.ApplyTorque( torque * WorldRotation );
}
private class DownforcePoint
{
public float MaxForce { get; set; }
public Vector3 Position { get; set; }
}
public const float RHO = 1.225f;
[Property, Group( "Aerodinamics" )] public Vector3 Dimensions = new( 2f, 4.5f, 1.5f );
[Property, Group( "Aerodinamics" )] public float FrontalCd { get; set; } = 0.35f;
[Property, Group( "Aerodinamics" )] public float SideCd { get; set; } = 1.05f;
[Property, Group( "Aerodinamics" )] public float MaxDownforceSpeed { get; set; } = 80f;
[Property, Group( "Aerodinamics" )] private List<DownforcePoint> DownforcePoints { get; set; } = [];
private float _forwardSpeed;
private float _frontalArea;
private float _sideArea;
private float _sideSpeed;
private float lateralDragForce;
private float longitudinalDragForce;
private void SimulateAerodinamics()
{
if ( CurrentSpeed < 1f )
{
longitudinalDragForce = 0;
lateralDragForce = 0;
return;
}
_frontalArea = Dimensions.x * Dimensions.z * 0.85f;
_sideArea = Dimensions.y * Dimensions.z * 0.8f;
_forwardSpeed = LocalVelocity.x.InchToMeter();
_sideSpeed = LocalVelocity.y.InchToMeter();
longitudinalDragForce = 0.5f * RHO * _frontalArea * FrontalCd * (_forwardSpeed * _forwardSpeed) * (_forwardSpeed > 0 ? -1f : 1f);
lateralDragForce = 0.5f * RHO * _sideArea * SideCd * (_sideSpeed * _sideSpeed) * (_sideSpeed > 0 ? -1f : 1f);
Body.ApplyForce( new Vector3( longitudinalDragForce.MeterToInch(), lateralDragForce.MeterToInch(), 0 ).RotateAround( Vector3.Zero, WorldRotation ) );
float speedPercent = CurrentSpeed / MaxDownforceSpeed;
float forceCoeff = 1f - (1f - MathF.Pow( speedPercent, 2f ));
foreach ( DownforcePoint dp in DownforcePoints )
Body.ApplyForceAt( Transform.World.PointToWorld( dp.Position ), forceCoeff.MeterToInch() * dp.MaxForce.MeterToInch() * -WorldRotation.Up );
}
}