first commit

This commit is contained in:
Valera
2025-06-11 20:19:35 +07:00
commit 35790cbd34
107 changed files with 3400 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
using Sandbox;
namespace VeloX;
public abstract partial class VeloXBase
{
//[Property, Feature( "Input" ), InputAction] string ThrottleInput { get; set; } = "Forward";
[Property, Feature( "Input" )] internal InputResolver Input { get; set; } = new();
[Property, Feature( "Input" )] public GameObject Driver { get => Input.Driver; set => Input.Driver = value; }
}

View File

@@ -0,0 +1,43 @@
using Sandbox;
namespace VeloX;
public abstract partial class VeloXBase
{
private Vector3 linForce;
private Vector3 angForce;
private void PhysicsSimulate()
{
var drag = AngularDrag;
var mass = Body.Mass;
var angVel = Body.AngularVelocity;
linForce.x = 0;
linForce.y = 0;
linForce.z = 0;
angForce.x = angVel.x * drag.x * mass;
angForce.y = angVel.y * drag.y * mass;
angForce.z = angVel.z * drag.z * mass;
if ( Wheels.Count > 0 )
{
Vector3 vehVel = Body.Velocity;
Vector3 vehAngVel = Body.AngularVelocity;
var dt = Time.Delta;
foreach ( var v in Wheels )
v.DoPhysics( this, ref vehVel, ref vehAngVel, ref linForce, ref angForce, in dt );
Body.Velocity = vehVel;
Body.AngularVelocity = vehAngVel;
}
Body.ApplyForce( linForce );
Body.ApplyTorque( angForce );
}
}

View File

@@ -0,0 +1,62 @@
using Sandbox;
using Sandbox.Audio;
using System;
using System.Text.Json.Serialization;
using VeloX.Audio;
using static Sandbox.Utility.Noise;
using static VeloX.EngineStream;
namespace VeloX;
public abstract partial class VeloXBase : Component, Component.ICollisionListener
{
[Property, Feature( "Effects" ), Group( "Sound" )]
public SoundEvent SuspensionHeavySound { get; protected set; } = ResourceLibrary.Get<SoundEvent>( "sounds/suspension/compress_heavy.sound" );
[Property, Feature( "Effects" ), Group( "Sound" )]
public SoundEvent SuspensionDownSound { get; protected set; } = ResourceLibrary.Get<SoundEvent>( "sounds/suspension/pneumatic_down.sound" );
[Property, Feature( "Effects" ), Group( "Sound" )]
public SoundEvent SuspensionUpSound { get; protected set; } = ResourceLibrary.Get<SoundEvent>( "sounds/suspension/pneumatic_up.sound" );
[Property, Feature( "Effects" ), Group( "Sound" )]
public SoundEvent SoftCollisionSound { get; protected set; } = ResourceLibrary.Get<SoundEvent>( "sounds/collisions/car_light.sound" );
[Property, Feature( "Effects" ), Group( "Sound" )]
public SoundEvent HardCollisionSound { get; protected set; } = ResourceLibrary.Get<SoundEvent>( "sounds/collisions/car_heavy.sound" );
[Property, Feature( "Effects" ), Group( "Sound" )]
public SoundEvent VehicleScrapeSound { get; protected set; } = ResourceLibrary.Get<SoundEvent>( "sounds/collisions/metal_scrape.sound" );
[Property, Feature( "Effects" ), Group( "Particle" )]
public GameObject MetalImpactEffect { get; protected set; } = GameObject.GetPrefab( "effects/metal_impact.prefab" );
void ICollisionListener.OnCollisionStart( Collision collision )
{
var speed = MathF.Abs( collision.Contact.NormalSpeed );
var surfaceNormal = collision.Contact.Normal;
if ( speed < 100 )
return;
var isHardHit = speed > 300;
var volume = Math.Clamp( speed / 400f, 0, 1 );
var sound = Sound.Play( SoftCollisionSound, WorldPosition );
sound.Volume = volume;
var a = MetalImpactEffect.Clone( new CloneConfig() { StartEnabled = true, Transform = new( collision.Contact.Point, surfaceNormal.Cross( surfaceNormal ).EulerAngles ) } );
a.Components.Get<ParticleConeEmitter>().Burst = speed / 2;
if ( isHardHit )
{
var hardSound = Sound.Play( HardCollisionSound, WorldPosition );
hardSound.Volume = volume;
}
else if ( surfaceNormal.Dot( -collision.Contact.Speed.Normal ) < 0.5f )
{
var scrapSound = Sound.Play( VehicleScrapeSound, WorldPosition );
scrapSound.Volume = 0.4f;
}
}
}

