prerelease

This commit is contained in:
2025-11-06 12:13:30 +07:00
parent 0905876b99
commit ae5cd2c8b6
48 changed files with 3753 additions and 1404 deletions

View File

@@ -1,43 +0,0 @@
using Sandbox;
using Sandbox.Services;
using System;
using System.Threading;
namespace VeloX;
public class Pacejka
{
public class PacejkaPreset
{
[KeyProperty] public float B { get; set; } = 10.86f;
[KeyProperty] public float C { get; set; } = 2.15f;
[KeyProperty] public float D { get; set; } = 0.933f;
[KeyProperty] public float E { get; set; } = 0.992f;
public float Evaluate( float slip ) => D * MathF.Sin( C * MathF.Atan( B * slip - E * (B * slip - MathF.Atan( B * slip )) ) );
public float GetPeakSlip()
{
float peakSlip = -1;
float yMax = 0;
for ( float i = 0; i < 1f; i += 0.01f )
{
float y = Evaluate( i );
if ( y > yMax )
{
yMax = y;
peakSlip = i;
}
}
return peakSlip;
}
}
public PacejkaPreset Lateral { get; set; } = new();
public PacejkaPreset Longitudinal { get; set; } = new();
public float PacejkaFx( float slip ) => Longitudinal.Evaluate( slip );
public float PacejkaFy( float slip ) => Lateral.Evaluate( slip );
}

View File

@@ -1,18 +0,0 @@
namespace VeloX;
public class BasicSuspension
{
private readonly Hinge Hinge;
public BasicSuspension( Vector3 wheel, Vector3 hingeBody, Vector3 hingeWheel )
{
Vector3 hingePoint = wheel - (hingeWheel - hingeBody);
Hinge = new Hinge( hingePoint, hingeWheel - hingeBody );
}
public virtual void GetWheelTransform( float travel, out Rotation rotation, out Vector3 position )
{
rotation = Rotation.Identity;
position = Hinge.Rotate( travel );
}
}

View File

@@ -1,26 +0,0 @@
using System;
namespace VeloX;
internal readonly struct Hinge( Vector3 hinge_anchor, Vector3 hinge_arm )
{
[Description( "the point that the wheels are rotated around as the suspension compresses" )]
public readonly Vector3 Anchor = hinge_anchor;
[Description( "anchor to wheel vector" )]
public readonly Vector3 Arm = hinge_arm;
[Description( "arm length squared" )]
public readonly float LengthSquared = hinge_arm.Dot( hinge_arm );
[Description( "1 / arm length in hinge axis normal plane" )]
public readonly float NormXY = 1 / MathF.Sqrt( hinge_arm.x * hinge_arm.x + hinge_arm.y * hinge_arm.y );
public readonly Vector3 Rotate( float travel )
{
float z = Arm.z + travel;
float lengthSq = MathF.Max( LengthSquared - z * z, 0.0f );
float nxy = NormXY * MathF.Sqrt( lengthSq );
return Anchor + new Vector3( Arm.x * nxy, Arm.y * nxy, z );
}
}

View File

@@ -1,32 +0,0 @@
using Sandbox;
using System;
namespace VeloX;
public class MacPhersonSuspension
{
public readonly Vector3 WheelOffset;
public readonly Vector3 UprightTop;
public readonly Vector3 UprightAxis;
private readonly Hinge Hinge;
public MacPhersonSuspension( Vector3 wheel, Vector3 strutBody, Vector3 strutWheel, Vector3 hingeBody )
{
WheelOffset = wheel - strutWheel;
UprightTop = strutBody;
UprightAxis = (strutBody - strutWheel).Normal;
Hinge = new( hingeBody, strutWheel - hingeBody );
}
public void GetWheelTransform( float travel, out Rotation rotation, out Vector3 position )
{
Vector3 hingeEnd = Hinge.Rotate( travel );
Vector3 uprightAxisNew = (UprightTop - hingeEnd).Normal;
rotation = Rotation.FromAxis(
Vector3.Cross( UprightAxis, uprightAxisNew ),
MathF.Acos( Vector3.Dot( UprightAxis, uprightAxisNew ) ).RadianToDegree()
);
position = hingeEnd + WheelOffset.Transform( rotation );
}
}

View File

@@ -4,57 +4,45 @@ using System;
namespace VeloX;
[GameResource( "Wheel Friction", "tire", "Wheel Friction", Category = "VeloX", Icon = "radio_button_checked" )]
[AssetType( Name = "Wheel Friction", Extension = "tire", Category = "VeloX" )]
public class TirePreset : GameResource
{
public static TirePreset Default { get; } = ResourceLibrary.Get<TirePreset>( "frictions/default.tire" );
[Property] public Pacejka Pacejka { get; set; }
private float peakSlip = -1;
public float RollResistanceLin { get; set; } = 1E-3f;
public float RollResistanceQuad { get; set; } = 1E-6f;
[Property] public float B { get; set; } = 10.86f;
[Property] public float C { get; set; } = 2.15f;
[Property] public float D { get; set; } = 0.933f;
[Property] public float E { get; set; } = 0.992f;
public float GetRollingResistance( float velocity, float resistance_factor )
{ // surface influence on rolling resistance
float resistance = resistance_factor * RollResistanceLin;
// heat due to tire deformation increases rolling resistance
// approximate by quadratic function
resistance += velocity * velocity * RollResistanceQuad;
return resistance;
}
public static void ComputeSlip( float lon_velocity, float lat_velocity, float rot_velocity, float wheel_radius, out float slip_ratio, out float slip_angle )
public float GetPeakSlip()
{
var abs_lon = Math.Max( MathF.Abs( lon_velocity ), 1e-3f );
slip_ratio = lon_velocity - rot_velocity * wheel_radius;
if ( abs_lon >= 0.005f )
slip_ratio /= abs_lon;
else
slip_ratio *= abs_lon;
if ( abs_lon >= 0.5f )
slip_angle = MathF.Atan2( -lat_velocity, abs_lon ).RadianToDegree() / 50f;
else
slip_angle = -lat_velocity * (0.01f / Time.Delta);
slip_ratio = Math.Clamp( slip_ratio, -1, 1 );
slip_angle = Math.Clamp( slip_angle, -1, 1 );
if ( peakSlip == -1 )
peakSlip = CalcPeakSlip();
return peakSlip;
}
/// approximate asin(x) = x + x^3/6 for +-18 deg range
public static float ComputeCamberAngle( float sin_camber )
public float Evaluate( float t ) => D * MathF.Sin( C * MathF.Atan( B * t - E * (B * t - MathF.Atan( B * t )) ) );
private float CalcPeakSlip()
{
float sc = Math.Clamp( sin_camber, -0.3f, 0.3f );
return ((1 / 6.0f) * (sc * sc) + 1) * sc;
float peakSlip = -1;
float yMax = 0;
for ( float i = 0; i < 1f; i += 0.01f )
{
float y = Evaluate( i );
if ( y > yMax )
{
yMax = y;
peakSlip = i;
}
}
return peakSlip;
}
public static float ComputeCamberVelocity( float sa, float vx )
{
float tansa = (1 / 3.0f * (sa * sa) + 1) * sa;
return tansa * vx;
}
}

