Compare commits

..

8 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
24 changed files with 373 additions and 274 deletions

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.

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
{
@@ -106,15 +110,26 @@ public abstract partial class VeloXBase
}
protected void UpdateInput()
{
VerticalInput = Input.AnalogMove.x;
Handbrake = Input.Down( "Handbrake" ) ? 1 : 0;
//VerticalInput = Input.AnalogMove.x;
if ( IsDriverActive )
{
SteeringAngle = Input.AnalogMove.y;
Brakes = Input.Brake;
Throttle = Input.Throttle;
IsClutching = Input.Down( "Clutch" ) ? 1 : 0;
Handbrake = Input.Down( "Handbrake" ) ? 1 : 0;
IsShiftingUp = Input.Pressed( "Shift Up" );
IsShiftingDown = Input.Pressed( "Shift Down" );
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;

View File

@@ -5,7 +5,6 @@ namespace VeloX;
public abstract partial class VeloXBase
{
private Vector3 linForce;
private Vector3 angForce;
protected const float BrakeForce = 4500f;
protected const float HandbrakeForce = 35000f;
@@ -13,18 +12,16 @@ public abstract partial class VeloXBase
{
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;
@@ -47,23 +44,19 @@ public abstract partial class VeloXBase
}
if ( TotalSpeed < 1 && Input.AnalogMove.x == 0 )
if ( TotalSpeed < 1 && !AnyInput )
{
v.BrakeTorque = HandbrakeForce;
}
v.Update( this, in dt );
v.DoPhysics( in dt );
}
Body.Velocity = vehVel;
Body.AngularVelocity = vehAngVel;
}
Body.ApplyForce( linForce );
Body.ApplyTorque( angForce );
//Body.ApplyForce( linForce );
//Body.ApplyTorque( angForce );
}

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,18 +26,24 @@ 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();

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

@@ -35,8 +35,6 @@ public partial class VeloXWheel
public float CounterTorque { get; private set; }
//[Property, Range( 0, 2 )] public float BrakeMult { get; set; } = 1f;
public Friction ForwardFriction = new();
public Friction SidewayFriction = new();
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,6 +96,7 @@ public partial class VeloXWheel
private Vector3 currentPosition;
private Vector3 referenceError;
private Vector3 correctiveForce;
public bool wheelIsBlocked;
private void UpdateFriction( float dt )
{
var motorTorque = DriveTorque;
@@ -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 );

View File

@@ -1,4 +1,5 @@
using Sandbox;
using Sandbox.Utility;
using System;
namespace VeloX;
@@ -28,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; }
@@ -38,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 );
@@ -58,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();
}
@@ -79,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 )
@@ -109,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();
@@ -118,11 +114,14 @@ 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 );
}
}
@@ -130,15 +129,39 @@ public partial class VeloXWheel : Component
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 )
.WithCollisionRules( Vehicle.GameObject.Tags )
.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 )
@@ -179,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,6 +8,9 @@ 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" );
}
@@ -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();
@@ -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 );

View File

@@ -15,7 +15,7 @@ public partial class VeloXCar
if ( !UseABS )
return;
if ( TotalSpeed < 100 || ABSActive || Steering.AlmostEqual( 0, 1 ) )
if ( TotalSpeed < 100 || SteeringAngle.AlmostEqual( 0, 1 ) )
return;
@@ -26,10 +26,10 @@ public partial class VeloXCar
if ( !wheel.IsOnGround )
continue;
if ( wheel.NormalizedLongitudinalSlip >= 0.55f )
if ( wheel.wheelIsBlocked )
{
ABSActive = true;
wheel.BrakeTorque *= 0.25f;
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;
}
}

View File

@@ -32,7 +32,6 @@ public partial class VeloXCar
if ( !wheel.IsOnGround )
continue;
float additionalBrakeTorque = -angle * Math.Sign( wheel.LocalPosition.y ) * 20f;
if ( additionalBrakeTorque > 0 )
{

View File

@@ -21,16 +21,11 @@ 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, 0f, 0.85f ), -1, 1 );
@@ -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,23 +7,15 @@ namespace VeloX;
[Title( "VeloX - Car" )]
public partial class VeloXCar : VeloXBase
{
protected override void FixedUpdate()
{
UpdateInput();
PhysicsSimulate();
UpdateABS();
UpdateESC();
}
protected override void OnFixedUpdate()
{
if ( IsProxy )
return;
base.OnFixedUpdate();
if ( IsProxy )
return;
UpdateABS();
UpdateESC();
UpdateTCS();
var dt = Time.Delta;
//EngineThink( dt );
SimulateAerodinamics( dt );
@@ -35,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,15 +1,13 @@
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 Vector2 MouseDelta => Input.MouseDelta;
public Vector2 MouseWheel => Input.MouseWheel;
public Angles AnalogLook => Input.AnalogLook;
public Vector3 AnalogMove
{
get
@@ -21,43 +19,62 @@ public class InputResolver
input.y = -Input.GetAnalog( InputAnalog.LeftStickX );
return input;
}
return IsDriverActive ? Input.AnalogMove : default;
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 )
{
if ( IsDriverActive )
{
Input.TriggerHaptics( effect, lengthScale, frequencyScale, amplitudeScale );
}
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;
}