View File

@@ -0,0 +1,20 @@
using Sandbox;
using System.Collections.Generic;
namespace VeloX;
public abstract partial class VeloXBase
{
[Property] public List<VeloXWheel> Wheels { get; set; }
[Property] public TagSet WheelIgnoredTags { get; set; }
public List<VeloXWheel> FindWheels() => [.. Components.GetAll<VeloXWheel>()];
[Button]
public void SetupWheels()
{
Wheels = FindWheels();
}
}

38
Code/Base/VeloXBase.cs Normal file
View File

@@ -0,0 +1,38 @@
using Sandbox;
namespace VeloX;
public abstract partial class VeloXBase : Component
{
[Sync] public EngineState EngineState { get; set; }
[Sync] public WaterState WaterState { get; set; }
[Sync] public bool IsEngineOnFire { get; set; }
[Sync, Range( 0, 1 ), Property] public float Brake { get; set; }
[Sync( SyncFlags.Interpolate ), Range( 0, 1 ), Property] public float Throttle { get; set; }
[Property] public Vector3 AngularDrag { get; set; } = new( -0.1f, -0.1f, -3 );
[Property] public float Mass { get; set; } = 900;
[Property, Group( "Components" )] public Rigidbody Body { get; protected set; }
[Property, Group( "Components" )] public Collider Collider { get; protected set; }
[Sync] public Angles SteerAngle { get; set; }
public Vector3 LocalVelocity;
public float ForwardSpeed;
public float TotalSpeed;
protected override void OnFixedUpdate()
{
if ( IsProxy )
return;
LocalVelocity = WorldTransform.PointToLocal( WorldPosition + Body.Velocity );
ForwardSpeed = LocalVelocity.x;
TotalSpeed = LocalVelocity.Length;
Body.PhysicsBody.Mass = Mass;
PhysicsSimulate();
}
}

View File

@@ -0,0 +1,74 @@
using Sandbox;
namespace VeloX;
public partial class VeloXWheel : Component
{
protected override void DrawGizmos()
{
if ( !Gizmo.IsSelected )
return;
Gizmo.Draw.IgnoreDepth = true;
//
// Suspension length
//
{
var suspensionStart = Vector3.Zero;
var suspensionEnd = Vector3.Zero + Vector3.Down * SuspensionLength;
Gizmo.Draw.Color = Color.Cyan;
Gizmo.Draw.LineThickness = 0.25f;
Gizmo.Draw.Line( suspensionStart, suspensionEnd );
Gizmo.Draw.Line( suspensionStart + Vector3.Forward, suspensionStart + Vector3.Backward );
Gizmo.Draw.Line( suspensionEnd + Vector3.Forward, suspensionEnd + Vector3.Backward );
}
var widthOffset = Vector3.Right * Width * 0.5f;
//
// Wheel radius
//
{
Gizmo.Draw.LineThickness = 0.5f;
Gizmo.Draw.Color = Color.White;
Gizmo.Draw.LineCylinder( widthOffset, -widthOffset, Radius, Radius, 16 );
}
//
// Wheel width
//
{
var circlePosition = Vector3.Zero;
Gizmo.Draw.LineThickness = 0.25f;
Gizmo.Draw.Color = Color.White;
for ( float i = 0; i < 16; i++ )
{
var pos = circlePosition + Vector3.Up.RotateAround( Vector3.Zero, new Angles( i / 16 * 360, 0, 0 ) ) * Radius;
Gizmo.Draw.Line( new Line( pos - widthOffset, pos + widthOffset ) );
var pos2 = circlePosition + Vector3.Up.RotateAround( Vector3.Zero, new Angles( (i + 1) / 16 * 360, 0, 0 ) ) * Radius;
Gizmo.Draw.Line( pos - widthOffset, pos2 + widthOffset );
}
}
////
//// Forward direction
////
//{
// var arrowStart = Vector3.Forward * Radius;
// var arrowEnd = arrowStart + Vector3.Forward * 8f;
// Gizmo.Draw.Color = Color.Red;
// Gizmo.Draw.Arrow( arrowStart, arrowEnd, 4, 1 );
//}
}
}

View File

