Compare commits

...

13 Commits

Author SHA1 Message Date
e22e463f11 dedicated 2025-12-03 15:17:36 +07:00
66fcc6a2bb cumulative update 2025-12-01 00:02:14 +07:00
e12b75be45 update 2025-11-25 19:16:40 +07:00
68626640c2 new maps and other improvements 2025-11-23 22:51:41 +07:00
b02f14ee47 maps 2025-11-21 22:11:34 +07:00
b978b821fa peak long drag 2025-11-21 20:21:36 +07:00
562750107a fix aboba 2025-11-21 17:52:48 +07:00
6cb8f716b3 new cars and maps 2025-11-21 17:52:25 +07:00
558a1eda07 rebalance 2025-11-20 20:01:57 +07:00
a58888c314 rewrite ui 2025-11-20 00:17:51 +07:00
9d92c4ca93 add races 2025-11-19 17:38:09 +07:00
bbc479be33 update 2025-11-18 21:53:51 +07:00
97fcb29bc0 decals 2025-11-12 21:57:11 +07:00
28 changed files with 542 additions and 309 deletions

View File

@@ -1,8 +1,8 @@
{
"B": 8.36,
"B": 9,
"C": 2.15,
"D": 0.833,
"E": 0.992,
"D": 0.933,
"E": 0.971,
"__references": [],
"__version": 0
}

View File

@@ -12,6 +12,7 @@
"NetworkMode": 2,
"NetworkInterpolation": true,
"NetworkOrphaned": 0,
"NetworkTransmit": true,
"OwnerTransfer": 1,
"Components": [],
"Children": [
@@ -28,6 +29,7 @@
"NetworkMode": 2,
"NetworkInterpolation": true,
"NetworkOrphaned": 0,
"NetworkTransmit": true,
"OwnerTransfer": 1,
"Components": [
{
@@ -197,7 +199,7 @@
"FaceVelocity": false,
"FogStrength": 1,
"LeadingTrail": true,
"Lighting": false,
"Lighting": true,
"MotionBlur": false,
"OnComponentDestroy": null,
"OnComponentDisabled": null,
@@ -206,6 +208,7 @@
"OnComponentStart": null,
"OnComponentUpdate": null,
"Opaque": false,
"PlaybackSpeed": 1,
"RenderOptions": {
"GameLayer": true,
"OverlayLayer": false,
@@ -215,7 +218,7 @@
"RotationOffset": 0,
"Scale": 1,
"Shadows": true,
"SortMode": "Unsorted",
"SortMode": "ByDistance",
"Sprite": {
"$compiler": "embed",
"$source": null,
@@ -228,7 +231,8 @@
"LoopMode": "Loop",
"Frames": [
{
"Texture": "textures/smoketexturesheet.vtex"
"Texture": "textures/smoketexturesheet.vtex",
"BroadcastMessages": []
}
]
}

View File

@@ -0,0 +1,8 @@
{
"loop": false,
"start": 0,
"end": 0,
"rate": 44100,
"compress": false,
"bitrate": 256
}

View File

@@ -117,7 +117,7 @@ public partial class Clutch : PowertrainComponent
ClutchInput = 1f;
}
if ( Controller.SwappedBrakes > 0 )
if ( Controller.SwappedBrakes > 0 && Controller.SwappedThrottle == 0 )
{
ClutchInput = 0;
}
@@ -173,7 +173,7 @@ public partial class Clutch : PowertrainComponent
OutputInertia = (inertiaSum + halfClutchInertia) * _clutchEngagement + halfClutchInertia;
// Allow the torque output to be only up to the slip torque valu
float outputTorqueClamp = SlipTorque * _clutchEngagement;
float outputTorqueClamp = Controller.Engine.EstimatedPeakTorque * 1.5f * _clutchEngagement;
OutputTorque = InputTorque;
OutputTorque = Math.Clamp( OutputTorque, 0, outputTorqueClamp );

View File

@@ -78,7 +78,7 @@ public class Engine : PowertrainComponent, IScenePhysicsEvents
/// </summary>
[Property] public bool FlyingStartEnabled { get; set; }
[Property] public bool Ignition { get; set; }
[Property] public bool Ignition { get; private set; }
/// <summary>
/// Power curve with RPM range [0,1] on the X axis and power coefficient [0,1] on Y axis.
@@ -225,13 +225,21 @@ public class Engine : PowertrainComponent, IScenePhysicsEvents
if ( FlyingStartEnabled )
{
FlyingStart();
IsRunning = true;
IsActive = true;
}
else if ( !StarterActive && Controller != null )
{
StarterCoroutine();
}
}
else
{
IsRunning = true;
IsActive = true;
}
}
private async void StarterCoroutine()
{
if ( Type == EngineType.Electric || StarterActive )
@@ -251,6 +259,11 @@ public class Engine : PowertrainComponent, IScenePhysicsEvents
{
startTimer += 0.1f;
await Task.DelaySeconds( 0.1f );
if ( OutputAngularVelocity >= _idleAngularVelocity * 0.8f )
{
break;
}
}
}
finally
@@ -258,6 +271,7 @@ public class Engine : PowertrainComponent, IScenePhysicsEvents
_starterTorque = 0;
StarterActive = false;
IsActive = true;
IsRunning = true;
}
}
@@ -267,12 +281,15 @@ public class Engine : PowertrainComponent, IScenePhysicsEvents
Ignition = true;
StarterActive = false;
OutputAngularVelocity = IdleRPM.RPMToAngularVelocity();
IsRunning = true;
IsActive = true;
}
public void StopEngine()
{
Ignition = false;
IsActive = true;
IsRunning = false;
IsActive = false;
OnEngineStop?.Invoke();
}
@@ -308,11 +325,6 @@ public class Engine : PowertrainComponent, IScenePhysicsEvents
_revLimiterAngularVelocity = RevLimiterRPM.RPMToAngularVelocity();
if ( _revLimiterAngularVelocity == 0f )
return;
// Check for start on throttle
if ( !IsRunning && !StarterActive && AutoStartOnThrottle && ThrottlePosition > 0.2f )
StartEngine();
bool wasRunning = IsRunning;
IsRunning = Ignition;
@@ -341,6 +353,7 @@ public class Engine : PowertrainComponent, IScenePhysicsEvents
// Calculate/get torque returned from wheels
OutputTorque = generatedTorque - reactionTorque;
float returnTorque = ForwardStep( OutputTorque, 0, dt );
float totalTorque = generatedTorque + returnTorque + reactionTorque;
@@ -408,7 +421,7 @@ public class Engine : PowertrainComponent, IScenePhysicsEvents
// Apply idle throttle correction to keep the engine running
else
ApplyICEIdleCorrection();
// Trigger rev limiter if needed
if ( angularVelocity >= _revLimiterAngularVelocity && !RevLimiterActive )
RevLimiter();
@@ -454,7 +467,7 @@ public class Engine : PowertrainComponent, IScenePhysicsEvents
// if the idle RPM is below the target RPM.
float idleCorrection = _idleAngularVelocity * 1.08f - OutputAngularVelocity;
idleCorrection = idleCorrection < 0f ? 0f : idleCorrection;
float idleThrottlePosition = Math.Clamp( idleCorrection * 0.01f, 0, 1 );
float idleThrottlePosition = Math.Clamp( idleCorrection * 0.01f, 0, 1f );
ThrottlePosition = Math.Max( _userThrottleInput, idleThrottlePosition );
}
}

