Code/Vehicle/Wheel/WheelCollider.cs
using System;
using Sandbox;
namespace Meteor.VehicleTool.Vehicle.Wheel;
[Category( "Physics" )]
[Title( "Wheel Collider" )]
[Icon( "sports_soccer" )]
public partial class WheelCollider : Component
{
private float wheelRadius = 14;
private float mass = 20;
[Property] private ModelCollider BottomMeshCollider { get; set; }
[Property] private ModelCollider TopMeshCollider { get; set; }
[Property] private GameObject ColliderGO { get; set; }
[Group( "Properties" ), Property, Sync]
public float Radius
{
get => wheelRadius;
set
{
wheelRadius = value;
UpdatePhysicalProperties();
}
}
[Button]
internal void CreateColliders()
{
using var undo = Scene.Editor.UndoScope( "Create Colliders" ).WithComponentCreations().WithComponentChanges( this ).Push();
ColliderGO = new GameObject( GameObject, true, "Colliders" );
BottomMeshCollider = ColliderGO.AddComponent<ModelCollider>();
TopMeshCollider = ColliderGO.AddComponent<ModelCollider>();
UpdatePhysicalProperties();
}
private void UpdatePhysicalProperties()
{
Inertia = 0.5f * Mass * (wheelRadius.InchToMeter() * wheelRadius.InchToMeter());
if ( BottomMeshCollider != null )
{
float radiusUndersizing = Math.Clamp( wheelRadius.InchToMeter() * 0.05f, 0, 0.025f ).MeterToInch();
float widthUndersizing = Math.Clamp( Width.InchToMeter() * 0.05f, 0, 0.025f ).MeterToInch();
BottomMeshCollider.Model = CreateWheelMesh(
Radius - radiusUndersizing,
Width - widthUndersizing, false );
BottomMeshCollider.Friction = 0;
}
if ( TopMeshCollider != null )
{
float oversizing = Math.Clamp( Radius.InchToMeter() * 0.1f, 0, 0.1f ).MeterToInch();
TopMeshCollider.Model = CreateWheelMesh(
Radius + oversizing,
Width + oversizing, true );
TopMeshCollider.Friction = 0;
}
}
[Group( "Properties" ), Property, Sync] public float Width { get; set; } = 6;
[Group( "Properties" ), Property, Sync]
public float Mass
{
get => mass;
set
{
mass = value;
UpdatePhysicalProperties();
}
}
[Group( "Properties" ), Property, ReadOnly] public float Inertia { get; set; }
[Group( "Components" ), Property] public VehicleController Controller { get; set; }
public bool AutoSimulate = true;
private Rigidbody CarBody => Controller.Body;
protected override void OnAwake()
{
Controller ??= Components.Get<VehicleController>( FindMode.InAncestors );
}
protected override void OnStart()
{
base.OnStart();
velocityRotation = LocalRotation;
}
protected override void OnEnabled()
{
UpdatePhysicalProperties();
UpdateTotalSuspensionLength();
SuspensionLength = suspensionTotalLength / 2;
Controller?.Register( this );
}
protected override void OnDisabled()
{
Controller?.UnRegister( this );
}
protected override void OnDestroy()
{
Controller?.UnRegister( this );
}
protected override void OnFixedUpdate()
{
if ( AutoSimulate )
PhysUpdate( Time.Delta );
}
protected override void OnUpdate()
{
if ( UseVisual )
UpdateVisual();
}
public void PhysUpdate( float dt )
{
DoTrace();
ColliderGO.WorldPosition = GetCenter();
ColliderGO.WorldRotation = TransformRotationSteer;
axleAngle = AngularVelocity.RadianToDegree() * dt;
Scene.PhysicsWorld.PositionIterations = 10;
var bottomMeshColliderEnabled = false;
//Check for high vertical velocity and enable the collider if above one frame travel distance
float thresholdVelocity = suspensionTotalLength < 1e-5f ? float.MinValue : -suspensionTotalLength / dt;
float relativeYVelocity = Controller.LocalVelocity.z;
if ( relativeYVelocity < thresholdVelocity )
bottomMeshColliderEnabled = true;
Scene.PhysicsWorld.PositionIterations = 100;
if ( BottomMeshCollider.IsValid() )
BottomMeshCollider.Enabled = bottomMeshColliderEnabled || SuspensionLength == 0;
if ( !bottomMeshColliderEnabled )
UpdateSuspension( dt );
UpdateSteer();
UpdateHitVariables();
UpdateFriction( dt );
}
}