View File

@@ -0,0 +1,299 @@
using Sandbox;
using System;
namespace VeloX;
public partial class VeloXWheel
{
/// <summary>
/// Constant torque acting similar to brake torque.
/// Imitates rolling resistance.
/// </summary>
[Property, Range( 0, 500 ), Sync] public float RollingResistanceTorque { get; set; } = 30f;
/// <summary>
/// The percentage this wheel is contributing to the total vehicle load bearing.
/// </summary>
public float LoadContribution { get; set; } = 0.25f;
/// <summary>
/// Maximum load the tire is rated for in [N].
/// Used to calculate friction.Default value is adequate for most cars but
/// larger and heavier vehicles such as semi trucks will use higher values.
/// A good rule of the thumb is that this value should be 2x the Load
/// while vehicle is stationary.
/// </summary>
[Property, Sync] public float LoadRating { get; set; } = 5400;
/// <summary>
/// The amount of torque returned by the wheel.
/// Under no-slip conditions this will be equal to the torque that was input.
/// When there is wheel spin, the value will be less than the input torque.
/// </summary>
public float CounterTorque { get; private set; }
[Property, Sync] public bool AutoSetFriction { get; set; } = true;
[Property, Sync] public bool UseGroundVelocity { get; set; } = true;
[Property, Range( 0, 2 )] public float BrakeMult { get; set; } = 1f;
[Property] public Friction ForwardFriction = new();
[Property] public Friction SidewayFriction = new();
public Vector3 FrictionForce;
private Vector3 hitContactVelocity;
private Vector3 hitForwardDirection;
private Vector3 hitSidewaysDirection;
public Vector3 ContactRight => hitSidewaysDirection;
public Vector3 ContactForward => hitForwardDirection;
[Sync]
public float LongitudinalSlip
{
get => ForwardFriction.Slip;
private set => ForwardFriction.Slip = value;
}
public float LongitudinalSpeed => ForwardFriction.Speed;
public bool IsSkiddingLongitudinally => NormalizedLongitudinalSlip > 0.35f;
public float NormalizedLongitudinalSlip => Math.Clamp( Math.Abs( LongitudinalSlip ), 0, 1 );
[Sync]
public float LateralSlip
{
get => SidewayFriction.Slip;
private set => SidewayFriction.Slip = value;
}
public float LateralSpeed => SidewayFriction.Speed;
public bool IsSkiddingLaterally => NormalizedLateralSlip > 0.35f;
public float NormalizedLateralSlip => Math.Clamp( Math.Abs( LateralSlip ), 0, 1 );
public bool IsSkidding => IsSkiddingLaterally || IsSkiddingLongitudinally;
public float NormalizedSlip => (NormalizedLateralSlip + NormalizedLongitudinalSlip) / 2f;
private void UpdateHitVariables()
{
if ( IsOnGround )
{
hitContactVelocity = Vehicle.Body.GetVelocityAtPoint( ContactPosition + Vehicle.Body.MassCenter );
hitForwardDirection = ContactNormal.Cross( TransformRotationSteer.Right ).Normal;
hitSidewaysDirection = Rotation.FromAxis( ContactNormal, 90f ) * hitForwardDirection;
ForwardFriction.Speed = hitContactVelocity.Dot( hitForwardDirection ).InchToMeter();
SidewayFriction.Speed = hitContactVelocity.Dot( hitSidewaysDirection ).InchToMeter();
}
else
{
ForwardFriction.Speed = 0f;
SidewayFriction.Speed = 0f;
}
}
private Vector3 lowSpeedReferencePosition;
private bool lowSpeedReferenceIsSet;
private Vector3 currentPosition;
private Vector3 referenceError;
private Vector3 correctiveForce;
private void UpdateFriction( float dt )
{
var motorTorque = DriveTorque;
var brakeTorque = BrakeTorque * BrakeMult;
float allWheelLoadSum = Vehicle.CombinedLoad;
LoadContribution = allWheelLoadSum == 0 ? 1f : Fz / allWheelLoadSum;
float mRadius = Radius;
float invDt = 1f / dt;
float invRadius = 1f / mRadius;
float inertia = Inertia;
float invInertia = 1f / Inertia;
float loadClamped = Math.Clamp( Fz, 0, LoadRating );
float forwardLoadFactor = loadClamped * 1.35f;
float sideLoadFactor = loadClamped * 1.9f;
float loadPercent = Math.Clamp( Fz / LoadRating, 0f, 1f );
float slipLoadModifier = 1f - loadPercent * 0.4f;
//DebugOverlay.Text( WorldPosition, SidewayFriction.Speed.ToString(), overlay: true );
float mass = Vehicle.Body.Mass;
float absForwardSpeed = Math.Abs( ForwardFriction.Speed );
float forwardForceClamp = mass * LoadContribution * absForwardSpeed * invDt;
float absSideSpeed = Math.Abs( SidewayFriction.Speed );
float sideForceClamp = mass * LoadContribution * absSideSpeed * invDt;
float forwardSpeedClamp = 1.5f * (dt / 0.005f);
forwardSpeedClamp = Math.Clamp( forwardSpeedClamp, 1.5f, 10f );
float clampedAbsForwardSpeed = Math.Max( absForwardSpeed, forwardSpeedClamp );
// Calculate effect of camber on friction
float camberFrictionCoeff = Math.Max( 0, Vehicle.WorldRotation.Up.Dot( ContactNormal ) );
float peakForwardFrictionForce = forwardLoadFactor;
float absCombinedBrakeTorque = Math.Max( 0, brakeTorque + RollingResistanceTorque );
float signedCombinedBrakeTorque = absCombinedBrakeTorque * -Math.Sign( ForwardFriction.Speed );
float signedCombinedBrakeForce = signedCombinedBrakeTorque * invRadius;
float motorForce = motorTorque * invRadius;
float forwardInputForce = motorForce + signedCombinedBrakeForce;
float absMotorTorque = Math.Abs( motorTorque );
float absBrakeTorque = Math.Abs( brakeTorque );
float maxForwardForce = Tire.Evaluate( Math.Abs( ForwardFriction.Slip ) ) * Math.Min( peakForwardFrictionForce, forwardForceClamp );
maxForwardForce = absMotorTorque < absBrakeTorque ? maxForwardForce : peakForwardFrictionForce;
ForwardFriction.Force = forwardInputForce > maxForwardForce ? maxForwardForce
: forwardInputForce < -maxForwardForce ? -maxForwardForce : forwardInputForce;
bool wheelIsBlocked = false;
if ( IsOnGround )
{
float combinedWheelForce = motorForce + absCombinedBrakeTorque * invRadius * -Math.Sign( AngularVelocity );
float wheelForceClampOverflow = 0;
if ( (combinedWheelForce >= 0 && AngularVelocity < 0) || (combinedWheelForce < 0 && AngularVelocity > 0) )
{
float absWheelForceClamp = Math.Abs( AngularVelocity ) * inertia * invRadius * invDt;
float absCombinedWheelForce = combinedWheelForce < 0 ? -combinedWheelForce : combinedWheelForce;
float wheelForceDiff = absCombinedWheelForce - absWheelForceClamp;
wheelForceClampOverflow = Math.Max( 0, wheelForceDiff ) * Math.Sign( combinedWheelForce );
combinedWheelForce = Math.Clamp( combinedWheelForce, -absWheelForceClamp, absWheelForceClamp );
}
AngularVelocity += combinedWheelForce * mRadius * invInertia * dt;
// Surface (corrective) force
float noSlipAngularVelocity = ForwardFriction.Speed * invRadius;
float angularVelocityError = AngularVelocity - noSlipAngularVelocity;
float angularVelocityCorrectionForce = Math.Clamp( -angularVelocityError * inertia * invRadius * invDt, -maxForwardForce, maxForwardForce );
if ( absMotorTorque < absBrakeTorque && Math.Abs( wheelForceClampOverflow ) > Math.Abs( angularVelocityCorrectionForce ) )
{
wheelIsBlocked = true;
AngularVelocity += ForwardFriction.Speed > 0 ? 1e-10f : -1e-10f;
}
else
{
AngularVelocity += angularVelocityCorrectionForce * mRadius * invInertia * dt;
}
}
else
{
float maxBrakeTorque = AngularVelocity * inertia * invDt + motorTorque;
maxBrakeTorque = maxBrakeTorque < 0 ? -maxBrakeTorque : maxBrakeTorque;
float brakeTorqueSign = AngularVelocity < 0f ? -1f : 1f;
float clampedBrakeTorque = Math.Clamp( absCombinedBrakeTorque, -maxBrakeTorque, maxBrakeTorque );
AngularVelocity += (motorTorque - brakeTorqueSign * clampedBrakeTorque) * invInertia * dt;
}
float absAngularVelocity = AngularVelocity < 0 ? -AngularVelocity : AngularVelocity;
float maxCounterTorque = inertia * absAngularVelocity;
CounterTorque = Math.Clamp( (signedCombinedBrakeForce - ForwardFriction.Force) * mRadius, -maxCounterTorque, maxCounterTorque );
ForwardFriction.Slip = (ForwardFriction.Speed - AngularVelocity * mRadius) / clampedAbsForwardSpeed;
ForwardFriction.Slip *= slipLoadModifier;
SidewayFriction.Slip = MathF.Atan2( SidewayFriction.Speed, clampedAbsForwardSpeed ).RadianToDegree() * 0.01111f;
SidewayFriction.Slip *= slipLoadModifier;
float sideSlipSign = SidewayFriction.Slip > 0 ? 1 : -1;
float absSideSlip = Math.Abs( SidewayFriction.Slip );
float peakSideFrictionForce = sideLoadFactor;
float sideForce = -sideSlipSign * Tire.Evaluate( absSideSlip ) * sideLoadFactor;
SidewayFriction.Force = Math.Clamp( sideForce, -sideForceClamp, sideForceClamp );
SidewayFriction.Force *= camberFrictionCoeff;
if ( IsOnGround && absForwardSpeed < 0.12f && absSideSpeed < 0.12f )
{
float verticalOffset = RestLength + mRadius;
var transformPosition = WorldPosition;
var transformUp = TransformRotationSteer.Up;
currentPosition.x = transformPosition.x - transformUp.x * verticalOffset;
currentPosition.y = transformPosition.y - transformUp.y * verticalOffset;
currentPosition.z = transformPosition.z - transformUp.z * verticalOffset;
if ( !lowSpeedReferenceIsSet )
{
lowSpeedReferenceIsSet = true;
lowSpeedReferencePosition = currentPosition;
}
else
{
referenceError.x = lowSpeedReferencePosition.x - currentPosition.x;
referenceError.y = lowSpeedReferencePosition.y - currentPosition.y;
referenceError.z = lowSpeedReferencePosition.z - currentPosition.z;
correctiveForce.x = invDt * LoadContribution * mass * referenceError.x;
correctiveForce.y = invDt * LoadContribution * mass * referenceError.y;
correctiveForce.z = invDt * LoadContribution * mass * referenceError.z;
if ( wheelIsBlocked && absAngularVelocity < 0.5f )
{
ForwardFriction.Force += correctiveForce.Dot( hitForwardDirection );
}
SidewayFriction.Force += correctiveForce.Dot( hitSidewaysDirection );
}
}
else
{
lowSpeedReferenceIsSet = false;
}
ForwardFriction.Force = Math.Clamp( ForwardFriction.Force, -peakForwardFrictionForce, peakForwardFrictionForce );
SidewayFriction.Force = Math.Clamp( SidewayFriction.Force, -peakSideFrictionForce, peakSideFrictionForce );
if ( absForwardSpeed > 2f || absAngularVelocity > 4f )
{
float forwardSlipPercent = ForwardFriction.Slip / Tire.GetPeakSlip();
float sideSlipPercent = SidewayFriction.Slip / Tire.GetPeakSlip();
float slipCircleLimit = MathF.Sqrt( forwardSlipPercent * forwardSlipPercent + sideSlipPercent * sideSlipPercent );
if ( slipCircleLimit > 1f )
{
float beta = MathF.Atan2( sideSlipPercent, forwardSlipPercent * 0.9f );
float sinBeta = MathF.Sin( beta );
float cosBeta = MathF.Cos( beta );
float absForwardForce = ForwardFriction.Force < 0 ? -ForwardFriction.Force : ForwardFriction.Force;
float absSideForce = SidewayFriction.Force < 0 ? -SidewayFriction.Force : SidewayFriction.Force;
float f = absForwardForce * cosBeta * cosBeta + absSideForce * sinBeta * sinBeta;
ForwardFriction.Force = 0.5f * ForwardFriction.Force - f * cosBeta;
SidewayFriction.Force = 0.5f * SidewayFriction.Force - f * sinBeta;
}
}
if ( IsOnGround )
{
FrictionForce.x = (hitSidewaysDirection.x * SidewayFriction.Force + hitForwardDirection.x * ForwardFriction.Force).MeterToInch();
FrictionForce.y = (hitSidewaysDirection.y * SidewayFriction.Force + hitForwardDirection.y * ForwardFriction.Force).MeterToInch();
FrictionForce.z = (hitSidewaysDirection.z * SidewayFriction.Force + hitForwardDirection.z * ForwardFriction.Force).MeterToInch();
//DebugOverlay.Normal( WorldPosition, hitSidewaysDirection * 10, overlay: true );
//DebugOverlay.Normal( WorldPosition, hitForwardDirection * 10, overlay: true );
//DebugOverlay.Normal( WorldPosition, FrictionForce / 100, overlay: true );
//DebugOverlay.Normal( ContactPosition, ContactNormal * 10, overlay: true );
//DebugOverlay.Sphere( new( ContactPosition, 4 ), overlay: true );
//Vehicle.Body.ApplyForceAt( ContactPosition, FrictionForce );
}
else
FrictionForce = Vector3.Zero;
}
}