View File

@@ -15,7 +15,7 @@ public class Transmission : PowertrainComponent
/// <summary>
/// A class representing a single ground surface type.
/// </summary>
public partial class TransmissionGearingProfile
public class TransmissionGearingProfile
{
/// <summary>
/// List of forward gear ratios starting from 1st forward gear.
@@ -246,7 +246,6 @@ public class Transmission : PowertrainComponent
[Property] public bool AllowDownshiftGearSkipping { get; set; } = true;
private bool _repeatInputFlag;
private float _smoothedThrottleInput;
/// <summary>
/// Timer needed to prevent manual transmission from slipping out of gear too soon when hold in gear is enabled,
@@ -543,13 +542,14 @@ public class Transmission : PowertrainComponent
// Run the first half of shift timer
float shiftTimer = 0;
float halfDuration = ShiftDuration * 0.5f;
if ( !instant )
while ( shiftTimer < halfDuration )
{
ShiftProgress = shiftTimer / ShiftDuration;
shiftTimer += dt;
await Task.DelayRealtimeSeconds( dt );
}
//if ( !instant )
// while ( shiftTimer < halfDuration )
// {
// ShiftProgress = shiftTimer / ShiftDuration;
// shiftTimer += dt;
// await Task.DelayRealtimeSeconds( dt );
// }
// Do the shift at the half point of shift duration
Gear = targetGear;
@@ -603,28 +603,28 @@ public class Transmission : PowertrainComponent
float brakeInput = car.SwappedBrakes;
int currentGear = Gear;
// Assign base shift points
_targetDownshiftRPM = _downshiftRPM;
_targetUpshiftRPM = _upshiftRPM;
_targetDownshiftRPM = car.Engine.EstimatedPeakPowerRPM - 2000;
_targetUpshiftRPM = car.Engine.EstimatedPeakPowerRPM;
// Calculate shift points for variable shift RPM
if ( VariableShiftPoint )
{
// Smooth throttle input so that the variable shift point does not shift suddenly and cause gear hunting
_smoothedThrottleInput = MathX.Lerp( _smoothedThrottleInput, throttleInput, Time.Delta * 2f );
float revLimiterRPM = car.Engine.RevLimiterRPM;
//if ( VariableShiftPoint )
//{
// // Smooth throttle input so that the variable shift point does not shift suddenly and cause gear hunting
// _smoothedThrottleInput = MathX.Lerp( _smoothedThrottleInput, throttleInput, Time.Delta * 2f );
// float revLimiterRPM = car.Engine.RevLimiterRPM;
_targetUpshiftRPM = _upshiftRPM + Math.Clamp( _smoothedThrottleInput * VariableShiftIntensity, 0f, 1f ) * _upshiftRPM;
_targetUpshiftRPM = Math.Clamp( _targetUpshiftRPM, _upshiftRPM, revLimiterRPM * 0.97f );
// _targetUpshiftRPM = _upshiftRPM + Math.Clamp( _smoothedThrottleInput * VariableShiftIntensity, 0f, 1f ) * _upshiftRPM;
// _targetUpshiftRPM = Math.Clamp( _targetUpshiftRPM, _upshiftRPM, revLimiterRPM * 0.97f );
_targetDownshiftRPM = _downshiftRPM + Math.Clamp( _smoothedThrottleInput * VariableShiftIntensity, 0f, 1f ) * _downshiftRPM;
_targetDownshiftRPM = Math.Clamp( _targetDownshiftRPM, car.Engine.IdleRPM * 1.1f, _targetUpshiftRPM * 0.7f );
// _targetDownshiftRPM = _downshiftRPM + Math.Clamp( _smoothedThrottleInput * VariableShiftIntensity, 0f, 1f ) * _downshiftRPM;
// _targetDownshiftRPM = Math.Clamp( _targetDownshiftRPM, car.Engine.IdleRPM * 1.1f, _targetUpshiftRPM * 0.7f );
// Add incline modifier
float inclineModifier = Math.Clamp( car.WorldRotation.Forward.Dot( Vector3.Up ) * InclineEffectCoeff, 0f, 1f );
// // Add incline modifier
// float inclineModifier = Math.Clamp( car.WorldRotation.Forward.Dot( Vector3.Up ) * InclineEffectCoeff, 0f, 1f );
_targetUpshiftRPM += revLimiterRPM * inclineModifier;
_targetDownshiftRPM += revLimiterRPM * inclineModifier;
}
// _targetUpshiftRPM += revLimiterRPM * inclineModifier;
// _targetDownshiftRPM += revLimiterRPM * inclineModifier;
//}
// In neutral

View File

@@ -51,7 +51,7 @@ public partial class WheelPowertrain : PowertrainComponent
Wheel.AutoSimulate = false;
Wheel.StepPhys( Controller, dt );
return Math.Abs( Wheel.CounterTorque );
}

View File

@@ -30,7 +30,6 @@ public abstract partial class VeloXBase
Engine.Controller = this;
Engine.Inertia = 0.25f;
Engine.Ignition = false;
if ( !Clutch.IsValid() )
Clutch = new GameObject( Engine.GameObject, true, "Clutch" ).GetOrAddComponent<Clutch>();

View File

