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, "C": 2.15,
"D": 0.833, "D": 0.933,
"E": 0.992, "E": 0.971,
"__references": [], "__references": [],
"__version": 0 "__version": 0
} }

View File

@@ -12,6 +12,7 @@
"NetworkMode": 2, "NetworkMode": 2,
"NetworkInterpolation": true, "NetworkInterpolation": true,
"NetworkOrphaned": 0, "NetworkOrphaned": 0,
"NetworkTransmit": true,
"OwnerTransfer": 1, "OwnerTransfer": 1,
"Components": [], "Components": [],
"Children": [ "Children": [
@@ -28,6 +29,7 @@
"NetworkMode": 2, "NetworkMode": 2,
"NetworkInterpolation": true, "NetworkInterpolation": true,
"NetworkOrphaned": 0, "NetworkOrphaned": 0,
"NetworkTransmit": true,
"OwnerTransfer": 1, "OwnerTransfer": 1,
"Components": [ "Components": [
{ {
@@ -197,7 +199,7 @@
"FaceVelocity": false, "FaceVelocity": false,
"FogStrength": 1, "FogStrength": 1,
"LeadingTrail": true, "LeadingTrail": true,
"Lighting": false, "Lighting": true,
"MotionBlur": false, "MotionBlur": false,
"OnComponentDestroy": null, "OnComponentDestroy": null,
"OnComponentDisabled": null, "OnComponentDisabled": null,
@@ -206,6 +208,7 @@
"OnComponentStart": null, "OnComponentStart": null,
"OnComponentUpdate": null, "OnComponentUpdate": null,
"Opaque": false, "Opaque": false,
"PlaybackSpeed": 1,
"RenderOptions": { "RenderOptions": {
"GameLayer": true, "GameLayer": true,
"OverlayLayer": false, "OverlayLayer": false,
@@ -215,7 +218,7 @@
"RotationOffset": 0, "RotationOffset": 0,
"Scale": 1, "Scale": 1,
"Shadows": true, "Shadows": true,
"SortMode": "Unsorted", "SortMode": "ByDistance",
"Sprite": { "Sprite": {
"$compiler": "embed", "$compiler": "embed",
"$source": null, "$source": null,
@@ -228,7 +231,8 @@
"LoopMode": "Loop", "LoopMode": "Loop",
"Frames": [ "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; ClutchInput = 1f;
} }
if ( Controller.SwappedBrakes > 0 ) if ( Controller.SwappedBrakes > 0 && Controller.SwappedThrottle == 0 )
{ {
ClutchInput = 0; ClutchInput = 0;
} }
@@ -173,7 +173,7 @@ public partial class Clutch : PowertrainComponent
OutputInertia = (inertiaSum + halfClutchInertia) * _clutchEngagement + halfClutchInertia; OutputInertia = (inertiaSum + halfClutchInertia) * _clutchEngagement + halfClutchInertia;
// Allow the torque output to be only up to the slip torque valu // 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 = InputTorque;
OutputTorque = Math.Clamp( OutputTorque, 0, outputTorqueClamp ); OutputTorque = Math.Clamp( OutputTorque, 0, outputTorqueClamp );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,13 +1,23 @@
using Sandbox; using Sandbox;
using System;
namespace VeloX; namespace VeloX;
public abstract partial class VeloXBase : Component public abstract partial class VeloXBase : Component, IGameObjectNetworkEvents
{ {
[Sync] public WaterState WaterState { get; set; } [Sync, Change( nameof( OnEngineIgnitionChange ) )]
[Sync] public bool IsEngineOnFire { get; set; } public bool EngineIgnition { get; set; }
[Property, Sync] public EngineState EngineState { 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 Vector3 AngularDrag { get; set; } = new( -0.1f, -0.1f, -3 );
[Property] public float Mass { get; set; } = 900; [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; } [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 ForwardSpeed;
public float TotalSpeed; public float TotalSpeed;
protected override void OnFixedUpdate() protected override void OnFixedUpdate()
{ {
if ( !IsProxy )
{
LocalVelocity = WorldTransform.PointToLocal( WorldPosition + Body.Velocity );
Velocity = Body.Velocity;
}
ForwardSpeed = LocalVelocity.x;
TotalSpeed = LocalVelocity.Length;
if ( IsProxy ) if ( IsProxy )
return; return;
LocalVelocity = WorldTransform.PointToLocal( WorldPosition + Body.Velocity );
ForwardSpeed = LocalVelocity.x;
TotalSpeed = LocalVelocity.Length;
Body.PhysicsBody.Mass = Mass; Body.PhysicsBody.Mass = Mass;
FixedUpdate();
}
protected virtual void FixedUpdate()
{
UpdateInput(); UpdateInput();
PhysicsSimulate(); PhysicsSimulate();
} }
} }

View File

@@ -1,11 +1,16 @@
namespace VeloX; using System;
namespace VeloX;
public struct Friction public struct Friction
{ {
public float SlipCoef { get; set; }
public float ForceCoef { get; set; }
public float Force { get; set; } public float Force { get; set; }
public float Slip { get; set; } public float Slip { get; set; }
public float Speed { 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> /// </summary>
public float CounterTorque { get; private set; } public float CounterTorque { get; private set; }
[Property, Range( 0, 2 )] public float BrakeMult { get; set; } = 1f; //[Property, Range( 0, 2 )] public float BrakeMult { get; set; } = 1f;
public Friction ForwardFriction = new();
public Friction SidewayFriction = new();
public Vector3 FrictionForce; public Vector3 FrictionForce;
@@ -47,31 +45,32 @@ public partial class VeloXWheel
public Vector3 ContactRight => hitSidewaysDirection; public Vector3 ContactRight => hitSidewaysDirection;
public Vector3 ContactForward => hitForwardDirection; public Vector3 ContactForward => hitForwardDirection;
[Sync] public float LongitudinalSlip => sx;
public float LongitudinalSlip public float LongitudinalSpeed => vx;
{
get => ForwardFriction.Slip;
private set => ForwardFriction.Slip = value;
}
public float LongitudinalSpeed => ForwardFriction.Speed;
public bool IsSkiddingLongitudinally => NormalizedLongitudinalSlip > 0.35f; public bool IsSkiddingLongitudinally => NormalizedLongitudinalSlip > 0.35f;
public float NormalizedLongitudinalSlip => Math.Clamp( Math.Abs( LongitudinalSlip ), 0, 1 ); public float NormalizedLongitudinalSlip => Math.Clamp( Math.Abs( LongitudinalSlip ), 0, 1 );
public float LateralSlip => sy;
[Sync] public float LateralSpeed => vy;
public float LateralSlip
{
get => SidewayFriction.Slip;
private set => SidewayFriction.Slip = value;
}
public float LateralSpeed => SidewayFriction.Speed;
public bool IsSkiddingLaterally => NormalizedLateralSlip > 0.35f; public bool IsSkiddingLaterally => NormalizedLateralSlip > 0.35f;
public float NormalizedLateralSlip => Math.Clamp( Math.Abs( LateralSlip ), 0, 1 ); public float NormalizedLateralSlip => Math.Clamp( Math.Abs( LateralSlip ), 0, 1 );
public bool IsSkidding => IsSkiddingLaterally || IsSkiddingLongitudinally; public bool IsSkidding => IsSkiddingLaterally || IsSkiddingLongitudinally;
public float NormalizedSlip => (NormalizedLateralSlip + NormalizedLongitudinalSlip) / 2f; 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() private void UpdateHitVariables()
{ {
if ( IsOnGround ) if ( IsOnGround )
@@ -81,13 +80,13 @@ public partial class VeloXWheel
hitForwardDirection = ContactNormal.Cross( TransformRotationSteer.Right ).Normal; hitForwardDirection = ContactNormal.Cross( TransformRotationSteer.Right ).Normal;
hitSidewaysDirection = Rotation.FromAxis( ContactNormal, 90f ) * hitForwardDirection; hitSidewaysDirection = Rotation.FromAxis( ContactNormal, 90f ) * hitForwardDirection;
ForwardFriction.Speed = hitContactVelocity.Dot( hitForwardDirection ).InchToMeter(); vx = hitContactVelocity.Dot( hitForwardDirection ).InchToMeter();
SidewayFriction.Speed = hitContactVelocity.Dot( hitSidewaysDirection ).InchToMeter(); vy = hitContactVelocity.Dot( hitSidewaysDirection ).InchToMeter();
} }
else else
{ {
ForwardFriction.Speed = 0f; vx = 0;
SidewayFriction.Speed = 0f; vy = 0;
} }
} }
@@ -97,10 +96,11 @@ public partial class VeloXWheel
private Vector3 currentPosition; private Vector3 currentPosition;
private Vector3 referenceError; private Vector3 referenceError;
private Vector3 correctiveForce; private Vector3 correctiveForce;
public bool wheelIsBlocked;
private void UpdateFriction( float dt ) private void UpdateFriction( float dt )
{ {
var motorTorque = DriveTorque; var motorTorque = DriveTorque;
var brakeTorque = BrakeTorque * BrakeMult; var brakeTorque = BrakeTorque;
float allWheelLoadSum = Vehicle.CombinedLoad; float allWheelLoadSum = Vehicle.CombinedLoad;
@@ -120,26 +120,21 @@ public partial class VeloXWheel
float loadPercent = Math.Clamp( Fz / LoadRating, 0f, 1f ); float loadPercent = Math.Clamp( Fz / LoadRating, 0f, 1f );
float slipLoadModifier = 1f - loadPercent * 0.4f; float slipLoadModifier = 1f - loadPercent * 0.4f;
//DebugOverlay.Text( WorldPosition, SidewayFriction.Speed.ToString(), overlay: true );
float mass = Vehicle.Body.Mass; float mass = Vehicle.Body.Mass;
float absForwardSpeed = Math.Abs( ForwardFriction.Speed ); float absForwardSpeed = Math.Abs( vx );
float forwardForceClamp = mass * LoadContribution * absForwardSpeed * invDt; float forwardForceClamp = mass * LoadContribution * absForwardSpeed * invDt;
float absSideSpeed = Math.Abs( SidewayFriction.Speed ); float absSideSpeed = Math.Abs( vy );
float sideForceClamp = mass * LoadContribution * absSideSpeed * invDt; float sideForceClamp = mass * LoadContribution * absSideSpeed * invDt;
float forwardSpeedClamp = 1.5f * (dt / 0.005f); float forwardSpeedClamp = 1.5f * (dt / 0.005f);
forwardSpeedClamp = Math.Clamp( forwardSpeedClamp, 1.5f, 10f ); forwardSpeedClamp = Math.Clamp( forwardSpeedClamp, 1.5f, 10f );
float clampedAbsForwardSpeed = Math.Max( absForwardSpeed, forwardSpeedClamp ); float clampedAbsForwardSpeed = Math.Max( absForwardSpeed, forwardSpeedClamp );
// Calculate effect of camber on friction float peakForwardFrictionForce = 11000 * (1 - MathF.Exp( -0.00014f * forwardLoadFactor ));
float camberFrictionCoeff = Math.Max( 0, Vehicle.WorldRotation.Up.Dot( ContactNormal ) );
float peakForwardFrictionForce = forwardLoadFactor;
float absCombinedBrakeTorque = Math.Max( 0, brakeTorque + RollingResistanceTorque ); 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 signedCombinedBrakeForce = signedCombinedBrakeTorque * invRadius;
float motorForce = motorTorque * invRadius; float motorForce = motorTorque * invRadius;
float forwardInputForce = motorForce + signedCombinedBrakeForce; float forwardInputForce = motorForce + signedCombinedBrakeForce;
@@ -149,10 +144,10 @@ public partial class VeloXWheel
float maxForwardForce = Math.Min( peakForwardFrictionForce, forwardForceClamp ); float maxForwardForce = Math.Min( peakForwardFrictionForce, forwardForceClamp );
maxForwardForce = absMotorTorque < absBrakeTorque ? maxForwardForce : peakForwardFrictionForce; maxForwardForce = absMotorTorque < absBrakeTorque ? maxForwardForce : peakForwardFrictionForce;
ForwardFriction.Force = forwardInputForce > maxForwardForce ? maxForwardForce fx = forwardInputForce > maxForwardForce ? maxForwardForce
: forwardInputForce < -maxForwardForce ? -maxForwardForce : forwardInputForce; : forwardInputForce < -maxForwardForce ? -maxForwardForce : forwardInputForce;
bool wheelIsBlocked = false; wheelIsBlocked = false;
if ( IsOnGround ) if ( IsOnGround )
{ {
float combinedWheelForce = motorForce + absCombinedBrakeTorque * invRadius * -Math.Sign( AngularVelocity ); float combinedWheelForce = motorForce + absCombinedBrakeTorque * invRadius * -Math.Sign( AngularVelocity );
@@ -168,14 +163,14 @@ public partial class VeloXWheel
AngularVelocity += combinedWheelForce * mRadius * invInertia * dt; AngularVelocity += combinedWheelForce * mRadius * invInertia * dt;
// Surface (corrective) force // Surface (corrective) force
float noSlipAngularVelocity = ForwardFriction.Speed * invRadius; float noSlipAngularVelocity = vx * invRadius;
float angularVelocityError = AngularVelocity - noSlipAngularVelocity; float angularVelocityError = AngularVelocity - noSlipAngularVelocity;
float angularVelocityCorrectionForce = Math.Clamp( -angularVelocityError * inertia * invRadius * invDt, -maxForwardForce, maxForwardForce ); float angularVelocityCorrectionForce = Math.Clamp( -angularVelocityError * inertia * invRadius * invDt, -maxForwardForce, maxForwardForce );
if ( absMotorTorque < absBrakeTorque && Math.Abs( wheelForceClampOverflow ) > Math.Abs( angularVelocityCorrectionForce ) ) if ( absMotorTorque < absBrakeTorque && Math.Abs( wheelForceClampOverflow ) > Math.Abs( angularVelocityCorrectionForce ) )
{ {
wheelIsBlocked = true; wheelIsBlocked = true;
AngularVelocity += ForwardFriction.Speed > 0 ? 1e-10f : -1e-10f; AngularVelocity += vx > 0 ? 1e-10f : -1e-10f;
} }
else else
{ {
@@ -193,22 +188,24 @@ public partial class VeloXWheel
float absAngularVelocity = AngularVelocity < 0 ? -AngularVelocity : AngularVelocity; float absAngularVelocity = AngularVelocity < 0 ? -AngularVelocity : AngularVelocity;
float maxCounterTorque = inertia * absAngularVelocity; 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; sy = MathF.Atan2( vy, clampedAbsForwardSpeed );
ForwardFriction.Slip *= slipLoadModifier; sy *= slipLoadModifier;
SidewayFriction.Slip = MathF.Atan2( SidewayFriction.Speed, clampedAbsForwardSpeed ); float sideSlipSign = sy > 0 ? 1 : -1;
SidewayFriction.Slip *= slipLoadModifier; float absSideSlip = Math.Abs( sy );
float peakSideFrictionForce = 18000 * (1 - MathF.Exp( -0.0001f * sideLoadFactor ));
float sideSlipSign = SidewayFriction.Slip > 0 ? 1 : -1; float sideForce = -sideSlipSign * Tire.Evaluate( absSideSlip ) * peakSideFrictionForce;
float absSideSlip = Math.Abs( SidewayFriction.Slip ); fy = Math.Clamp( sideForce, -sideForceClamp, sideForceClamp );
float peakSideFrictionForce = sideLoadFactor;
float sideForce = -sideSlipSign * Tire.Evaluate( absSideSlip ) * sideLoadFactor; // Calculate effect of camber on friction
SidewayFriction.Force = Math.Clamp( sideForce, -sideForceClamp, sideForceClamp ); float camberFrictionCoeff = Math.Max( 0, Vehicle.WorldRotation.Up.Dot( ContactNormal ) );
SidewayFriction.Force *= camberFrictionCoeff; fy *= camberFrictionCoeff;
if ( IsOnGround && absForwardSpeed < 0.12f && absSideSpeed < 0.12f ) if ( IsOnGround && absForwardSpeed < 0.12f && absSideSpeed < 0.12f )
{ {
@@ -238,56 +235,36 @@ public partial class VeloXWheel
if ( wheelIsBlocked && absAngularVelocity < 0.5f ) if ( wheelIsBlocked && absAngularVelocity < 0.5f )
{ {
ForwardFriction.Force += correctiveForce.Dot( hitForwardDirection ); fx += correctiveForce.Dot( hitForwardDirection );
} }
SidewayFriction.Force += correctiveForce.Dot( hitSidewaysDirection ); fy += correctiveForce.Dot( hitSidewaysDirection );
} }
} }
else else
{ {
lowSpeedReferenceIsSet = false; 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 ) if ( IsOnGround )
{ {
FrictionForce.x = (hitSidewaysDirection.x * SidewayFriction.Force + hitForwardDirection.x * ForwardFriction.Force).MeterToInch(); FrictionForce.x = (hitSidewaysDirection.x * fy + hitForwardDirection.x * fx).MeterToInch();
FrictionForce.y = (hitSidewaysDirection.y * SidewayFriction.Force + hitForwardDirection.y * ForwardFriction.Force).MeterToInch(); FrictionForce.y = (hitSidewaysDirection.y * fy + hitForwardDirection.y * fx).MeterToInch();
FrictionForce.z = (hitSidewaysDirection.z * SidewayFriction.Force + hitForwardDirection.z * ForwardFriction.Force).MeterToInch(); FrictionForce.z = (hitSidewaysDirection.z * fy + hitForwardDirection.z * fx).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 );
//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 else
FrictionForce = Vector3.Zero; FrictionForce = Vector3.Zero;

View File

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

View File

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

View File

@@ -8,9 +8,8 @@ namespace VeloX;
public partial class VeloXWheel 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_ANGLE = 10f;
public const float MIN_DRIFT_SPEED = 30f; public const float MIN_DRIFT_SPEED = 30f;
public const float MAX_DRIFT_ANGLE = 110f; public const float MAX_DRIFT_ANGLE = 110f;
@@ -19,11 +18,13 @@ public partial class VeloXWheel
protected override void OnStart() protected override void OnStart()
{ {
base.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() public float GetSlip()
{ {
@@ -36,12 +37,8 @@ public partial class VeloXWheel
// return val; // return val;
return val * 5; return val * 5;
} }
protected override void OnUpdate() private void UpdateSmoke()
{ {
base.OnUpdate();
if ( IsProxy )
return;
UpdateSkid();
SmokeObject.WorldPosition = ContactPosition + Vehicle.WorldRotation.Down * Radius.MeterToInch(); SmokeObject.WorldPosition = ContactPosition + Vehicle.WorldRotation.Down * Radius.MeterToInch();
smokeMul = Math.Max( 0, GetSlip() - 3 ); smokeMul = Math.Max( 0, GetSlip() - 3 );
@@ -73,11 +70,13 @@ public partial class VeloXWheel
ConstantA = 0, ConstantA = 0,
ConstantB = 360, ConstantB = 360,
}; };
effect.Scale = new() effect.Scale = new()
{ {
Type = ParticleFloat.ValueType.Curve, Type = ParticleFloat.ValueType.Range,
Evaluation = ParticleFloat.EvaluationType.Life, 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; effect.StartDelay = 0.025f + (1 - smokeMul) * 0.03f;

View File

@@ -1,4 +1,5 @@
using Sandbox; using Sandbox;
using Sandbox.Utility;
using System; using System;
namespace VeloX; namespace VeloX;
@@ -14,7 +15,6 @@ public partial class VeloXWheel : Component
[Property, Group( "Suspension" )] public float SpringStiffness { get; set; } = 20000.0f; [Property, Group( "Suspension" )] public float SpringStiffness { get; set; } = 20000.0f;
[Property, Group( "Suspension" )] float ReboundStiffness { get; set; } = 2200; [Property, Group( "Suspension" )] float ReboundStiffness { get; set; } = 2200;
[Property, Group( "Suspension" )] float CompressionStiffness { get; set; } = 2400; [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 TirePreset Tire { get; set; } = ResourceLibrary.Get<TirePreset>( "frictions/default.tire" );
[Property, Group( "Traction" )] public float SurfaceGrip { get; set; } = 1f; [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); } 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 ); private static Rotation CylinderOffset = Rotation.FromRoll( 90 );
[Sync] public bool IsOnGround { get; private set; } [Sync] public bool IsOnGround { get; private set; }
@@ -39,13 +39,13 @@ public partial class VeloXWheel : Component
[Property] public float BrakeTorque { get; set; } [Property] public float BrakeTorque { get; set; }
public float Compression { get; protected set; } // meters 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 Fz { get; protected set; } // N
public float AngularVelocity { get; protected set; } // rad/s 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 ContactNormal { get; protected set; }
[Sync] public Vector3 ContactPosition { get; protected set; } [Sync] public Vector3 ContactPosition { get; protected set; }
Rotation TransformRotationSteer => Vehicle.WorldTransform.RotationToWorld( Vehicle.SteerAngle * SteerMultiplier ); Rotation TransformRotationSteer => Vehicle.WorldTransform.RotationToWorld( Vehicle.SteerAngle * SteerMultiplier );
@@ -59,14 +59,9 @@ public partial class VeloXWheel : Component
Inertia = BaseInertia; Inertia = BaseInertia;
} }
internal void Update( VeloXBase vehicle, in float dt ) private void UpdateVisuals()
{ {
UpdateVisuals( vehicle, dt ); WorldRotation = Vehicle.WorldTransform.RotationToWorld( Vehicle.SteerAngle * SteerMultiplier ).RotateAroundAxis( Vector3.Right, -RollAngle );
}
private void UpdateVisuals( VeloXBase vehicle, in float dt )
{
WorldRotation = vehicle.WorldTransform.RotationToWorld( vehicle.SteerAngle * SteerMultiplier ).RotateAroundAxis( Vector3.Right, -RollAngle );
LocalPosition = StartPos + Vector3.Down * LastLength.MeterToInch(); LocalPosition = StartPos + Vector3.Down * LastLength.MeterToInch();
} }
@@ -80,6 +75,7 @@ public partial class VeloXWheel : Component
public void UpdateForce() public void UpdateForce()
{ {
Vehicle.Body.ApplyForceAt( ContactPosition, FrictionForce + ContactNormal * Fz.MeterToInch() ); Vehicle.Body.ApplyForceAt( ContactPosition, FrictionForce + ContactNormal * Fz.MeterToInch() );
FrictionForce = 0;
} }
internal void StepPhys( VeloXBase vehicle, in float dt ) internal void StepPhys( VeloXBase vehicle, in float dt )
@@ -110,7 +106,6 @@ public partial class VeloXWheel : Component
Compression = wheelTraceData.Compression / hitCount; Compression = wheelTraceData.Compression / hitCount;
ContactNormal = (wheelTraceData.ContactNormal / hitCount).Normal; ContactNormal = (wheelTraceData.ContactNormal / hitCount).Normal;
ContactPosition = wheelTraceData.ContactPosition / hitCount; ContactPosition = wheelTraceData.ContactPosition / hitCount;
//DoSuspensionSounds( vehicle, (RestLength - Compression) * 0.8f);
LastLength = RestLength - Compression; LastLength = RestLength - Compression;
UpdateHitVariables(); UpdateHitVariables();
@@ -119,28 +114,54 @@ public partial class VeloXWheel : Component
else else
{ {
IsOnGround = false; IsOnGround = false;
// Wheel is off the ground
Compression = 0f; Compression = 0f;
Fz = 0f; Fz = 0f;
ContactNormal = Vector3.Up; ContactNormal = Vector3.Up;
ContactPosition = WorldPosition; 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 ) 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 if ( trace.StartedSolid )
.FromTo( start, end ) {
.Cylinder( width, Radius.MeterToInch() ) trace = Scene.Trace
.Rotated( vehicle.WorldRotation * CylinderOffset ) .FromTo( start, end + Vehicle.WorldRotation.Down * Radius.MeterToInch() )
.UseHitPosition( false ) .UseHitPosition( false )
.IgnoreGameObjectHierarchy( Vehicle.GameObject ) .IgnoreGameObjectHierarchy( Vehicle.GameObject )
.WithoutTags( playerTag ) .WithCollisionRules( Vehicle.GameObject.Tags )
.Run(); .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 ); //DebugOverlay.Trace( trace, overlay: true );
if ( trace.Hit ) if ( trace.Hit )
@@ -160,10 +181,6 @@ public partial class VeloXWheel : Component
float FzPoint = springForce + damperForce; float FzPoint = springForce + damperForce;
// Bump stop
float minCompression = -0.05f;
if ( compression <= minCompression ) FzPoint += (minCompression - compression) * BumpStopStiffness;
wheelTraceData.ContactNormal += contactNormal; wheelTraceData.ContactNormal += contactNormal;
wheelTraceData.ContactPosition += contactPos; wheelTraceData.ContactPosition += contactPos;
wheelTraceData.Compression += compression; wheelTraceData.Compression += compression;
@@ -185,40 +202,20 @@ public partial class VeloXWheel : Component
StepRotation( Vehicle, dt ); StepRotation( Vehicle, dt );
} }
const float HubCoulombNm = 20f;
const float HubViscous = 0.1f;
private void StepRotation( VeloXBase vehicle, in float dt ) 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 += MathX.RadianToDegree( AngularVelocity ) * dt;
RollAngle = (RollAngle % 360f + 360f) % 360f; 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 Sandbox;
using System;
using System.Diagnostics;
using System.Linq; using System.Linq;
namespace VeloX; namespace VeloX;
@@ -10,13 +8,16 @@ internal sealed class WheelManager : GameObjectSystem
public WheelManager( Scene scene ) : base( scene ) public WheelManager( Scene scene ) : base( scene )
{ {
if ( Application.IsDedicatedServer )
return;
Listen( Stage.StartFixedUpdate, -99, UpdateWheels, "UpdateWheels" ); Listen( Stage.StartFixedUpdate, -99, UpdateWheels, "UpdateWheels" );
Listen( Stage.StartFixedUpdate, -100, UpdateEngine, "UpdateEngine" ); Listen( Stage.StartFixedUpdate, -100, UpdateEngine, "UpdateEngine" );
} }
private void UpdateWheels() private void UpdateWheels()
{ {
if ( !Game.IsPlaying ) if ( !Game.IsPlaying || Scene.IsEditor )
return; return;
//Stopwatch sw = Stopwatch.StartNew(); //Stopwatch sw = Stopwatch.StartNew();
@@ -24,13 +25,18 @@ internal sealed class WheelManager : GameObjectSystem
if ( !wheels.Any() ) return; if ( !wheels.Any() ) return;
var timeDelta = Time.Delta; var timeDelta = Time.Delta;
Sandbox.Utility.Parallel.ForEach( wheels, item => //Sandbox.Utility.Parallel.ForEach( wheels, item =>
{ //{
if ( !item.IsProxy ) // if ( !item.IsProxy && item.IsValid() )
// item.DoPhysics( timeDelta );
//} );
foreach ( var item in wheels )
if ( item.IsValid() && !item.IsProxy )
item.DoPhysics( timeDelta ); item.DoPhysics( timeDelta );
} );
foreach ( var wheel in wheels ) foreach ( var wheel in wheels )
wheel.UpdateForce(); if ( wheel.IsValid() && !wheel.IsProxy)
wheel.UpdateForce();
//sw.Stop(); //sw.Stop();
@@ -38,7 +44,7 @@ internal sealed class WheelManager : GameObjectSystem
} }
private void UpdateEngine() private void UpdateEngine()
{ {
if ( !Game.IsPlaying ) if ( !Game.IsPlaying || Scene.IsEditor )
return; return;
//Stopwatch sw = Stopwatch.StartNew(); //Stopwatch sw = Stopwatch.StartNew();
@@ -46,11 +52,17 @@ internal sealed class WheelManager : GameObjectSystem
if ( !engines.Any() ) return; if ( !engines.Any() ) return;
var timeDelta = Time.Delta; var timeDelta = Time.Delta;
Sandbox.Utility.Parallel.ForEach( engines, item => //Sandbox.Utility.Parallel.ForEach( engines, item =>
{ //{
if ( !item.IsProxy ) // foreach ( var wheel in engines )
item.UpdateEngine( timeDelta ); // if ( !wheel.IsProxy && wheel.IsValid() )
} ); // wheel.UpdateEngine( timeDelta );
//} );
foreach ( var wheel in engines )
if ( wheel.IsValid() )
wheel.UpdateEngine( timeDelta );
//sw.Stop(); //sw.Stop();
//DebugOverlaySystem.Current.ScreenText( new Vector2( 120, 54 ), $"Engine Sim: {sw.Elapsed.TotalMilliseconds,6:F2} ms", 24 ); //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;
using Sandbox.Audio;
using System; using System;
namespace VeloX; namespace VeloX;
@@ -48,23 +49,27 @@ public partial class VeloXCar
protected virtual void UpdateDrift( float dt ) 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() ) float mul = Math.Clamp( avgslip, 0, 2 );
_skidHandle = Sound.PlayFile( SkidSound );
if ( !_skidHandle.IsValid() )
return;
targetVolume = mul; targetVolume = mul;
targetPitch = 0.75f + 0.25f * 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.Pitch += (targetPitch - _skidHandle.Pitch) * dt * 5f;
_skidHandle.Volume += (targetVolume - _skidHandle.Volume) * dt * 10f; _skidHandle.Volume += (targetVolume - _skidHandle.Volume) * dt * 10f;
_skidHandle.Position = WorldPosition; _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 int CarDirection { get { return ForwardSpeed < 1 ? 0 : (VelocityAngle < 90 && VelocityAngle > -90 ? 1 : -1); } }
public float VelocityAngle { get; private set; } public float VelocityAngle { get; private set; }
[Sync] public float Steering { get; private set; }
private float currentSteerAngle; private float currentSteerAngle;
private float inputSteer;
private void UpdateSteering( float dt ) private void UpdateSteering( float dt )
{ {
inputSteer = Input.AnalogMove.y; float targetSteerAngle = SteeringAngle * MaxSteerAngle;
float targetSteerAngle = inputSteer * MaxSteerAngle;
if ( !Input.Down( "Jump" ) ) 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 ); VelocityAngle = -Body.Velocity.SignedAngle( WorldRotation.Forward, WorldRotation.Up );
@@ -42,10 +37,9 @@ public partial class VeloXCar
if ( TotalSpeed > 150 && CarDirection > 0 && IsOnGround ) if ( TotalSpeed > 150 && CarDirection > 0 && IsOnGround )
targetAngle = VelocityAngle * MaxSteerAngleMultiplier; 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 ); currentSteerAngle = ExpDecay( currentSteerAngle, targetSteerAngle, lerpSpeed, Time.Delta );
Steering = currentSteerAngle + targetAngle; SteerAngle = new( 0, Math.Clamp( currentSteerAngle + targetAngle, -MaxSteerAngle, MaxSteerAngle ), 0 );
SteerAngle = new( 0, Math.Clamp( Steering, -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" )] [Title( "VeloX - Car" )]
public partial class VeloXCar : VeloXBase public partial class VeloXCar : VeloXBase
{ {
protected override void OnFixedUpdate() protected override void OnFixedUpdate()
{ {
if ( IsProxy )
return;
base.OnFixedUpdate(); base.OnFixedUpdate();
if ( IsProxy )
return;
UpdateABS();
UpdateESC();
UpdateTCS();
var dt = Time.Delta; var dt = Time.Delta;
//EngineThink( dt ); //EngineThink( dt );
SimulateAerodinamics( dt ); SimulateAerodinamics( dt );
@@ -26,14 +27,24 @@ public partial class VeloXCar : VeloXBase
private void UpdateUnflip( float dt ) private void UpdateUnflip( float dt )
{ {
if ( Math.Abs( inputSteer ) < 0.1f ) if ( Math.Abs( SteeringAngle ) < 0.1f )
return; return;
if ( Math.Abs( WorldRotation.Angles().roll ) < 70 ) if ( IsOnGround || Math.Abs( WorldRotation.Roll() ) < 60 )
return; return;
var angVel = Body.AngularVelocity; var angVel = Body.WorldTransform.PointToLocal( WorldPosition + Body.AngularVelocity );
var force = inputSteer * Mass * Math.Clamp( 1 - angVel.x / 50, 0, 1 ) * 0.05f;
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; Body.AngularVelocity -= Body.WorldRotation.Forward * force * dt;
} }
} }

View File

@@ -1,43 +1,80 @@
using Sandbox; using Sandbox;
namespace VeloX; namespace VeloX;
public class InputResolver public class InputResolver
{ {
public Connection Driver { get; internal set; }
private bool IsDriverActive => Driver is not null; public Vector2 MouseDelta => Input.MouseDelta;
public Vector2 MouseWheel => Input.MouseWheel;
public Vector2 MouseDelta => IsDriverActive ? Input.MouseDelta : default; public Angles AnalogLook => Input.AnalogLook;
public Vector2 MouseWheel => IsDriverActive ? Input.MouseWheel : default; public Vector3 AnalogMove
public Angles AnalogLook => IsDriverActive ? Input.AnalogLook : default; {
public Vector3 AnalogMove => IsDriverActive ? Input.AnalogMove : default; 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 ) 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 ) public bool Pressed( string action )
{ {
return IsDriverActive && Input.Pressed( action ); return Input.Pressed( action );
} }
public bool Released( string 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 ) 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() public void StopAllHaptics()
{ {
if ( IsDriverActive ) Input.StopAllHaptics();
Input.StopAllHaptics();
} }
} }

View File

@@ -10,7 +10,7 @@ namespace VeloX;
public class EngineStreamPlayer( EngineStream stream ) : IDisposable 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 EngineStream Stream { get; set; } = stream;
public EngineState EngineState { get; set; } public EngineState EngineState { get; set; }
@@ -103,6 +103,7 @@ public class EngineStreamPlayer( EngineStream stream ) : IDisposable
channel.Position = position; channel.Position = position;
channel.ListenLocal = isLocal; channel.ListenLocal = isLocal;
channel.Paused = EngineSoundPaused || layer.IsMuted; channel.Paused = EngineSoundPaused || layer.IsMuted;
channel.FollowParent = true;
channel.TargetMixer = EngineMixer; channel.TargetMixer = EngineMixer;
} }

View File

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