View File

@@ -18,7 +18,7 @@ public partial class VeloXWheel : Component
//
{
var suspensionStart = Vector3.Zero;
var suspensionEnd = Vector3.Zero + Vector3.Down * SuspensionLength;
var suspensionEnd = Vector3.Zero + Vector3.Down * RestLength.MeterToInch();
Gizmo.Draw.Color = Color.Cyan;
Gizmo.Draw.LineThickness = 0.25f;
@@ -28,7 +28,7 @@ public partial class VeloXWheel : Component
Gizmo.Draw.Line( suspensionStart + Vector3.Forward, suspensionStart + Vector3.Backward );
Gizmo.Draw.Line( suspensionEnd + Vector3.Forward, suspensionEnd + Vector3.Backward );
}
var widthOffset = Vector3.Right * Width * 0.5f;
var widthOffset = Vector3.Right * Width.MeterToInch() * 0.5f;
//
// Wheel radius
//
@@ -36,7 +36,7 @@ public partial class VeloXWheel : Component
Gizmo.Draw.LineThickness = 0.5f;
Gizmo.Draw.Color = Color.White;
Gizmo.Draw.LineCylinder( widthOffset, -widthOffset, Radius, Radius, 16 );
Gizmo.Draw.LineCylinder( widthOffset, -widthOffset, Radius.MeterToInch(), Radius.MeterToInch(), 16 );
}
//
@@ -51,11 +51,11 @@ public partial class VeloXWheel : Component
for ( float i = 0; i < 16; i++ )
{
var pos = circlePosition + Vector3.Up.RotateAround( Vector3.Zero, new Angles( i / 16 * 360, 0, 0 ) ) * Radius;
var pos = circlePosition + Vector3.Up.RotateAround( Vector3.Zero, new Angles( i / 16 * 360, 0, 0 ) ) * Radius.MeterToInch();
Gizmo.Draw.Line( new Line( pos - widthOffset, pos + widthOffset ) );
var pos2 = circlePosition + Vector3.Up.RotateAround( Vector3.Zero, new Angles( (i + 1) / 16 * 360, 0, 0 ) ) * Radius;
var pos2 = circlePosition + Vector3.Up.RotateAround( Vector3.Zero, new Angles( (i + 1) / 16 * 360, 0, 0 ) ) * Radius.MeterToInch();
Gizmo.Draw.Line( pos - widthOffset, pos2 + widthOffset );
}
}
@@ -64,7 +64,7 @@ public partial class VeloXWheel : Component
//// Forward direction
////
//{
// var arrowStart = Vector3.Forward * Radius;
// var arrowStart = Vector3.Forward * Radius.MeterToInch();
// var arrowEnd = arrowStart + Vector3.Forward * 8f;
// Gizmo.Draw.Color = Color.Red;