@@ -4,10 +4,12 @@ namespace VeloX;
public abstract partial class VeloXBase
{
[Feature( "Input" )] internal InputResolver Input { get; set; } = new();
[Feature( "Input" )] public Connection Driver { get => Input.Driver; set => Input.Driver = value; }
internal readonly InputResolver Input = new();
public Guid DriverId { get; set; }
public Connection Driver => Connection.Find( DriverId );
public bool IsDriver => Connection.Local == Driver;
private bool IsDriverActive => Driver is not null;
public Vector2 MouseDelta => IsDriverActive ? Input.MouseDelta : default;
@@ -34,6 +36,8 @@ public abstract partial class VeloXBase
public float SwappedBrakes => IsInputSwapped ? Throttle : Brakes;
public bool AnyInput => Throttle > 0 || Brakes > 0;
[Sync]
public float VerticalInput
{
@@ -59,7 +63,7 @@ public abstract partial class VeloXBase
/// Throttle axis.
/// For combined throttle/brake input use 'VerticalInput' instead.
/// </summary>
[Sync( SyncFlags.Interpolate ), Range( 0, 1 ), Property]
[Sync( SyncFlags.Interpolate ), Range( 0, 1 ), Property, ReadOnly]
public float Throttle
{
get => throttle;
@@ -70,19 +74,20 @@ public abstract partial class VeloXBase
/// Brake axis.
/// For combined throttle/brake input use 'VerticalInput' instead.
/// </summary>
[Sync]
[Range( 0, 1 ), Property, ReadOnly]
public float Brakes
{
get => brakes;
set => brakes = Math.Clamp( value, 0, 1 );
}
[Sync]
[Sync( SyncFlags.Interpolate ), Range( 0, 1 ), Property, ReadOnly]
public float SteeringAngle
{
get => steerAngle;
set => steerAngle = Math.Clamp( value, -1, 1 );
}
[Sync]
public float Handbrake
{
@@ -103,17 +108,28 @@ public abstract partial class VeloXBase
IsShiftingUp = false;
IsShiftingDown = false;
}
private void UpdateInput()
protected void UpdateInput()
{
VerticalInput = TotalSpeed < 10 ? Input.AnalogMove.x * 0.5f : Input.AnalogMove.x;
Handbrake = Input.Down( "Jump" ) ? 1 : 0;
//VerticalInput = Input.AnalogMove.x;
if ( IsDriverActive )
{
SteeringAngle = Input.AnalogMove.y;
Brakes = Input.Brake;
Throttle = Input.Throttle;
IsClutching = (Input.Down( "Run" ) || Input.Down( "Jump" )) ? 1 : 0;
Handbrake = Input.Down( "Handbrake" ) ? 1 : 0;
IsShiftingUp = Input.Pressed( "Attack1" );
IsShiftingDown = Input.Pressed( "Attack2" );
SteeringAngle = Input.AnalogMove.y;
IsClutching = Input.Down( "Clutch" ) ? 1 : 0;
IsShiftingUp = Input.Pressed( "Shift Up" );
IsShiftingDown = Input.Pressed( "Shift Down" );
}
else
{
ResetInput();
}
if ( TotalSpeed < 150 && Driver is null )
Handbrake = 1;
@@ -143,6 +159,13 @@ public abstract partial class VeloXBase
Input.TriggerHaptics( leftMotor, rightMotor, leftTrigger, rightTrigger, duration );
}
}
public void TriggerHaptics( HapticEffect effect, float lengthScale = 1, float frequencyScale = 1, float amplitudeScale = 1 )
{
if ( IsDriverActive )
{
Input.TriggerHaptics( effect, lengthScale, frequencyScale, amplitudeScale );
}
}
public void StopAllHaptics()
{

View File

@@ -5,26 +5,23 @@ namespace VeloX;
public abstract partial class VeloXBase
{
private Vector3 linForce;
private Vector3 angForce;
[Property] float BrakeForce { get; set; } = 1500f;
[Property] float HandbrakeForce { get; set; } = 3500f;
private void PhysicsSimulate()
protected const float BrakeForce = 4500f;
protected const float HandbrakeForce = 35000f;
protected void PhysicsSimulate()
{
if ( Body.Sleeping && Input.AnalogMove.x == 0 )
return;
//Body.PhysicsBody.SetInertiaTensor( new Vector3( 800000, 3000000, 6000000 ), Rotation.Identity );
var drag = AngularDrag;
var mass = Body.Mass;
var angVel = Body.AngularVelocity;
linForce.x = 0;
linForce.y = 0;
linForce.z = 0;
angForce = angForce.WithX( angVel.x * drag.x * mass * 1000 );
angForce = angForce.WithY( angVel.y * drag.y * mass * 1000 );
angForce = angForce.WithZ( angVel.z * drag.z * mass * 1000 );
if ( Wheels.Count > 0 )
{
Vector3 vehVel = Body.Velocity;
@@ -36,22 +33,30 @@ public abstract partial class VeloXBase
CombinedLoad += v.Fz;
foreach ( var v in Wheels )
{
v.BrakeTorque = SwappedBrakes * BrakeForce;
if ( !v.IsFront )
if ( v.IsFront )
{
v.BrakeTorque = SwappedBrakes * BrakeForce * 1.3f;
}
else
{
v.BrakeTorque = SwappedBrakes * BrakeForce * 0.7f;
v.BrakeTorque += Handbrake * HandbrakeForce;
v.Update( this, in dt );
v.DoPhysics( in dt );
}
if ( TotalSpeed < 1 && !AnyInput )
{
v.BrakeTorque = HandbrakeForce;
}
}
Body.Velocity = vehVel;
Body.AngularVelocity = vehAngVel;
}
Body.ApplyForce( linForce );
Body.ApplyTorque( angForce );
//Body.ApplyForce( linForce );
//Body.ApplyTorque( angForce );
}

View File

@@ -52,11 +52,13 @@ public abstract partial class VeloXBase : Component, Component.ICollisionListene
{
var hardSound = Sound.Play( HardCollisionSound, WorldPosition );
hardSound.Volume = volume;
TriggerHaptics( HapticEffect.HardImpact );
}
else if ( surfaceNormal.Dot( -collision.Contact.Speed.Normal ) < 0.5f )
{
var scrapSound = Sound.Play( VehicleScrapeSound, WorldPosition );
scrapSound.Volume = 0.4f;
TriggerHaptics( HapticEffect.SoftImpact );
}
}
}

View File

@@ -1,13 +1,23 @@
using Sandbox;
using System;
namespace VeloX;
public abstract partial class VeloXBase : Component
public abstract partial class VeloXBase : Component, IGameObjectNetworkEvents
{
[Sync] public WaterState WaterState { get; set; }
[Sync] public bool IsEngineOnFire { get; set; }
[Property, Sync] public EngineState EngineState { get; set; }
[Sync, Change( nameof( OnEngineIgnitionChange ) )]
public bool EngineIgnition { get; set; }
private void OnEngineIgnitionChange( bool oldvalue, bool newvalue )
{
if ( newvalue )
Engine?.StartEngine();
else
Engine?.StopEngine();
}
[Property] public Vector3 AngularDrag { get; set; } = new( -0.1f, -0.1f, -3 );
[Property] public float Mass { get; set; } = 900;
@@ -16,23 +26,34 @@ public abstract partial class VeloXBase : Component
[Sync( SyncFlags.Interpolate )] public Angles SteerAngle { get; set; }
public Vector3 LocalVelocity;
[Sync( SyncFlags.Interpolate )] public Vector3 LocalVelocity { get; set; }
[Sync( SyncFlags.Interpolate )] public Vector3 Velocity { get; set; }
public float ForwardSpeed;
public float TotalSpeed;
protected override void OnFixedUpdate()
{
if ( !IsProxy )
{
LocalVelocity = WorldTransform.PointToLocal( WorldPosition + Body.Velocity );
Velocity = Body.Velocity;
}
ForwardSpeed = LocalVelocity.x;
TotalSpeed = LocalVelocity.Length;
if ( IsProxy )
return;
LocalVelocity = WorldTransform.PointToLocal( WorldPosition + Body.Velocity );
ForwardSpeed = LocalVelocity.x;
TotalSpeed = LocalVelocity.Length;
Body.PhysicsBody.Mass = Mass;
FixedUpdate();
}
protected virtual void FixedUpdate()
{
UpdateInput();
PhysicsSimulate();
}
}

View File

@@ -1,11 +1,16 @@
namespace VeloX;
using System;
namespace VeloX;
public struct Friction
{
public float SlipCoef { get; set; }
public float ForceCoef { get; set; }
public float Force { get; set; }
public float Slip { get; set; }
public float Speed { get; set; }
public override readonly string ToString()
{
return $"Force:{Math.Round(Force, 2)}\nSlip:{Math.Round(Slip, 2)}\nSpeed:{Math.Round(Speed, 2)}";
}
}

View File

@@ -34,9 +34,7 @@ public partial class VeloXWheel
/// </summary>
public float CounterTorque { get; private set; }
[Property, Range( 0, 2 )] public float BrakeMult { get; set; } = 1f;
public Friction ForwardFriction = new();
public Friction SidewayFriction = new();
//[Property, Range( 0, 2 )] public float BrakeMult { get; set; } = 1f;
public Vector3 FrictionForce;
@@ -47,31 +45,32 @@ public partial class VeloXWheel
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 float LongitudinalSlip => sx;
public float LongitudinalSpeed => vx;
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 float LateralSlip => sy;
public float LateralSpeed => vy;
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;
// speed
[Sync] private float vx { get; set; }
[Sync] private float vy { get; set; }
// force
[Sync] private float fx { get; set; }
[Sync] private float fy { get; set; }
// slip
[Sync] private float sx { get; set; }
[Sync] private float sy { get; set; }
private void UpdateHitVariables()
{
if ( IsOnGround )
@@ -81,13 +80,13 @@ public partial class VeloXWheel
hitForwardDirection = ContactNormal.Cross( TransformRotationSteer.Right ).Normal;
hitSidewaysDirection = Rotation.FromAxis( ContactNormal, 90f ) * hitForwardDirection;
ForwardFriction.Speed = hitContactVelocity.Dot( hitForwardDirection ).InchToMeter();
SidewayFriction.Speed = hitContactVelocity.Dot( hitSidewaysDirection ).InchToMeter();
vx = hitContactVelocity.Dot( hitForwardDirection ).InchToMeter();
vy = hitContactVelocity.Dot( hitSidewaysDirection ).InchToMeter();
}
else
{
ForwardFriction.Speed = 0f;
SidewayFriction.Speed = 0f;
vx = 0;
vy = 0;
}
}
@@ -97,10 +96,11 @@ public partial class VeloXWheel
private Vector3 currentPosition;
private Vector3 referenceError;
private Vector3 correctiveForce;
public bool wheelIsBlocked;
private void UpdateFriction( float dt )
{
var motorTorque = DriveTorque;
var brakeTorque = BrakeTorque * BrakeMult;
var brakeTorque = BrakeTorque;
float allWheelLoadSum = Vehicle.CombinedLoad;
@@ -120,26 +120,21 @@ public partial class VeloXWheel
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 absForwardSpeed = Math.Abs( vx );
float forwardForceClamp = mass * LoadContribution * absForwardSpeed * invDt;
float absSideSpeed = Math.Abs( SidewayFriction.Speed );
float absSideSpeed = Math.Abs( vy );
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 peakForwardFrictionForce = 11000 * (1 - MathF.Exp( -0.00014f * forwardLoadFactor ));
float absCombinedBrakeTorque = Math.Max( 0, brakeTorque + RollingResistanceTorque );
float signedCombinedBrakeTorque = absCombinedBrakeTorque * -Math.Sign( ForwardFriction.Speed );
float signedCombinedBrakeTorque = absCombinedBrakeTorque * (vx > 0 ? -1 : 1);
float signedCombinedBrakeForce = signedCombinedBrakeTorque * invRadius;
float motorForce = motorTorque * invRadius;
float forwardInputForce = motorForce + signedCombinedBrakeForce;
@@ -149,10 +144,10 @@ public partial class VeloXWheel
float maxForwardForce = Math.Min( peakForwardFrictionForce, forwardForceClamp );
maxForwardForce = absMotorTorque < absBrakeTorque ? maxForwardForce : peakForwardFrictionForce;
ForwardFriction.Force = forwardInputForce > maxForwardForce ? maxForwardForce
fx = forwardInputForce > maxForwardForce ? maxForwardForce
: forwardInputForce < -maxForwardForce ? -maxForwardForce : forwardInputForce;
bool wheelIsBlocked = false;
wheelIsBlocked = false;
if ( IsOnGround )
{
float combinedWheelForce = motorForce + absCombinedBrakeTorque * invRadius * -Math.Sign( AngularVelocity );
@@ -168,14 +163,14 @@ public partial class VeloXWheel
AngularVelocity += combinedWheelForce * mRadius * invInertia * dt;
// Surface (corrective) force
float noSlipAngularVelocity = ForwardFriction.Speed * invRadius;
float noSlipAngularVelocity = vx * 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;
AngularVelocity += vx > 0 ? 1e-10f : -1e-10f;
}
else
{
@@ -193,22 +188,24 @@ public partial class VeloXWheel
float absAngularVelocity = AngularVelocity < 0 ? -AngularVelocity : AngularVelocity;
float maxCounterTorque = inertia * absAngularVelocity;
CounterTorque = Math.Clamp( (signedCombinedBrakeForce - ForwardFriction.Force) * mRadius, -maxCounterTorque, maxCounterTorque );
CounterTorque = Math.Clamp( (signedCombinedBrakeForce - fx) * mRadius, -maxCounterTorque, maxCounterTorque );
sx = (vx - AngularVelocity * mRadius) / clampedAbsForwardSpeed;
sx *= slipLoadModifier;
ForwardFriction.Slip = (ForwardFriction.Speed - AngularVelocity * mRadius) / clampedAbsForwardSpeed;
ForwardFriction.Slip *= slipLoadModifier;
sy = MathF.Atan2( vy, clampedAbsForwardSpeed );
sy *= slipLoadModifier;
SidewayFriction.Slip = MathF.Atan2( SidewayFriction.Speed, clampedAbsForwardSpeed );
SidewayFriction.Slip *= slipLoadModifier;
float sideSlipSign = sy > 0 ? 1 : -1;
float absSideSlip = Math.Abs( sy );
float peakSideFrictionForce = 18000 * (1 - MathF.Exp( -0.0001f * sideLoadFactor ));
float sideSlipSign = SidewayFriction.Slip > 0 ? 1 : -1;
float absSideSlip = Math.Abs( SidewayFriction.Slip );
float peakSideFrictionForce = sideLoadFactor;
float sideForce = -sideSlipSign * Tire.Evaluate( absSideSlip ) * peakSideFrictionForce;
fy = Math.Clamp( sideForce, -sideForceClamp, sideForceClamp );
float sideForce = -sideSlipSign * Tire.Evaluate( absSideSlip ) * sideLoadFactor;
SidewayFriction.Force = Math.Clamp( sideForce, -sideForceClamp, sideForceClamp );
SidewayFriction.Force *= camberFrictionCoeff;
// Calculate effect of camber on friction
float camberFrictionCoeff = Math.Max( 0, Vehicle.WorldRotation.Up.Dot( ContactNormal ) );
fy *= camberFrictionCoeff;
if ( IsOnGround && absForwardSpeed < 0.12f && absSideSpeed < 0.12f )
{
@@ -238,56 +235,36 @@ public partial class VeloXWheel
if ( wheelIsBlocked && absAngularVelocity < 0.5f )
{
ForwardFriction.Force += correctiveForce.Dot( hitForwardDirection );
fx += correctiveForce.Dot( hitForwardDirection );
}
SidewayFriction.Force += correctiveForce.Dot( hitSidewaysDirection );
fy += correctiveForce.Dot( hitSidewaysDirection );
}
}
else
{
lowSpeedReferenceIsSet = false;
}
ForwardFriction.Force = Math.Clamp( ForwardFriction.Force, -peakForwardFrictionForce, peakForwardFrictionForce );
SidewayFriction.Force = Math.Clamp( SidewayFriction.Force, -peakSideFrictionForce, peakSideFrictionForce );
fx = Math.Clamp( fx, -peakForwardFrictionForce, peakForwardFrictionForce );
fy = Math.Clamp( fy, -peakSideFrictionForce, peakSideFrictionForce );
if ( absForwardSpeed > 2f || absAngularVelocity > 4f )
if ( absForwardSpeed > 0.01f || absAngularVelocity > 0.01f )
{
var f = MathF.Sqrt( fx * fx + fy * fy );
var d = Math.Abs( new Vector2( sx, sy ).Normal.y );
fy = f * d * Math.Sign( fy );
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, Vector3.Up * AngularVelocity, overlay: true );
//DebugOverlay.Sphere( new( ContactPosition, 4 ), overlay: true );
//Vehicle.Body.ApplyForceAt( ContactPosition, FrictionForce );
FrictionForce.x = (hitSidewaysDirection.x * fy + hitForwardDirection.x * fx).MeterToInch();
FrictionForce.y = (hitSidewaysDirection.y * fy + hitForwardDirection.y * fx).MeterToInch();
FrictionForce.z = (hitSidewaysDirection.z * fy + hitForwardDirection.z * fx).MeterToInch();
//DebugOverlay.Normal( WorldPosition, hitSidewaysDirection * 10, overlay: true, color: Color.Red );
//DebugOverlay.Normal( WorldPosition, hitForwardDirection * 10, overlay: true, color: Color.Green );
//DebugOverlay.Normal( WorldPosition, FrictionForce.ClampLength( 30 ), overlay: true, color: Color.Cyan );
//DebugOverlay.ScreenText( Scene.Camera.PointToScreenPixels( WorldPosition ), $"{ForwardFriction}\nMotor:{(int)motorTorque}\nBrake:{(int)brakeTorque}", flags: TextFlag.LeftTop );
}
else
FrictionForce = Vector3.Zero;

View File

@@ -1,4 +1,5 @@
using Sandbox;
using System;
namespace VeloX;

View File

@@ -34,7 +34,7 @@ public partial class VeloXWheel
{
GameObject go = new()
{
WorldPosition = ContactPosition + Vehicle.WorldRotation.Down * (Radius.MeterToInch() - 0.01f),
WorldPosition = ContactPosition + Vehicle.WorldRotation.Down * (Radius.MeterToInch() - 1f),
WorldRotation = Rotation.LookAt( hitSidewaysDirection )
};
_skidMark = go.AddComponent<LineRenderer>();
@@ -45,7 +45,7 @@ public partial class VeloXWheel
_skidMark.CastShadows = false;
_skidMark.Width = Width.MeterToInch() / 2;
_skidMark.AutoCalculateNormals = false;
_skidMark.SplineInterpolation = 4;
_skidMark.SplineInterpolation = 1;
go.Flags = go.Flags.WithFlag( GameObjectFlags.Hidden, true );
go.Flags = go.Flags.WithFlag( GameObjectFlags.NotNetworked, true );
SkidMarks.Enqueue( _skidMark );
@@ -53,8 +53,7 @@ public partial class VeloXWheel
protected void UpdateSkid()
{
if ( IsProxy )
return;
while ( SkidMarks.Count > MaxSkid )
{
SkidMarks.Dequeue()?.DestroyGameObject();
@@ -81,7 +80,7 @@ public partial class VeloXWheel
GameObject go = new()
{
Parent = _skidMark.GameObject,
WorldPosition = ContactPosition + Vehicle.WorldRotation.Down * Radius.MeterToInch(),
WorldPosition = ContactPosition + Vehicle.WorldRotation.Down * (Radius.MeterToInch() - 1f),
WorldRotation = Rotation.LookAt( ContactNormal.RotateAround( Vector3.Zero, Rotation.FromRoll( 90 ) ) )
};
go.Flags = go.Flags.WithFlag( GameObjectFlags.Hidden, true );

View File

@@ -8,9 +8,8 @@ namespace VeloX;
public partial class VeloXWheel
{
private static readonly GameObject SmokePrefab = GameObject.GetPrefab( "prefabs/particles/tire_smoke.prefab" );
private GameObject SmokeObject { get; set; }
private GameObject SmokeObject;
public const float MIN_DRIFT_ANGLE = 10f;
public const float MIN_DRIFT_SPEED = 30f;
public const float MAX_DRIFT_ANGLE = 110f;
@@ -19,11 +18,13 @@ public partial class VeloXWheel
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 } );
SmokeObject = GameObject.GetPrefab( "prefabs/particles/tire_smoke.prefab" ).Clone( new CloneConfig() { Parent = GameObject, StartEnabled = true } );
SmokeObject.Flags = SmokeObject.Flags.WithFlag( GameObjectFlags.NotNetworked, true );
var emitter = SmokeObject.Components.Get<ParticleSphereEmitter>( FindMode.EverythingInSelfAndDescendants );
emitter.Enabled = false;
}
public float GetSlip()
{
@@ -36,12 +37,8 @@ public partial class VeloXWheel
// return val;
return val * 5;
}
protected override void OnUpdate()
private void UpdateSmoke()
{
base.OnUpdate();
if ( IsProxy )
return;
UpdateSkid();
SmokeObject.WorldPosition = ContactPosition + Vehicle.WorldRotation.Down * Radius.MeterToInch();
smokeMul = Math.Max( 0, GetSlip() - 3 );
@@ -73,11 +70,13 @@ public partial class VeloXWheel
ConstantA = 0,
ConstantB = 360,
};
effect.Scale = new()
{
Type = ParticleFloat.ValueType.Curve,
Type = ParticleFloat.ValueType.Range,
Evaluation = ParticleFloat.EvaluationType.Life,
CurveA = new( new List<Curve.Frame>() { new( 0, 10f ), new( 0.8f, 50f ), new( 1f, 160f ) } ),
ConstantA = 10,
ConstantB = 100,
};
effect.StartDelay = 0.025f + (1 - smokeMul) * 0.03f;

View File

@@ -1,4 +1,5 @@
using Sandbox;
using Sandbox.Utility;
using System;
namespace VeloX;
@@ -14,7 +15,6 @@ public partial class VeloXWheel : Component
[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, Group( "Traction" )] public TirePreset Tire { get; set; } = ResourceLibrary.Get<TirePreset>( "frictions/default.tire" );
[Property, Group( "Traction" )] public float SurfaceGrip { get; set; } = 1f;
@@ -29,7 +29,7 @@ public partial class VeloXWheel : Component
public float RPM { get => AngularVelocity * 60f / MathF.Tau; set => AngularVelocity = value / (60 / MathF.Tau); }
private Vector3 StartPos { get; set; }
[Sync] private Vector3 StartPos { get; set; }
private static Rotation CylinderOffset = Rotation.FromRoll( 90 );
[Sync] public bool IsOnGround { get; private set; }
@@ -39,13 +39,13 @@ public partial class VeloXWheel : Component
[Property] public float BrakeTorque { get; set; }
public float Compression { get; protected set; } // meters
public float LastLength { get; protected set; } // meters
[Sync( SyncFlags.Interpolate )] public float LastLength { get; protected set; } // meters
public float Fz { get; protected set; } // N
public float AngularVelocity { get; protected set; } // rad/s
public float RollAngle { get; protected set; } // degrees
[Sync( SyncFlags.Interpolate )] public float RollAngle { get; protected set; }
private VeloXBase Vehicle;
public VeloXBase Vehicle { get; private set; }
[Sync] public Vector3 ContactNormal { get; protected set; }
[Sync] public Vector3 ContactPosition { get; protected set; }
Rotation TransformRotationSteer => Vehicle.WorldTransform.RotationToWorld( Vehicle.SteerAngle * SteerMultiplier );
@@ -59,14 +59,9 @@ public partial class VeloXWheel : Component
Inertia = BaseInertia;
}
internal void Update( VeloXBase vehicle, in float dt )
private void UpdateVisuals()
{
UpdateVisuals( vehicle, dt );
}
private void UpdateVisuals( VeloXBase vehicle, in float dt )
{
WorldRotation = vehicle.WorldTransform.RotationToWorld( vehicle.SteerAngle * SteerMultiplier ).RotateAroundAxis( Vector3.Right, -RollAngle );
WorldRotation = Vehicle.WorldTransform.RotationToWorld( Vehicle.SteerAngle * SteerMultiplier ).RotateAroundAxis( Vector3.Right, -RollAngle );
LocalPosition = StartPos + Vector3.Down * LastLength.MeterToInch();
}
@@ -80,6 +75,7 @@ public partial class VeloXWheel : Component
public void UpdateForce()
{
Vehicle.Body.ApplyForceAt( ContactPosition, FrictionForce + ContactNormal * Fz.MeterToInch() );
FrictionForce = 0;
}
internal void StepPhys( VeloXBase vehicle, in float dt )
@@ -110,7 +106,6 @@ public partial class VeloXWheel : Component
Compression = wheelTraceData.Compression / hitCount;
ContactNormal = (wheelTraceData.ContactNormal / hitCount).Normal;
ContactPosition = wheelTraceData.ContactPosition / hitCount;
//DoSuspensionSounds( vehicle, (RestLength - Compression) * 0.8f);
LastLength = RestLength - Compression;
UpdateHitVariables();
@@ -119,28 +114,54 @@ public partial class VeloXWheel : Component
else
{
IsOnGround = false;
// Wheel is off the ground
Compression = 0f;
Fz = 0f;
ContactNormal = Vector3.Up;
ContactPosition = WorldPosition;
LastLength = RestLength;
UpdateHitVariables();
UpdateFriction( dt );
}
}
const string playerTag = "player";
private bool TraceWheel( VeloXBase vehicle, ref WheelTraceData wheelTraceData, Vector3 start, Vector3 end, float width, in float dt )
{
SceneTraceResult trace;
if ( IsOnGround && vehicle.TotalSpeed < 550 )
{
trace = Scene.Trace
.FromTo( start, end )
.Cylinder( width, Radius.MeterToInch() )
.Rotated( vehicle.WorldRotation * CylinderOffset )
.UseHitPosition( false )
.IgnoreGameObjectHierarchy( Vehicle.GameObject )
.WithCollisionRules( Vehicle.GameObject.Tags )
.Run();
var trace = Scene.Trace
.FromTo( start, end )
.Cylinder( width, Radius.MeterToInch() )
.Rotated( vehicle.WorldRotation * CylinderOffset )
.UseHitPosition( false )
.IgnoreGameObjectHierarchy( Vehicle.GameObject )
.WithoutTags( playerTag )
.Run();
if ( trace.StartedSolid )
{
trace = Scene.Trace
.FromTo( start, end + Vehicle.WorldRotation.Down * Radius.MeterToInch() )
.UseHitPosition( false )
.IgnoreGameObjectHierarchy( Vehicle.GameObject )
.WithCollisionRules( Vehicle.GameObject.Tags )
.Run();
trace.EndPosition += Vehicle.WorldRotation.Up * Math.Min( Radius.MeterToInch(), trace.Distance );
}
}
else
{
trace = Scene.Trace
.FromTo( start, end + Vehicle.WorldRotation.Down * Radius.MeterToInch() )
.UseHitPosition( false )
.IgnoreGameObjectHierarchy( Vehicle.GameObject )
.WithCollisionRules( Vehicle.GameObject.Tags )
.Run();
trace.EndPosition += Vehicle.WorldRotation.Up * Math.Min( Radius.MeterToInch(), trace.Distance );
}
//DebugOverlay.Trace( trace, overlay: true );
if ( trace.Hit )
@@ -160,10 +181,6 @@ public partial class VeloXWheel : Component
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;
@@ -185,40 +202,20 @@ public partial class VeloXWheel : Component
StepRotation( Vehicle, dt );
}
const float HubCoulombNm = 20f;
const float HubViscous = 0.1f;
private void StepRotation( VeloXBase vehicle, in float dt )
{
float inertia = MathF.Max( 1f, Inertia );
float roadTorque = ForwardFriction.Speed * Radius;
float externalTorque = DriveTorque - roadTorque;
float rollingResistanceTorque = Fz * Radius * SurfaceResistance;
float coulombTorque = BrakeTorque + rollingResistanceTorque + HubCoulombNm;
float omega = AngularVelocity;
if ( MathF.Abs( omega ) < 1e-6f && MathF.Abs( externalTorque ) <= coulombTorque )
{
AngularVelocity = 0f;
}
else
{
if ( HubViscous > 0f ) omega *= MathF.Exp( -(HubViscous / inertia) * dt );
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;
RollAngle += MathX.RadianToDegree( AngularVelocity ) * dt;
RollAngle = (RollAngle % 360f + 360f) % 360f;
}
protected override void OnFixedUpdate()
{
UpdateSmoke();
}
protected override void OnUpdate()
{
UpdateVisuals();
UpdateSkid();
}
}