@@ -0,0 +1,18 @@
using Sandbox;
namespace VeloX;
public partial class VeloXWheel
{
[Property] float BrakePowerMax { get; set; } = 3000;
[Property, Group( "Suspension" )] float SuspensionLength { get; set; } = 10;
[Property, Group( "Suspension" )] float SpringStrength { get; set; } = 800;
[Property, Group( "Suspension" )] float SpringDamper { get; set; } = 3000;
[Property, Group( "Traction" )] float ForwardTractionMax { get; set; } = 2600;
[Property, Group( "Traction" )] float SideTractionMultiplier { get; set; } = 20;
[Property, Group( "Traction" )] float SideTractionMaxAng { get; set; } = 25;
[Property, Group( "Traction" )] float SideTractionMax { get; set; } = 2400;
[Property, Group( "Traction" )] float SideTractionMin { get; set; } = 800;
}

View File

@@ -0,0 +1,240 @@
using Sandbox;
using System;
using System.Runtime.Intrinsics.Arm;
using System.Text.RegularExpressions;
namespace VeloX;
[Group( "VeloX" )]
[Title( "VeloX - Wheel" )]
public partial class VeloXWheel : Component
{
[Property] public float Radius { get; set; } = 15;
[Property] public float Width { get; set; } = 6;
[Sync] public float SideSlip { get; private set; }
[Sync] public float ForwardSlip { get; private set; }
[Sync, Range( 0, 1 )] public float BrakePower { get; set; }
[Sync] public float Torque { get; set; }
[Sync, Range( 0, 1 )] public float Brake { get; set; }
[Property] public bool IsFront { get; protected set; }
[Property] public float SteerMultiplier { get; set; }
public float Spin { get; private set; }
public float RPM { get => angularVelocity * 60f / MathF.Tau; set => angularVelocity = value / (60 / MathF.Tau); }
internal float DistributionFactor { get; set; }
private Vector3 StartPos { get; set; }
private static Rotation CylinderOffset => Rotation.FromRoll( 90 );
public SceneTraceResult Trace { get; private set; }
public bool IsOnGround => Trace.Hit;
private float lastSpringOffset;
private Vector2 tractionCycle;
private float angularVelocity;
private float lastFraction;
private RealTimeUntil expandSoundCD;
private RealTimeUntil contractSoundCD;
protected override void OnAwake()
{
base.OnAwake();
if ( StartPos.IsNearZeroLength )
StartPos = LocalPosition;
}
private static float TractionRamp( float slipAngle, float sideTractionMaxAng, float sideTractionMax, float sideTractionMin )
{
sideTractionMaxAng /= 90; // Convert max slip angle to the 0 - 1 range
var x = (slipAngle - sideTractionMaxAng) / (1 - sideTractionMaxAng);
return slipAngle < sideTractionMaxAng ? sideTractionMax : (sideTractionMax * (1 - x)) + (sideTractionMin * x);
}
private void DoSuspensionSounds( VeloXBase vehicle, float change )
{
if ( change > 0.1f && expandSoundCD )
{
expandSoundCD = 0.3f;
var sound = Sound.Play( vehicle.SuspensionUpSound, WorldPosition );
sound.Volume = Math.Clamp( Math.Abs( change ) * 5f, 0, 0.5f );
}
if ( change < -0.1f && contractSoundCD )
{
contractSoundCD = 0.3f;
change = MathF.Abs( change );
var sound = Sound.Play( change > 0.3f ? vehicle.SuspensionHeavySound : vehicle.SuspensionDownSound, WorldPosition );
sound.Volume = Math.Clamp( change * 5f, 0, 1 );
}
}
internal void Update( VeloXBase vehicle, in float dt )
{
UpdateVisuals( vehicle, dt );
if ( !IsOnGround )
{
angularVelocity += (Torque / 20) * dt;
angularVelocity = MathX.Approach( angularVelocity, 0, dt * 4 );
}
}
private void UpdateVisuals( VeloXBase vehicle, in float dt )
{
//Rotate the wheel around the axle axis
Spin = (Spin - MathX.RadianToDegree( angularVelocity ) * dt) % 360;
WorldRotation = vehicle.WorldTransform.RotationToWorld( vehicle.SteerAngle * SteerMultiplier ).RotateAroundAxis( Vector3.Right, Spin );
}
public void DoPhysics( VeloXBase vehicle, ref Vector3 vehVel, ref Vector3 vehAngVel, ref Vector3 outLin, ref Vector3 outAng, in float dt )
{
var pos = vehicle.WorldTransform.PointToWorld( StartPos );
var ang = vehicle.WorldTransform.RotationToWorld( vehicle.SteerAngle * SteerMultiplier );
var fw = ang.Forward;
var rt = ang.Right;
var up = ang.Up;
var maxLen = SuspensionLength;
var endPos = pos + ang.Down * maxLen;
Trace = Scene.Trace.IgnoreGameObjectHierarchy( vehicle.GameObject )
.FromTo( pos, endPos )
.Cylinder( Width, Radius )
.Rotated( vehicle.WorldTransform.Rotation * CylinderOffset )
.UseRenderMeshes( false )
.UseHitPosition( false )
.WithoutTags( vehicle.WheelIgnoredTags )
.Run();
var fraction = Trace.Fraction;
var contactPos = pos - maxLen * fraction * up;
LocalPosition = vehicle.WorldTransform.PointToLocal( contactPos );
DoSuspensionSounds( vehicle, fraction - lastFraction );
lastFraction = fraction;
if ( !IsOnGround )
{
SideSlip = 0;
ForwardSlip = 0;
return;
}
var normal = Trace.Normal;
var vel = vehicle.Body.GetVelocityAtPoint( pos );
// Split that velocity among our local directions
var velF = fw.Dot( vel );
var velR = rt.Dot( vel );
var velU = normal.Dot( vel );
var absVelR = Math.Abs( velR );
//Make forward forces be perpendicular to the surface normal
fw = normal.Cross( rt );
//Suspension spring force &damping
var offset = maxLen - (fraction * maxLen);
var springForce = (offset * SpringStrength);
var damperForce = (lastSpringOffset - offset) * SpringDamper;
lastSpringOffset = offset;
var force = (springForce - damperForce) * up.Dot( normal ) * normal;
//If the suspension spring is going to be fully compressed on the next frame...
if ( velU < 0 && offset + Math.Abs( velU * dt ) > SuspensionLength )
{
var (linearVel, angularVel) = vehicle.Body.PhysicsBody.CalculateVelocityOffset( (-velU / dt) * normal, pos );
vehVel += linearVel;
vehAngVel += angularVel;
}
//Rolling resistance
force += 0.05f * -velF * fw;
//Brake and torque forces
var surfaceGrip = 1;
var maxTraction = ForwardTractionMax * surfaceGrip * 1;
//Grip loss logic
var brakeForce = MathX.Clamp( -velF, -Brake, Brake ) * BrakePowerMax * surfaceGrip;
var forwardForce = Torque + brakeForce;
var signForwardForce = forwardForce > 0 ? 1 : (forwardForce < 0 ? -1 : 0);
// Given an amount of sideways slippage( up to the max.traction )
// and the forward force, calculate how much grip we are losing.
tractionCycle.x = Math.Min( absVelR, maxTraction );
tractionCycle.y = forwardForce;
var gripLoss = Math.Max( tractionCycle.Length - maxTraction, 0 );
// Reduce the forward force by the amount of grip we lost,
// but still allow some amount of brake force to apply regardless.
forwardForce += -(gripLoss * signForwardForce) + MathX.Clamp( brakeForce * 0.5f, -maxTraction, maxTraction );
force += fw * forwardForce;
// Get how fast the wheel would be spinning if it had never lost grip
var groundAngularVelocity = MathF.Tau * (velF / (Radius * MathF.Tau));
// Add our grip loss to our spin velocity
var _angvel = groundAngularVelocity + gripLoss * (Torque > 0 ? 1 : (Torque < 0 ? -1 : 0));
// Smoothly match our current angular velocity to the angular velocity affected by grip loss
angularVelocity = MathX.Approach( angularVelocity, _angvel, dt * 200 );
ForwardSlip = groundAngularVelocity - angularVelocity;
// Calculate side slip angle
var slipAngle = MathF.Atan2( velR, MathF.Abs( velF ) ) / MathF.PI * 2;
SideSlip = slipAngle * MathX.Clamp( vehicle.TotalSpeed * 0.005f, 0, 1 ) * 2;
//Sideways traction ramp
slipAngle = MathF.Abs( slipAngle * slipAngle );
maxTraction = TractionRamp( slipAngle, SideTractionMaxAng, SideTractionMax, SideTractionMin );
var sideForce = -rt.Dot( vel * SideTractionMultiplier );
// Reduce sideways traction force as the wheel slips forward
sideForce *= 1 - Math.Clamp( MathF.Abs( gripLoss ) * 0.1f, 0, 1 ) * 0.9f;
// Apply sideways traction force
force += Math.Clamp( sideForce, -maxTraction, maxTraction ) * surfaceGrip * rt;
force += velR * SideTractionMultiplier * -0.1f * rt;
//Apply the forces at the axle / ground contact position
vehicle.Body.ApplyForceAt( pos, force / dt );
}
}