View File

@@ -0,0 +1,89 @@
using Sandbox;
using System.Collections.Generic;
namespace VeloX;
public partial class VeloXWheel
{
[ConCmd( "clear_skids" )]
public static void ClearSkids()
{
while ( SkidMarks.Count > 1 )
SkidMarks.Dequeue()?.DestroyGameObject();
}
[ConVar( "skidmark_max_skid" )]
public static float MaxSkid { get; set; } = 50.0f;
[ConVar( "skidmark_min_slide" )]
public static float MinSlide { get; set; } = 0.1f;
private static readonly Queue<LineRenderer> SkidMarks = [];
private LineRenderer _skidMark;
private void ResetSkid()
{
_skidMark = null;
}
private void CreateSkid()
{
GameObject go = new()
{
WorldPosition = ContactPosition + Vehicle.WorldRotation.Down * (Radius.MeterToInch() - 0.01f),
WorldRotation = Rotation.LookAt( hitSidewaysDirection )
};
_skidMark = go.AddComponent<LineRenderer>();
_skidMark.Face = SceneLineObject.FaceMode.Normal;
_skidMark.Points = [go];
_skidMark.Color = Color.Black.WithAlpha( 0.5f );
_skidMark.Opaque = false;
_skidMark.CastShadows = false;
_skidMark.Width = Width.MeterToInch() / 2;
_skidMark.AutoCalculateNormals = false;
_skidMark.SplineInterpolation = 4;
go.Flags = go.Flags.WithFlag( GameObjectFlags.Hidden, true );
SkidMarks.Enqueue( _skidMark );
}
protected void UpdateSkid()
{
if ( IsProxy )
return;
while ( SkidMarks.Count > MaxSkid )
{
SkidMarks.Dequeue()?.DestroyGameObject();
}
if ( !IsOnGround )
{
ResetSkid();
return;
}
var slideAmount = NormalizedSlip;
if ( slideAmount < MinSlide * 2 )
{
ResetSkid();
return;
}
if ( !_skidMark.IsValid() )
{
CreateSkid();
}
GameObject go = new()
{
Parent = _skidMark.GameObject,
WorldPosition = ContactPosition + Vehicle.WorldRotation.Down * Radius.MeterToInch(),
WorldRotation = Rotation.LookAt( ContactNormal.RotateAround( Vector3.Zero, Rotation.FromRoll( 90 ) ) )
};
go.Flags = go.Flags.WithFlag( GameObjectFlags.Hidden, true );
_skidMark.Points.Add( go );
}
}