View File

@@ -1,6 +1,4 @@
using Sandbox;
using System;
using System.Diagnostics;
using System.Linq;
namespace VeloX;
@@ -10,13 +8,16 @@ internal sealed class WheelManager : GameObjectSystem
public WheelManager( Scene scene ) : base( scene )
{
if ( Application.IsDedicatedServer )
return;
Listen( Stage.StartFixedUpdate, -99, UpdateWheels, "UpdateWheels" );
Listen( Stage.StartFixedUpdate, -100, UpdateEngine, "UpdateEngine" );
}
private void UpdateWheels()
{
if ( !Game.IsPlaying )
if ( !Game.IsPlaying || Scene.IsEditor )
return;
//Stopwatch sw = Stopwatch.StartNew();
@@ -24,13 +25,18 @@ internal sealed class WheelManager : GameObjectSystem
if ( !wheels.Any() ) return;
var timeDelta = Time.Delta;
Sandbox.Utility.Parallel.ForEach( wheels, item =>
{
if ( !item.IsProxy )
//Sandbox.Utility.Parallel.ForEach( wheels, item =>
//{
// if ( !item.IsProxy && item.IsValid() )
// item.DoPhysics( timeDelta );
//} );
foreach ( var item in wheels )
if ( item.IsValid() && !item.IsProxy )
item.DoPhysics( timeDelta );
} );
foreach ( var wheel in wheels )
wheel.UpdateForce();
if ( wheel.IsValid() && !wheel.IsProxy)
wheel.UpdateForce();
//sw.Stop();
@@ -38,7 +44,7 @@ internal sealed class WheelManager : GameObjectSystem
}
private void UpdateEngine()
{
if ( !Game.IsPlaying )
if ( !Game.IsPlaying || Scene.IsEditor )
return;
//Stopwatch sw = Stopwatch.StartNew();
@@ -46,11 +52,17 @@ internal sealed class WheelManager : GameObjectSystem
if ( !engines.Any() ) return;
var timeDelta = Time.Delta;
Sandbox.Utility.Parallel.ForEach( engines, item =>
{
if ( !item.IsProxy )
item.UpdateEngine( timeDelta );
} );
//Sandbox.Utility.Parallel.ForEach( engines, item =>
//{
// foreach ( var wheel in engines )
// if ( !wheel.IsProxy && wheel.IsValid() )
// wheel.UpdateEngine( timeDelta );
//} );
foreach ( var wheel in engines )
if ( wheel.IsValid() )
wheel.UpdateEngine( timeDelta );
//sw.Stop();
//DebugOverlaySystem.Current.ScreenText( new Vector2( 120, 54 ), $"Engine Sim: {sw.Elapsed.TotalMilliseconds,6:F2} ms", 24 );

37
Code/Car/VeloXCar.ABS.cs Normal file
View File

@@ -0,0 +1,37 @@
using Sandbox;
using System;
namespace VeloX;
public partial class VeloXCar
{
public bool ABSActive { get; private set; } = true;
public static bool UseABS = true;
private void UpdateABS()
{
ABSActive = false;
if ( !UseABS )
return;
if ( TotalSpeed < 100 || SteeringAngle.AlmostEqual( 0, 1 ) )
return;
if ( Brakes == 0 || CarDirection != 1 || Engine.RevLimiterActive || Handbrake >= 0.1f )
return;
foreach ( var wheel in Wheels )
{
if ( !wheel.IsOnGround )
continue;
if ( wheel.wheelIsBlocked )
{
ABSActive = true;
wheel.BrakeTorque = 0f;
}
}
}
}

View File

@@ -1,6 +1,7 @@
using Sandbox;
using Sandbox.Audio;
using System;
namespace VeloX;
@@ -48,23 +49,27 @@ public partial class VeloXCar
protected virtual void UpdateDrift( float dt )
{
float driftAngle = GetDriftAngle();
float mul = (driftAngle - MIN_DRIFT_ANGLE) / (90 - MIN_DRIFT_ANGLE);
var avgslip = 0f;
foreach ( var item in Wheels )
avgslip += item.NormalizedLongitudinalSlip + item.NormalizedLateralSlip;
if ( !_skidHandle.IsValid() )
_skidHandle = Sound.PlayFile( SkidSound );
if ( !_skidHandle.IsValid() )
return;
float mul = Math.Clamp( avgslip, 0, 2 );
targetVolume = mul;
targetPitch = 0.75f + 0.25f * mul;
if ( mul > 0.1f && !_skidHandle.IsValid() )
{
_skidHandle = Sound.PlayFile( SkidSound );
_skidHandle.TargetMixer = Mixer.Default;
}
if ( !_skidHandle.IsValid() )
return;
_skidHandle.Pitch += (targetPitch - _skidHandle.Pitch) * dt * 5f;
_skidHandle.Volume += (targetVolume - _skidHandle.Volume) * dt * 10f;
_skidHandle.Position = WorldPosition;
}
}