View File

@@ -0,0 +1,109 @@
using Sandbox;
using System;
using System.Collections.Generic;
using System.Threading;
namespace VeloX;
public partial class VeloXWheel
{
private static readonly GameObject SmokePrefab = GameObject.GetPrefab( "prefabs/particles/tire_smoke.prefab" );
private GameObject SmokeObject { get; set; }
public const float MIN_DRIFT_ANGLE = 10f;
public const float MIN_DRIFT_SPEED = 30f;
public const float MAX_DRIFT_ANGLE = 110f;
private float smokeMul;
private float timeMul;
protected override void OnStart()
{
base.OnStart();
if ( IsProxy )
return;
if ( !SmokeObject.IsValid() )
SmokeObject = GameObject.GetPrefab( "prefabs/particles/tire_smoke.prefab" ).Clone( new CloneConfig() { Parent = GameObject, StartEnabled = true } );
}
public float GetSlip()
{
if ( !IsOnGround )
return 0;
var val = Math.Abs( LateralSlip ) + Math.Abs( LongitudinalSlip );
timeMul = timeMul.LerpTo( val, 0.1f );
if ( timeMul > 2 )
return val;
return 0;
}
protected override void OnUpdate()
{
base.OnUpdate();
if ( IsProxy )
return;
UpdateSkid();
SmokeObject.WorldPosition = ContactPosition + Vehicle.WorldRotation.Down * Radius.MeterToInch();
smokeMul = Math.Max( 0, GetSlip() - 3 );
bool density = false;
if ( smokeMul > 0 )
density = true;
var emitter = SmokeObject.Components.Get<ParticleSphereEmitter>( FindMode.EverythingInSelfAndDescendants );
emitter.Enabled = density;
emitter.Radius = 0f;
emitter.Velocity = 0;
emitter.Duration = 5;
emitter.Burst = 5;
float sizeMul = 0.7f + smokeMul * 0.3f;
emitter.Rate = smokeMul * 100f;
emitter.RateOverDistance = smokeMul * 10f;
var effect = SmokeObject.Components.Get<ParticleEffect>( FindMode.EverythingInSelfAndDescendants );
effect.MaxParticles = 500;
effect.Damping = 0.9f;
effect.ApplyRotation = true;
effect.ApplyShape = true;
effect.Roll = new()
{
Type = ParticleFloat.ValueType.Range,
Evaluation = ParticleFloat.EvaluationType.Seed,
ConstantA = 0,
ConstantB = 360,
};
effect.Scale = new()
{
Type = ParticleFloat.ValueType.Curve,
Evaluation = ParticleFloat.EvaluationType.Life,
CurveA = new( new List<Curve.Frame>() { new( 0, 10f ), new( 0.8f, 50f ), new( 1f, 160f * sizeMul ) } ),
};
effect.StartDelay = 0.025f + (1 - smokeMul) * 0.03f;
effect.ApplyColor = true;
effect.Gradient = new()
{
Type = ParticleGradient.ValueType.Range,
Evaluation = ParticleGradient.EvaluationType.Particle,
ConstantA = Color.White,
ConstantB = Color.Transparent,
};
effect.StartVelocity = new()
{
Type = ParticleFloat.ValueType.Range,
Evaluation = ParticleFloat.EvaluationType.Seed,
ConstantA = 10,
ConstantB = 70,
};
effect.Force = true;
effect.InitialVelocity = hitForwardDirection * LongitudinalSlip * 10f;
effect.ForceDirection = 0;
effect.SheetSequence = true;
effect.SequenceSpeed = 0.5f;
effect.SequenceTime = 1f;
}
}

View File

@@ -1,16 +1,5 @@
using Sandbox;
using Sandbox.Rendering;
using Sandbox.Services;
using Sandbox.UI;
using System;
using System.Collections.Specialized;
using System.Diagnostics.Metrics;
using System.Numerics;
using System.Text.RegularExpressions;
using System.Threading;
using static Sandbox.CameraComponent;
using static Sandbox.Package;
using static Sandbox.SkinnedModelRenderer;
namespace VeloX;
@@ -18,72 +7,67 @@ namespace VeloX;
[Title( "VeloX - Wheel" )]
public partial class VeloXWheel : Component
{
[Property, Group( "Suspension" )] public float Radius { get; set; } = 0.35f;
[Property, Group( "Suspension" )] public float Width { get; set; } = 0.1f;
[Property] public float Mass { get; set; } = 5;
[Property, Group( "Suspension" )] float RestLength { get; set; } = 0.22f;
[Property, Group( "Suspension" )] public float SpringStiffness { get; set; } = 20000.0f;
[Property, Group( "Suspension" )] float ReboundStiffness { get; set; } = 2200;
[Property, Group( "Suspension" )] float CompressionStiffness { get; set; } = 2400;
[Property, Group( "Suspension" )] float BumpStopStiffness { get; set; } = 5000;
[Property] public float Radius { get; set; } = 15;
[Property] public float Mass { get; set; } = 20;
[Property] public float RollingResistance { get; set; } = 20;
[Property] public float SlipCircleShape { get; set; } = 1.05f;
[Property] public TirePreset TirePreset { get; set; }
[Property] public float Width { get; set; } = 6;
public float SideSlip { get; private set; }
public float ForwardSlip { get; private set; }
[Property, Group( "Traction" )] public TirePreset Tire { get; set; } = ResourceLibrary.Get<TirePreset>( "frictions/default.tire" );
[Property, Group( "Traction" )] public float SurfaceGrip { get; set; } = 1f;
[Property, Group( "Traction" )] public float SurfaceResistance { get; set; } = 0.05f;
public bool AutoSimulate = true;
public float BaseInertia => Mass * (Radius * Radius); // kg·m²
[Property] public float Inertia { get; set; } = 1.5f; // kg·m²
public float SideSlip => SlipAngle;
public float ForwardSlip => DynamicSlipRatio;
[Sync] public float Torque { get; set; }
[Sync, Range( 0, 1 )] public float Brake { get; set; }
[Property] float BrakePowerMax { get; set; } = 3000;
[Property] public bool IsFront { get; protected set; }
[Property] public float SteerMultiplier { get; set; }
[Property] public float CasterAngle { get; set; } = 7; // todo
[Property] public float CamberAngle { get; set; } = -3;
[Property] public float ToeAngle { get; set; } = 0.5f;
[Property] public float Ackermann { get; set; } = 0;
[Property, Group( "Suspension" )] float SuspensionLength { get; set; } = 10;
[Property, Group( "Suspension" )] float SpringStrength { get; set; } = 800;
[Property, Group( "Suspension" )] float SpringDamper { get; set; } = 3000;
[Property] public bool AutoPhysics { get; set; } = true;
public float Spin { get; private set; }
public float RPM { get => angularVelocity * 30f / MathF.PI; set => angularVelocity = value / (30f / MathF.PI); }
public float AngularVelocity { get => angularVelocity; set => angularVelocity = value; }
internal float DistributionFactor { get; set; }
public float RPM { get => AngularVelocity * 60f / MathF.Tau; set => AngularVelocity = value / (60 / MathF.Tau); }
[Sync] internal float DistributionFactor { get; set; }
private Vector3 StartPos { get; set; }
private static Rotation CylinderOffset => Rotation.FromRoll( 90 );
private static Rotation CylinderOffset = Rotation.FromRoll( 90 );
public SceneTraceResult Trace { get; private set; }
public bool IsOnGround => Trace.Hit;
[Sync] public bool IsOnGround { get; private set; }
private float lastSpringOffset;
private float angularVelocity;
private float load;
private float lastFraction;
private Vector3 contactPos;
private Vector3 forward;
private Vector3 right;
private Vector3 up;
[Property] public float DriveTorque => Torque;
[Property] public float BrakeTorque => Brake * 5000f;
private float forwardFriction;
private float sideFriction;
private Vector3 force;
public float CounterTorque { get; private set; }
public float Compression { get; protected set; } // meters
public float LastLength { get; protected set; } // meters
public float Fz { get; protected set; } // N
public float AngularVelocity { get; protected set; } // rad/s
public float Fx { get; protected set; } // N
public float Fy { get; protected set; } // N
public float RollAngle { get; protected set; } // degrees
internal float BaseInertia => 0.5f * Mass * MathF.Pow( Radius.InchToMeter(), 2 );
public float Inertia
{
get => BaseInertia + inertia;
set => inertia = value;
}
private VeloXBase Vehicle;
[Sync] public Vector3 ContactNormal { get; protected set; }
[Sync] public Vector3 ContactPosition { get; protected set; }
public float SlipRatio { get; protected set; }
public float SlipAngle { get; protected set; }
public float DynamicSlipRatio { get; protected set; }
public float DynamicSlipAngle { get; protected set; }
Rotation TransformRotationSteer => Vehicle.WorldTransform.RotationToWorld( Vehicle.SteerAngle * SteerMultiplier );
protected override void OnAwake()
{
Vehicle = Components.Get<VeloXBase>( FindMode.EverythingInSelfAndAncestors );
base.OnAwake();
if ( StartPos.IsNearZeroLength )
StartPos = LocalPosition;
Inertia = BaseInertia;
}
internal void Update( VeloXBase vehicle, in float dt )
@@ -93,224 +77,198 @@ public partial class VeloXWheel : Component
private void UpdateVisuals( VeloXBase vehicle, in float dt )
{
Spin -= angularVelocity.RadianToDegree() * dt;
WorldRotation = vehicle.WorldTransform.RotationToWorld( GetSteer( vehicle.SteerAngle.yaw ) ) * Rotation.FromAxis( Vector3.Right, Spin );
WorldRotation = vehicle.WorldTransform.RotationToWorld( vehicle.SteerAngle * SteerMultiplier ).RotateAroundAxis( Vector3.Right, -RollAngle );
LocalPosition = StartPos + Vector3.Down * LastLength.MeterToInch();
}
private Rotation GetSteer( float steer )
private struct WheelTraceData
{
float angle = (-steer * SteerMultiplier).DegreeToRadian();
float t = MathF.Tan( (MathF.PI / 2) - angle ) - Ackermann;
float steering_angle = MathF.CopySign( float.Pi / 2, t ) - MathF.Atan( t );
var steering_axis = Vector3.Up * MathF.Cos( -CasterAngle.DegreeToRadian() ) +
Vector3.Right * MathF.Sin( -CasterAngle.DegreeToRadian() );
return Rotation.FromAxis( Vector3.Forward, -CamberAngle ) * Rotation.FromAxis( steering_axis, steering_angle.RadianToDegree() );
internal Vector3 ContactNormal;
internal Vector3 ContactPosition;
internal float Compression;
internal float Force;
}
public void UpdateForce()
{
Vehicle.Body.ApplyForceAt( ContactPosition, FrictionForce + ContactNormal * Fz.MeterToInch() );
}
private static float GetLongitudinalLoadCoefficient( float load ) => 11000 * (1 - MathF.Exp( -0.00014f * load ));
private static float GetLateralLoadCoefficient( float load ) => 18000 * (1 - MathF.Exp( -0.0001f * load ));
private float inertia;
private (float, float, float, float) StepLongitudinal( float Tm, float Tb, float Vx, float W, float Lc, float R, float I )
internal void StepPhys( VeloXBase vehicle, in float dt )
{
float wInit = W;
float vxAbs = Math.Abs( Vx );
float Sx;
if ( Lc < 0.01f )
var _rigidbody = vehicle.Body;
const int numSamples = 3;
float halfWidth = Width.MeterToInch() * 0.5f;
var ang = vehicle.WorldTransform.RotationToWorld( vehicle.SteerAngle * SteerMultiplier );
Vector3 right = Vector3.VectorPlaneProject( ang.Right, Vector3.Up ).Normal;
int hitCount = 0;
WheelTraceData wheelTraceData = new();
for ( int i = 0; i < numSamples; i++ )
{
Sx = 0;
float t = (float)i / (numSamples - 1);
float offset = MathX.Lerp( -halfWidth, halfWidth, t );
Vector3 start = vehicle.WorldTransform.PointToWorld( StartPos + Vector3.Right * offset );
Vector3 end = start + vehicle.WorldRotation.Down * RestLength.MeterToInch();
if ( TraceWheel( vehicle, ref wheelTraceData, start, end, Width.MeterToInch() / numSamples, dt ) )
hitCount++;
}
else if ( vxAbs >= 0.01f )
if ( hitCount > 0 )
{
Sx = (W * R - Vx) / vxAbs;
IsOnGround = true;
//// Average all contacts
Fz = Math.Max( wheelTraceData.Force / hitCount, 0 );
Compression = wheelTraceData.Compression / hitCount;
ContactNormal = (wheelTraceData.ContactNormal / hitCount).Normal;
ContactPosition = wheelTraceData.ContactPosition / hitCount;
//DoSuspensionSounds( vehicle, (RestLength - Compression) * 0.8f);
LastLength = RestLength - Compression;
//DebugOverlay.Normal( ContactPosition, ContactNormal * Fz / 1000 );
// Apply suspension force
//_rigidbody.ApplyForceAt( ContactPosition, ContactNormal * Fz.MeterToInch() );
UpdateHitVariables();
// Friction
Vector3 forward = ContactNormal.Cross( right ).Normal;
right = Rotation.FromAxis( ContactNormal, 90f ) * forward;
var velAtContact = _rigidbody.GetVelocityAtPoint( ContactPosition + vehicle.Body.MassCenter ) * 0.0254f;
float vx = Vector3.Dot( velAtContact, forward );
float vy = Vector3.Dot( velAtContact, right );
float wheelLinearVel = AngularVelocity * Radius;
float creepVel = 0.5f;
SlipRatio = (wheelLinearVel - vx) / (MathF.Abs( vx ) + creepVel);
SlipAngle = MathX.RadianToDegree( MathF.Atan2( vy, MathF.Abs( vx ) + creepVel ) );
float latCoeff = 1.0f - MathF.Exp( -MathF.Max( MathF.Abs( vx ), 1f ) * dt / 0.01f );
float longCoeff = 1.0f - MathF.Exp( -MathF.Max( MathF.Abs( vx ), 1f ) * dt / 0.01f );
DynamicSlipAngle += (SlipAngle - DynamicSlipAngle) * latCoeff;
DynamicSlipRatio += (SlipRatio - DynamicSlipRatio) * longCoeff;
UpdateFriction( dt );
}
else
{
Sx = (W * R - Vx) * 0.6f;
IsOnGround = false;
// Wheel is off the ground
Compression = 0f;
Fz = 0f;
Fx = 0f;
Fy = 0f;
ContactNormal = Vector3.Up;
ContactPosition = WorldPosition;
}
Sx = Math.Clamp( Sx, -1, 1 );
W += Tm / I * Time.Delta;
Tb *= W > 0 ? -1 : 1;
float tbCap = Math.Abs( W ) * I / Time.Delta;
float tbr = Math.Abs( Tb ) - Math.Abs( tbCap );
tbr = Math.Max( tbr, 0 );
Tb = Math.Clamp( Tb, -tbCap, tbCap );
W += Tb / I * Time.Delta;
float maxTorque = TirePreset.Pacejka.PacejkaFx( Math.Abs( Sx ) ) * Lc * R;
float errorTorque = (W - Vx / R) * I / Time.Delta;
float surfaceTorque = Math.Clamp( errorTorque, -maxTorque, maxTorque );
W -= surfaceTorque / I * Time.Delta;
float Fx = surfaceTorque / R;
tbr *= (W > 0 ? -1 : 1);
float TbCap2 = Math.Abs( W ) * I / Time.Delta;
float Tb2 = Math.Clamp( tbr, -TbCap2, TbCap2 );
W += Tb2 / I * Time.Delta;
float deltaOmegaTorque = (W - wInit) * I / Time.Delta;
float Tcnt = -surfaceTorque + Tb + Tb2 - deltaOmegaTorque;
return (W, Sx, Fx, Tcnt);
}
private void StepLateral( float Vx, float Vy, float Lc, out float Sy, out float Fy )
const string playerTag = "player";
private bool TraceWheel( VeloXBase vehicle, ref WheelTraceData wheelTraceData, Vector3 start, Vector3 end, float width, in float dt )
{
float VxAbs = Math.Abs( Vx );
if ( Lc < 0.01f )
{
Sy = 0;
}
else if ( VxAbs > 0.1f )
var trace = Scene.Trace
.FromTo( start, end )
.Cylinder( width, Radius.MeterToInch() )
.Rotated( vehicle.WorldRotation * CylinderOffset )
.UseHitPosition( false )
.IgnoreGameObjectHierarchy( Vehicle.GameObject )
.WithoutTags( playerTag )
.Run();
//DebugOverlay.Trace( trace, overlay: true );
if ( trace.Hit )
{
Sy = MathX.RadianToDegree( MathF.Atan( Vy / VxAbs ) ) / 50;
}
else
{
Vector3 contactPos = trace.EndPosition;
Vector3 contactNormal = trace.Normal;
float currentLength = trace.Distance.InchToMeter();
float compression = (RestLength - currentLength).Clamp( -RestLength, RestLength );
Sy = Vy * (0.003f / Time.Delta);
// Nonlinear spring
float springForce = SpringStiffness * compression * (1f + 2f * compression / RestLength);
// Damping
float damperVelocity = (LastLength - currentLength) / dt;
float damperForce = damperVelocity > 0 ? damperVelocity * ReboundStiffness : damperVelocity * CompressionStiffness;
float FzPoint = springForce + damperForce;
// Bump stop
float minCompression = -0.05f;
if ( compression <= minCompression ) FzPoint += (minCompression - compression) * BumpStopStiffness;
wheelTraceData.ContactNormal += contactNormal;
wheelTraceData.ContactPosition += contactPos;
wheelTraceData.Compression += compression;
wheelTraceData.Force += Math.Max( 0, FzPoint );
return true;
}
Sy = Math.Clamp( Sy, -1, 1 );
float slipSign = Sy < 0 ? -1 : 1;
Fy = -slipSign * TirePreset.Pacejka.PacejkaFy( Math.Abs( Sy ) ) * Lc;
return false;
}
private void SlipCircle( float Sx, float Sy, float Fx, ref float Fy )
public void DoPhysics( in float dt )
{
float SxAbs = Math.Abs( Sx );
if ( SxAbs > 0.01f )
{
float SxClamped = Math.Clamp( Sx, -1, 1 );
float SyClamped = Math.Clamp( Sy, -1, 1 );
Vector2 combinedSlip = new( SxClamped * 1.05f, SyClamped );
Vector2 slipDir = combinedSlip.Normal;
float F = MathF.Sqrt( Fx * Fx + Fy * Fy );
float absSlipDirY = Math.Abs( slipDir.y );
Fy = F * absSlipDirY * (Fy < 0 ? -1 : 1);
}
}
public void DoPhysics( VeloXBase vehicle )
{
var pos = vehicle.WorldTransform.PointToWorld( StartPos );
var ang = vehicle.WorldTransform.RotationToWorld( GetSteer( vehicle.SteerAngle.yaw ) );
var maxLen = SuspensionLength;
var endPos = pos + ang.Down * maxLen;
Trace = Scene.Trace
.IgnoreGameObjectHierarchy( vehicle.GameObject )
.Cylinder( Width, Radius, pos, endPos )
.Rotated( vehicle.WorldTransform.Rotation * CylinderOffset )
.UseRenderMeshes( false )
.UseHitPosition( true )
.WithoutTags( vehicle.WheelIgnoredTags )
.Run();
forward = Vector3.VectorPlaneProject( ang.Forward, Trace.Normal );
right = Vector3.VectorPlaneProject( ang.Right, Trace.Normal );
var fraction = Trace.Fraction;
contactPos = pos - maxLen * fraction * ang.Up;
LocalPosition = vehicle.WorldTransform.PointToLocal( contactPos );
DoSuspensionSounds( vehicle, fraction - lastFraction );
lastFraction = fraction;
if ( !IsOnGround )
if ( IsProxy )
return;
var vel = vehicle.Body.GetVelocityAtPoint( contactPos );
var offset = maxLen - (fraction * maxLen);
var springForce = offset * SpringStrength;
var damperForce = (lastSpringOffset - offset) * SpringDamper;
lastSpringOffset = offset;
var velU = Trace.Normal.Dot( vel );
if ( velU < 0 && offset + Math.Abs( velU * Time.Delta ) > SuspensionLength )
{
vehicle.Body.CalculateVelocityOffset( -velU / Time.Delta * Trace.Normal, pos, out var linearImp, out var angularImp );
vehicle.Body.Velocity += linearImp;
vehicle.Body.AngularVelocity += angularImp;
vehicle.Body.CalculateVelocityOffset( Trace.HitPosition - (contactPos + Trace.Normal * velU * Time.Delta), pos, out var lin, out _ );
vehicle.WorldPosition += lin / Time.Delta;
damperForce = 0;
}
force = (springForce - damperForce) * Trace.Normal;
load = Math.Max( springForce - damperForce, 0 );
float R = Radius.InchToMeter();
float forwardSpeed = vel.Dot( forward ).InchToMeter();
float sideSpeed = vel.Dot( right ).InchToMeter();
float longitudinalLoadCoefficient = GetLongitudinalLoadCoefficient( load );
float lateralLoadCoefficient = GetLateralLoadCoefficient( load );
float F_roll = TirePreset.GetRollingResistance( angularVelocity * R, 1.0f ) * 10000;
( float W, float Sx, float Fx, float counterTq) = StepLongitudinal(
Torque,
Brake * BrakePowerMax + F_roll,
forwardSpeed,
angularVelocity,
longitudinalLoadCoefficient,
R,
Inertia
);
StepLateral( forwardSpeed, sideSpeed, lateralLoadCoefficient, out float Sy, out float Fy );
SlipCircle( Sx, Sy, Fx, ref Fy );
CounterTorque = counterTq;
angularVelocity = W;
force += forward * Fx;
force += right * Fy * Math.Clamp( vehicle.TotalSpeed * 0.005f, 0, 1 );
ForwardSlip = Sx;
SideSlip = Sy * Math.Clamp( vehicle.TotalSpeed * 0.005f, 0, 1 );
vehicle.Body.ApplyForceAt( pos, force / Time.Delta );
StepRotation( Vehicle, dt );
if ( AutoSimulate )
StepPhys( Vehicle, dt );
}
#if DEBUG
protected override void OnUpdate()
private void StepRotation( VeloXBase vehicle, in float dt )
{
DebugOverlay.Normal( contactPos, forward * forwardFriction, Color.Red, overlay: true );
DebugOverlay.Normal( contactPos, right * sideFriction, Color.Green, overlay: true );
DebugOverlay.Normal( contactPos, up * force / 1000f, Color.Blue, overlay: true );
}
#endif
float inertia = MathF.Max( 1f, Inertia );
float roadTorque = Fx * Radius;
float externalTorque = DriveTorque - roadTorque;
float rollingResistanceTorque = Fz * Radius * SurfaceResistance;
const float HubCoulombNm = 20f;
const float HubViscous = 0.1f;
float coulombTorque = BrakeTorque + rollingResistanceTorque + HubCoulombNm;
float omega = AngularVelocity;
if ( MathF.Abs( omega ) < 1e-6f && MathF.Abs( externalTorque ) <= coulombTorque )
{
AngularVelocity = 0f;
}
else
{
// viscous decay
if ( HubViscous > 0f ) omega *= MathF.Exp( -(HubViscous / inertia) * dt );
// Coulomb drag
if ( coulombTorque > 0f && omega != 0f )
{
float dir = MathF.Sign( omega );
float deltaOmega = (coulombTorque / inertia) * dt;
if ( deltaOmega >= MathF.Abs( omega ) ) omega = 0f;
else omega -= dir * deltaOmega;
}
if ( MathF.Abs( omega ) < 0.01f ) omega = 0f;
}
AngularVelocity = omega; // wider sanity range
RollAngle += MathX.RadianToDegree( AngularVelocity ) * dt;
RollAngle = (RollAngle % 360f + 360f) % 360f;
}
}