44
Code/Car/VeloXCar.ESC.cs Normal file
View File

@@ -0,0 +1,44 @@
using Sandbox;
using Sandbox.VR;
using System;
using VeloX.Utils;
namespace VeloX;
public partial class VeloXCar
{
public bool ESCActive { get; private set; } = true;
public static bool UseESC = true;
private void UpdateESC()
{
ESCActive = false;
if ( !UseESC )
return;
if ( TotalSpeed < 100 || CarDirection != 1 )
return;
float angle = Body.Velocity.SignedAngle( WorldRotation.Forward, WorldRotation.Up ); ;
angle -= SteerAngle.yaw * 0.5f;
float absAngle = angle < 0 ? -angle : angle;
if ( Engine.RevLimiterActive || absAngle < 2f )
return;
foreach ( var wheel in Wheels )
{
if ( !wheel.IsOnGround )
continue;
float additionalBrakeTorque = -angle * Math.Sign( wheel.LocalPosition.y ) * 20f;
if ( additionalBrakeTorque > 0 )
{
ESCActive = true;
wheel.BrakeTorque += additionalBrakeTorque;
}
}
}
}

View File

@@ -21,19 +21,14 @@ public partial class VeloXCar
public int CarDirection { get { return ForwardSpeed < 1 ? 0 : (VelocityAngle < 90 && VelocityAngle > -90 ? 1 : -1); } }
public float VelocityAngle { get; private set; }
[Sync] public float Steering { get; private set; }
private float currentSteerAngle;
private float inputSteer;
private void UpdateSteering( float dt )
{
inputSteer = Input.AnalogMove.y;
float targetSteerAngle = inputSteer * MaxSteerAngle;
float targetSteerAngle = SteeringAngle * MaxSteerAngle;
if ( !Input.Down( "Jump" ) )
targetSteerAngle *= Math.Clamp( 1 - Math.Clamp( TotalSpeed / 3000, 0.01f, 0.9f ), -1, 1 );
targetSteerAngle *= Math.Clamp( 1 - Math.Clamp( TotalSpeed / 3000, 0f, 0.85f ), -1, 1 );
VelocityAngle = -Body.Velocity.SignedAngle( WorldRotation.Forward, WorldRotation.Up );
@@ -42,10 +37,9 @@ public partial class VeloXCar
if ( TotalSpeed > 150 && CarDirection > 0 && IsOnGround )
targetAngle = VelocityAngle * MaxSteerAngleMultiplier;
float lerpSpeed = Math.Abs( inputSteer ) < 0.1f ? SteerReturnSpeed : SteerInputResponse;
float lerpSpeed = Math.Abs( SteeringAngle ) < 0.1f ? SteerReturnSpeed : SteerInputResponse;
currentSteerAngle = ExpDecay( currentSteerAngle, targetSteerAngle, lerpSpeed, Time.Delta );
Steering = currentSteerAngle + targetAngle;
SteerAngle = new( 0, Math.Clamp( Steering, -MaxSteerAngle, MaxSteerAngle ), 0 );
SteerAngle = new( 0, Math.Clamp( currentSteerAngle + targetAngle, -MaxSteerAngle, MaxSteerAngle ), 0 );
}
}