View File

@@ -0,0 +1,58 @@
using Sandbox;
using System;
using System.Diagnostics;
using System.Linq;
namespace VeloX;
internal sealed class WheelManager : GameObjectSystem
{
public WheelManager( Scene scene ) : base( scene )
{
Listen( Stage.StartFixedUpdate, -99, UpdateWheels, "UpdateWheels" );
Listen( Stage.StartFixedUpdate, -100, UpdateEngine, "UpdateEngine" );
}
private void UpdateWheels()
{
if ( !Game.IsPlaying )
return;
//Stopwatch sw = Stopwatch.StartNew();
var wheels = Scene.GetAll<VeloXWheel>();
if ( !wheels.Any() ) return;
var timeDelta = Time.Delta;
Sandbox.Utility.Parallel.ForEach( wheels, item =>
{
if ( !item.IsProxy )
item.DoPhysics( timeDelta );
} );
foreach ( var wheel in wheels )
wheel.UpdateForce();
//sw.Stop();
//DebugOverlaySystem.Current.ScreenText( new Vector2( 120, 30 ), $"Wheel Sim: {sw.Elapsed.TotalMilliseconds,6:F2} ms", 24 );
}
private void UpdateEngine()
{
if ( !Game.IsPlaying )
return;
//Stopwatch sw = Stopwatch.StartNew();
var engines = Scene.GetAll<Engine>();
if ( !engines.Any() ) return;
var timeDelta = Time.Delta;
Sandbox.Utility.Parallel.ForEach( engines, item =>
{
if ( !item.IsProxy )
item.UpdateEngine( timeDelta );
} );
//sw.Stop();
//DebugOverlaySystem.Current.ScreenText( new Vector2( 120, 54 ), $"Engine Sim: {sw.Elapsed.TotalMilliseconds,6:F2} ms", 24 );
}
}