39
Code/Car/VeloXCar.TCS.cs Normal file
View File

@@ -0,0 +1,39 @@
using Sandbox;
using System;
namespace VeloX;
public partial class VeloXCar
{
public bool TCSActive { get; private set; } = true;
public static bool UseTCS = true;
private void UpdateTCS()
{
TCSActive = false;
if ( !UseTCS )
return;
if ( TotalSpeed < 50 || CarDirection != 1 )
return;
float vehicleSpeed = TotalSpeed.InchToMeter();
foreach ( var wheel in Wheels )
{
if ( !wheel.IsOnGround )
continue;
float wheelLinearSpeed = wheel.AngularVelocity * wheel.Radius;
float wheelSlip = wheelLinearSpeed - vehicleSpeed;
if ( wheelSlip > 2.0f )
{
TCSActive = true;
wheel.BrakeTorque = wheelSlip * 1000;
}
}
}
}

View File

@@ -7,14 +7,15 @@ namespace VeloX;
[Title( "VeloX - Car" )]
public partial class VeloXCar : VeloXBase
{
protected override void OnFixedUpdate()
{
if ( IsProxy )
return;
base.OnFixedUpdate();
if ( IsProxy )
return;
UpdateABS();
UpdateESC();
UpdateTCS();
var dt = Time.Delta;
//EngineThink( dt );
SimulateAerodinamics( dt );
@@ -26,14 +27,24 @@ public partial class VeloXCar : VeloXBase
private void UpdateUnflip( float dt )
{
if ( Math.Abs( inputSteer ) < 0.1f )
if ( Math.Abs( SteeringAngle ) < 0.1f )
return;
if ( Math.Abs( WorldRotation.Angles().roll ) < 70 )
if ( IsOnGround || Math.Abs( WorldRotation.Roll() ) < 60 )
return;
var angVel = Body.AngularVelocity;
var force = inputSteer * Mass * Math.Clamp( 1 - angVel.x / 50, 0, 1 ) * 0.05f;
var angVel = Body.WorldTransform.PointToLocal( WorldPosition + Body.AngularVelocity );
float maxAngularVelocity = 2.0f;
float velocityFactor = 1.0f - Math.Clamp( Math.Abs( angVel.x ) / maxAngularVelocity, 0f, 1f );
if ( velocityFactor <= 0.01f )
return;
var force = SteeringAngle * velocityFactor * 150;
Body.AngularVelocity -= Body.WorldRotation.Forward * force * dt;
}
}

View File

@@ -1,43 +1,80 @@
using Sandbox;
namespace VeloX;
public class InputResolver
{
public Connection Driver { get; internal set; }
private bool IsDriverActive => Driver is not null;
public Vector2 MouseDelta => IsDriverActive ? Input.MouseDelta : default;
public Vector2 MouseWheel => IsDriverActive ? Input.MouseWheel : default;
public Angles AnalogLook => IsDriverActive ? Input.AnalogLook : default;
public Vector3 AnalogMove => IsDriverActive ? Input.AnalogMove : default;
public Vector2 MouseDelta => Input.MouseDelta;
public Vector2 MouseWheel => Input.MouseWheel;
public Angles AnalogLook => Input.AnalogLook;
public Vector3 AnalogMove
{
get
{
if ( Input.UsingController )
{
Vector2 input = 0;
input.x = Input.GetAnalog( InputAnalog.RightTrigger ) - Input.GetAnalog( InputAnalog.LeftTrigger );
input.y = -Input.GetAnalog( InputAnalog.LeftStickX );
return input;
}
return Input.AnalogMove;
}
}
public bool Down( string action )
{
return IsDriverActive && Input.Down( action );
return Input.Down( action );
}
public float Brake
{
get
{
if ( Input.UsingController )
return Input.GetAnalog( InputAnalog.LeftTrigger );
return Input.Down( "Brake" ) ? 1 : 0;
}
}
public float Throttle
{
get
{
if ( Input.UsingController )
return Input.GetAnalog( InputAnalog.RightTrigger );
return Input.Down( "Throttle" ) ? 1 : 0;
}
}
public bool Pressed( string action )
{
return IsDriverActive && Input.Pressed( action );
return Input.Pressed( action );
}
public bool Released( string action )
{
return IsDriverActive && Input.Released( action );
return Input.Released( action );
}
public float GetAnalog( InputAnalog analog )
{
return Input.GetAnalog( analog );
}
public void TriggerHaptics( float leftMotor, float rightMotor, float leftTrigger = 0f, float rightTrigger = 0f, int duration = 500 )
{
if ( IsDriverActive )
{
Input.TriggerHaptics( leftMotor, rightMotor, leftTrigger, rightTrigger, duration );
}
Input.TriggerHaptics( leftMotor, rightMotor, leftTrigger, rightTrigger, duration );
}
public void TriggerHaptics( HapticEffect effect, float lengthScale = 1, float frequencyScale = 1, float amplitudeScale = 1 )
{
Input.TriggerHaptics( effect, lengthScale, frequencyScale, amplitudeScale );
}
public void StopAllHaptics()
{
if ( IsDriverActive )
Input.StopAllHaptics();
Input.StopAllHaptics();
}
}

View File

@@ -10,7 +10,7 @@ namespace VeloX;
public class EngineStreamPlayer( EngineStream stream ) : IDisposable
{
private static readonly Mixer EngineMixer = Mixer.FindMixerByName( "Car Engine" );
private static readonly Mixer EngineMixer = Mixer.FindMixerByName( "Engine" );
public EngineStream Stream { get; set; } = stream;
public EngineState EngineState { get; set; }
@@ -26,7 +26,7 @@ public class EngineStreamPlayer( EngineStream stream ) : IDisposable
public void Update( float deltaTime, Vector3 position, bool isLocal = false )
{
var globalPitch = 1.0f;
// Gear wobble effect
@@ -51,7 +51,7 @@ public class EngineStreamPlayer( EngineStream stream ) : IDisposable
foreach ( var (id, layer) in Stream.Layers )
{
EngineSounds.TryGetValue( layer, out var channel );
if ( !channel.IsValid() && layer.AudioPath.IsValid() )
{
channel = Sound.PlayFile( layer.AudioPath );
@@ -103,6 +103,7 @@ public class EngineStreamPlayer( EngineStream stream ) : IDisposable
channel.Position = position;
channel.ListenLocal = isLocal;
channel.Paused = EngineSoundPaused || layer.IsMuted;
channel.FollowParent = true;
channel.TargetMixer = EngineMixer;
}

View File

@@ -6,6 +6,6 @@ public class TestInit
[AssemblyInitialize]
public static void ClassInitialize( TestContext context )
{
Sandbox.Application.InitUnitTest();
Sandbox.Application.InitUnitTest<TestInit>( false, false );
}
}