Compare commits
No commits in common. "ab8cc70785fe8881207ef689bdbf9963c0ebf7dd" and "0905876b993ab39794fd626f116df05360bd1e93" have entirely different histories.
ab8cc70785
...
0905876b99
@ -18,7 +18,7 @@
|
||||
"__type": "Sandbox.ParticleEffect",
|
||||
"__guid": "4e3d505f-af05-4c8f-85ee-c5a264ff23b6",
|
||||
"__enabled": true,
|
||||
"__version": 3,
|
||||
"__version": 1,
|
||||
"Alpha": {
|
||||
"Type": "Curve",
|
||||
"Evaluation": "Life",
|
||||
@ -46,7 +46,8 @@
|
||||
"out": 0,
|
||||
"mode": "Mirrored"
|
||||
}
|
||||
]
|
||||
],
|
||||
"Constants": "1,0,0,0"
|
||||
},
|
||||
"ApplyAlpha": false,
|
||||
"ApplyColor": true,
|
||||
@ -62,11 +63,6 @@
|
||||
"CollisionPrefabChance": 1,
|
||||
"CollisionPrefabRotation": 0,
|
||||
"CollisionRadius": 1,
|
||||
"ConstantMovement": {
|
||||
"X": 0,
|
||||
"Y": 0,
|
||||
"Z": 0
|
||||
},
|
||||
"Damping": 0,
|
||||
"DieOnCollisionChance": 0,
|
||||
"FollowerPrefab": [],
|
||||
@ -128,17 +124,29 @@
|
||||
"ConstantA": "1,1,1,1",
|
||||
"ConstantB": "1,0,0,1"
|
||||
},
|
||||
"InitialVelocity": {
|
||||
"X": 0,
|
||||
"Y": 0,
|
||||
"Z": 0
|
||||
},
|
||||
"Lifetime": {
|
||||
"Type": "Range",
|
||||
"Evaluation": "Seed",
|
||||
"Evaluation": "Particle",
|
||||
"CurveA": [
|
||||
{
|
||||
"x": 0.5,
|
||||
"y": 0.5,
|
||||
"in": 0,
|
||||
"out": 0,
|
||||
"mode": "Mirrored"
|
||||
}
|
||||
],
|
||||
"CurveB": [
|
||||
{
|
||||
"x": 0.5,
|
||||
"y": 0.5,
|
||||
"in": 0,
|
||||
"out": 0,
|
||||
"mode": "Mirrored"
|
||||
}
|
||||
],
|
||||
"Constants": "0.1,2,0,0"
|
||||
},
|
||||
"LocalSpace": 0,
|
||||
"MaxParticles": 500,
|
||||
"OnComponentDestroy": null,
|
||||
"OnComponentDisabled": null,
|
||||
@ -158,7 +166,29 @@
|
||||
"Pitch": 0,
|
||||
"PreWarm": 0,
|
||||
"PushStrength": 0,
|
||||
"Roll": 0,
|
||||
"Roll": {
|
||||
"Type": "Range",
|
||||
"Evaluation": "Particle",
|
||||
"CurveA": [
|
||||
{
|
||||
"x": 0.5,
|
||||
"y": 0.5,
|
||||
"in": 0,
|
||||
"out": 0,
|
||||
"mode": "Mirrored"
|
||||
}
|
||||
],
|
||||
"CurveB": [
|
||||
{
|
||||
"x": 0.5,
|
||||
"y": 0.5,
|
||||
"in": 0,
|
||||
"out": 0,
|
||||
"mode": "Mirrored"
|
||||
}
|
||||
],
|
||||
"Constants": "0,360,0,0"
|
||||
},
|
||||
"Scale": {
|
||||
"Type": "Curve",
|
||||
"Evaluation": "Life",
|
||||
@ -200,16 +230,36 @@
|
||||
"out": 0,
|
||||
"mode": "Mirrored"
|
||||
}
|
||||
]
|
||||
],
|
||||
"Constants": "1,0,0,0"
|
||||
},
|
||||
"SequenceId": 0,
|
||||
"SequenceSpeed": 1,
|
||||
"SequenceTime": 1,
|
||||
"SheetSequence": false,
|
||||
"Space": "World",
|
||||
"StartDelay": 0,
|
||||
"StartVelocity": {
|
||||
"Type": "Range",
|
||||
"Evaluation": "Seed",
|
||||
"Evaluation": "Particle",
|
||||
"CurveA": [
|
||||
{
|
||||
"x": 0.5,
|
||||
"y": 0.5,
|
||||
"in": 0,
|
||||
"out": 0,
|
||||
"mode": "Mirrored"
|
||||
}
|
||||
],
|
||||
"CurveB": [
|
||||
{
|
||||
"x": 0.5,
|
||||
"y": 0.5,
|
||||
"in": 0,
|
||||
"out": 0,
|
||||
"mode": "Mirrored"
|
||||
}
|
||||
],
|
||||
"Constants": "10,70,0,0"
|
||||
},
|
||||
"Stretch": 0,
|
||||
@ -217,11 +267,7 @@
|
||||
"Timing": "GameTime",
|
||||
"Tint": "1,1,1,1",
|
||||
"UsePrefabFeature": false,
|
||||
"Yaw": {
|
||||
"Type": "Range",
|
||||
"Evaluation": "Seed",
|
||||
"Constants": "0,360,0,0"
|
||||
}
|
||||
"Yaw": 0
|
||||
},
|
||||
{
|
||||
"__type": "Sandbox.ParticleSpriteRenderer",
|
||||
@ -256,8 +302,7 @@
|
||||
"Scale": 1,
|
||||
"Shadows": false,
|
||||
"SortMode": "Unsorted",
|
||||
"Texture": "textures/shapes/spark3.vtex",
|
||||
"TextureFilter": "Bilinear"
|
||||
"Texture": "materials/particles/shapes/spark2.vtex"
|
||||
},
|
||||
{
|
||||
"__type": "Sandbox.ParticleConeEmitter",
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
{
|
||||
"Pacejka": {
|
||||
"Lateral": {
|
||||
"B": 16,
|
||||
"C": 1.5100001,
|
||||
"D": 2,
|
||||
"E": -0.8
|
||||
},
|
||||
"Longitudinal": {
|
||||
"B": 16,
|
||||
"C": 1.3,
|
||||
"D": 1.2,
|
||||
"E": 0.5
|
||||
}
|
||||
},
|
||||
"RollResistanceLin": 0.001,
|
||||
"RollResistanceQuad": 1E-06,
|
||||
"__references": [],
|
||||
"__version": 0
|
||||
}
|
||||
@ -1,8 +1,20 @@
|
||||
{
|
||||
"B": 8.36,
|
||||
"C": 2.15,
|
||||
"D": 0.833,
|
||||
"E": 0.992,
|
||||
"Pacejka": {
|
||||
"Lateral": {
|
||||
"B": 12,
|
||||
"C": 1.3,
|
||||
"D": 1.8,
|
||||
"E": -1.8
|
||||
},
|
||||
"Longitudinal": {
|
||||
"B": 10.86,
|
||||
"C": 2.15,
|
||||
"D": 2,
|
||||
"E": 0.992
|
||||
}
|
||||
},
|
||||
"RollResistanceLin": 0.001,
|
||||
"RollResistanceQuad": 1E-06,
|
||||
"__references": [],
|
||||
"__version": 0
|
||||
}
|
||||
@ -1,295 +0,0 @@
|
||||
{
|
||||
"RootObject": {
|
||||
"__guid": "67db5230-4e08-4970-ab3c-df73f37dd486",
|
||||
"__version": 1,
|
||||
"Flags": 0,
|
||||
"Name": "tire_smoke",
|
||||
"Position": "0,0,0",
|
||||
"Rotation": "0,0,0,1",
|
||||
"Scale": "1,1,1",
|
||||
"Tags": "",
|
||||
"Enabled": true,
|
||||
"NetworkMode": 2,
|
||||
"NetworkInterpolation": true,
|
||||
"NetworkOrphaned": 0,
|
||||
"OwnerTransfer": 1,
|
||||
"Components": [],
|
||||
"Children": [
|
||||
{
|
||||
"__guid": "83d2bd2a-7d1d-466c-a1a7-cb4771a95da5",
|
||||
"__version": 1,
|
||||
"Flags": 0,
|
||||
"Name": "Smoke",
|
||||
"Position": "0,0,0",
|
||||
"Rotation": "0,0,0,1",
|
||||
"Scale": "1,1,1",
|
||||
"Tags": "particles",
|
||||
"Enabled": true,
|
||||
"NetworkMode": 2,
|
||||
"NetworkInterpolation": true,
|
||||
"NetworkOrphaned": 0,
|
||||
"OwnerTransfer": 1,
|
||||
"Components": [
|
||||
{
|
||||
"__type": "Sandbox.ParticleEffect",
|
||||
"__guid": "6db5eadc-45c0-482f-8da3-f29b79b84cfe",
|
||||
"__enabled": true,
|
||||
"__version": 3,
|
||||
"Alpha": {
|
||||
"Type": "Curve",
|
||||
"Evaluation": "Life",
|
||||
"CurveA": [
|
||||
{
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"in": -4.0000024,
|
||||
"out": 4.0000024,
|
||||
"mode": "Mirrored"
|
||||
},
|
||||
{
|
||||
"x": 0.20812808,
|
||||
"y": 0.475,
|
||||
"in": 0,
|
||||
"out": 0,
|
||||
"mode": "Mirrored"
|
||||
},
|
||||
{
|
||||
"x": 1,
|
||||
"y": 0,
|
||||
"in": 0,
|
||||
"out": 0,
|
||||
"mode": "Mirrored"
|
||||
}
|
||||
],
|
||||
"CurveB": [
|
||||
{
|
||||
"x": 0.5,
|
||||
"y": 0.5,
|
||||
"in": 0,
|
||||
"out": 0,
|
||||
"mode": "Mirrored"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ApplyAlpha": true,
|
||||
"ApplyColor": true,
|
||||
"ApplyRotation": true,
|
||||
"ApplyShape": true,
|
||||
"Bounce": 1,
|
||||
"Brightness": 1,
|
||||
"Bumpiness": 0,
|
||||
"Collision": false,
|
||||
"CollisionIgnore": null,
|
||||
"CollisionPrefab": null,
|
||||
"CollisionPrefabAlign": false,
|
||||
"CollisionPrefabChance": 1,
|
||||
"CollisionPrefabRotation": 0,
|
||||
"CollisionRadius": 1,
|
||||
"ConstantMovement": {
|
||||
"X": 0,
|
||||
"Y": 0,
|
||||
"Z": 0
|
||||
},
|
||||
"Damping": 5,
|
||||
"DieOnCollisionChance": 0,
|
||||
"FollowerPrefab": null,
|
||||
"FollowerPrefabChance": 1,
|
||||
"FollowerPrefabKill": true,
|
||||
"Force": true,
|
||||
"ForceDirection": "0,0,220",
|
||||
"ForceScale": 1,
|
||||
"ForceSpace": "World",
|
||||
"Friction": 1,
|
||||
"Gradient": {
|
||||
"Type": "Range",
|
||||
"Evaluation": "Life",
|
||||
"GradientA": {
|
||||
"blend": "Linear",
|
||||
"color": [
|
||||
{
|
||||
"t": 0.5,
|
||||
"c": "1,1,1,1"
|
||||
}
|
||||
],
|
||||
"alpha": []
|
||||
},
|
||||
"GradientB": {
|
||||
"blend": "Linear",
|
||||
"color": [
|
||||
{
|
||||
"t": 0.5,
|
||||
"c": "1,1,1,1"
|
||||
}
|
||||
],
|
||||
"alpha": []
|
||||
},
|
||||
"ConstantA": "0.18317,0.18317,0.18317,1",
|
||||
"ConstantB": "1,1,1,1"
|
||||
},
|
||||
"InitialVelocity": {
|
||||
"X": 0,
|
||||
"Y": 0,
|
||||
"Z": 0
|
||||
},
|
||||
"Lifetime": {
|
||||
"Type": "Range",
|
||||
"Evaluation": "Seed",
|
||||
"Constants": "2,3,0,0"
|
||||
},
|
||||
"LocalSpace": 0,
|
||||
"MaxParticles": 25,
|
||||
"OnComponentDestroy": null,
|
||||
"OnComponentDisabled": null,
|
||||
"OnComponentEnabled": null,
|
||||
"OnComponentFixedUpdate": null,
|
||||
"OnComponentStart": null,
|
||||
"OnComponentUpdate": null,
|
||||
"OnParticleCreated": null,
|
||||
"OnParticleDestroyed": null,
|
||||
"OrbitalForce": {
|
||||
"X": 0,
|
||||
"Y": 0,
|
||||
"Z": 0
|
||||
},
|
||||
"OrbitalPull": 0,
|
||||
"PerParticleTimeScale": 1,
|
||||
"Pitch": 0,
|
||||
"PreWarm": 0,
|
||||
"PushStrength": 0,
|
||||
"Roll": 0,
|
||||
"Scale": {
|
||||
"Type": "Range",
|
||||
"Evaluation": "Seed",
|
||||
"Constants": "50,60,0,0"
|
||||
},
|
||||
"SequenceId": 0,
|
||||
"SequenceSpeed": 0.5,
|
||||
"SequenceTime": 1,
|
||||
"SheetSequence": true,
|
||||
"StartDelay": 0.025,
|
||||
"StartVelocity": {
|
||||
"Type": "Range",
|
||||
"Evaluation": "Seed",
|
||||
"Constants": "10,70,0,0"
|
||||
},
|
||||
"Stretch": 0,
|
||||
"TimeScale": 1,
|
||||
"Timing": "GameTime",
|
||||
"Tint": "1,1,1,1",
|
||||
"UsePrefabFeature": false,
|
||||
"Yaw": {
|
||||
"Type": "Range",
|
||||
"Evaluation": "Seed",
|
||||
"Constants": "0,360,0,0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"__type": "Sandbox.ParticleSpriteRenderer",
|
||||
"__guid": "efbb8b5c-15b8-4e3f-ab55-9e7483b1c1f4",
|
||||
"__enabled": true,
|
||||
"__version": 2,
|
||||
"Additive": false,
|
||||
"Alignment": "LookAtCamera",
|
||||
"BlurAmount": 0.5,
|
||||
"BlurOpacity": 0.91,
|
||||
"BlurSpacing": 0.73,
|
||||
"DepthFeather": 0,
|
||||
"FaceVelocity": false,
|
||||
"FogStrength": 1,
|
||||
"LeadingTrail": true,
|
||||
"Lighting": false,
|
||||
"MotionBlur": false,
|
||||
"OnComponentDestroy": null,
|
||||
"OnComponentDisabled": null,
|
||||
"OnComponentEnabled": null,
|
||||
"OnComponentFixedUpdate": null,
|
||||
"OnComponentStart": null,
|
||||
"OnComponentUpdate": null,
|
||||
"Opaque": false,
|
||||
"RenderOptions": {
|
||||
"GameLayer": true,
|
||||
"OverlayLayer": false,
|
||||
"BloomLayer": false,
|
||||
"AfterUILayer": false
|
||||
},
|
||||
"RotationOffset": 0,
|
||||
"Scale": 1,
|
||||
"Shadows": true,
|
||||
"SortMode": "Unsorted",
|
||||
"Sprite": {
|
||||
"$compiler": "embed",
|
||||
"$source": null,
|
||||
"data": {
|
||||
"Animations": [
|
||||
{
|
||||
"Name": "Default",
|
||||
"FrameRate": 15,
|
||||
"Origin": "0.5,0.5",
|
||||
"LoopMode": "Loop",
|
||||
"Frames": [
|
||||
{
|
||||
"Texture": "textures/smoketexturesheet.vtex"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"__references": null
|
||||
},
|
||||
"compiled": null
|
||||
},
|
||||
"StartingAnimationName": "Default",
|
||||
"TextureFilter": "Bilinear"
|
||||
},
|
||||
{
|
||||
"__type": "Sandbox.ParticleSphereEmitter",
|
||||
"__guid": "4c00fdfa-a1eb-4ded-a6b2-fdf448823a6a",
|
||||
"__enabled": true,
|
||||
"Burst": 0,
|
||||
"Delay": 0,
|
||||
"DestroyOnEnd": false,
|
||||
"Duration": 5,
|
||||
"Loop": true,
|
||||
"OnComponentDestroy": null,
|
||||
"OnComponentDisabled": null,
|
||||
"OnComponentEnabled": null,
|
||||
"OnComponentFixedUpdate": null,
|
||||
"OnComponentStart": null,
|
||||
"OnComponentUpdate": null,
|
||||
"OnEdge": false,
|
||||
"Radius": 23.6,
|
||||
"Rate": 5,
|
||||
"RateOverDistance": 0,
|
||||
"Velocity": 0
|
||||
}
|
||||
],
|
||||
"Children": []
|
||||
}
|
||||
],
|
||||
"__properties": {
|
||||
"NetworkInterpolation": true,
|
||||
"TimeScale": 1,
|
||||
"WantsSystemScene": true,
|
||||
"Metadata": {},
|
||||
"NavMesh": {
|
||||
"Enabled": false,
|
||||
"IncludeStaticBodies": true,
|
||||
"IncludeKeyframedBodies": true,
|
||||
"EditorAutoUpdate": true,
|
||||
"AgentHeight": 64,
|
||||
"AgentRadius": 16,
|
||||
"AgentStepSize": 18,
|
||||
"AgentMaxSlope": 40,
|
||||
"ExcludedBodies": "",
|
||||
"IncludedBodies": ""
|
||||
}
|
||||
},
|
||||
"__variables": []
|
||||
},
|
||||
"ResourceVersion": 2,
|
||||
"ShowInMenu": false,
|
||||
"MenuPath": null,
|
||||
"MenuIcon": null,
|
||||
"DontBreakAsTemplate": false,
|
||||
"__references": [],
|
||||
"__version": 2
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 672 KiB |
@ -1,18 +0,0 @@
|
||||
{
|
||||
"Images": null,
|
||||
"Sequences": [
|
||||
{
|
||||
"Source": "textures/smoketexturesheet.png",
|
||||
"IsLooping": true,
|
||||
"FlipBook": true,
|
||||
"Columns": 6,
|
||||
"Rows": 5,
|
||||
"Frames": 30
|
||||
}
|
||||
],
|
||||
"InputColorSpace": "Linear",
|
||||
"OutputFormat": "DXT5",
|
||||
"OutputColorSpace": "Linear",
|
||||
"OutputMipAlgorithm": "None",
|
||||
"OutputTypeString": "2D"
|
||||
}
|
||||
@ -1,213 +1,49 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Sandbox;
|
||||
using Sandbox;
|
||||
using System;
|
||||
|
||||
namespace VeloX;
|
||||
namespace VeloX.Powertrain;
|
||||
|
||||
public partial class Clutch : PowertrainComponent
|
||||
public class Clutch : PowertrainComponent
|
||||
{
|
||||
protected override void OnAwake()
|
||||
{
|
||||
base.OnAwake();
|
||||
Name ??= "Clutch";
|
||||
}
|
||||
[Property] public override float Inertia { get; set; } = 0.002f;
|
||||
[Property] public float SlipTorque { get; set; } = 1000f;
|
||||
|
||||
/// <summary>
|
||||
/// RPM at which automatic clutch will try to engage.
|
||||
/// </summary>
|
||||
|
||||
[Property] public float EngagementRPM { get; set; } = 1200f;
|
||||
|
||||
public float ThrottleEngagementOffsetRPM = 400f;
|
||||
|
||||
/// <summary>
|
||||
/// Clutch engagement in range [0,1] where 1 is fully engaged clutch.
|
||||
/// Affected by Slip Torque field as the clutch can transfer [clutchEngagement * SlipTorque] Nm
|
||||
/// meaning that higher value of SlipTorque will result in more sensitive clutch.
|
||||
/// </summary>
|
||||
[Range( 0, 1 ), Property] public float ClutchInput { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Curve representing pedal travel vs. clutch engagement. Should start at 0,0 and end at 1,1.
|
||||
/// </summary>
|
||||
[Property] public Curve EngagementCurve { get; set; } = new( new List<Curve.Frame>() { new( 0, 0 ), new( 1, 1 ) } );
|
||||
|
||||
public enum ClutchControlType
|
||||
{
|
||||
Automatic,
|
||||
Manual
|
||||
}
|
||||
|
||||
[Property] public ClutchControlType СontrolType { get; set; } = ClutchControlType.Automatic;
|
||||
|
||||
/// <summary>
|
||||
/// The RPM range in which the clutch will go from disengaged to engaged and vice versa.
|
||||
/// E.g. if set to 400 and engagementRPM is 1000, 1000 will mean clutch is fully disengaged and
|
||||
/// 1400 fully engaged. Setting it too low might cause clutch to hunt/oscillate.
|
||||
/// </summary>
|
||||
[Property] public float EngagementRange { get; set; } = 400f;
|
||||
|
||||
/// <summary>
|
||||
/// Torque at which the clutch will slip / maximum torque that the clutch can transfer.
|
||||
/// This value also affects clutch engagement as higher slip value will result in clutch
|
||||
/// that grabs higher up / sooner. Too high slip torque value combined with low inertia of
|
||||
/// powertrain components might cause instability in powertrain solver.
|
||||
/// </summary>
|
||||
[Property] public float SlipTorque { get; set; } = 500f;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Amount of torque that will be passed through clutch even when completely disengaged
|
||||
/// to emulate torque converter creep on automatic transmissions.
|
||||
/// Should be higher than rolling resistance of the wheels to get the vehicle rolling.
|
||||
/// </summary>
|
||||
[Range( 0, 100f ), Property] public float CreepTorque { get; set; } = 0;
|
||||
|
||||
[Property] public float CreepSpeedLimit { get; set; } = 1f;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Clutch engagement based on ClutchInput and the clutchEngagementCurve
|
||||
/// </summary>
|
||||
[Property, ReadOnly]
|
||||
public float Engagement => _clutchEngagement;
|
||||
private float _clutchEngagement;
|
||||
|
||||
protected override void OnStart()
|
||||
{
|
||||
base.OnStart();
|
||||
SlipTorque = Controller.Engine.EstimatedPeakTorque * 1.5f;
|
||||
}
|
||||
|
||||
public override float QueryAngularVelocity( float angularVelocity, float dt )
|
||||
{
|
||||
InputAngularVelocity = angularVelocity;
|
||||
|
||||
// Return input angular velocity if InputNameHash or OutputNameHash is 0
|
||||
if ( InputNameHash == 0 || OutputNameHash == 0 )
|
||||
{
|
||||
return InputAngularVelocity;
|
||||
}
|
||||
|
||||
// Adjust clutch engagement based on conditions
|
||||
if ( СontrolType == ClutchControlType.Automatic )
|
||||
{
|
||||
Engine engine = Controller.Engine;
|
||||
// Override engagement when shifting to smoothly engage and disengage gears
|
||||
if ( Controller.Transmission.IsShifting )
|
||||
{
|
||||
float shiftProgress = Controller.Transmission.ShiftProgress;
|
||||
ClutchInput = MathF.Abs( MathF.Cos( MathF.PI * shiftProgress ) );
|
||||
}
|
||||
// Clutch engagement calculation for automatic clutch
|
||||
else
|
||||
{
|
||||
|
||||
// Calculate engagement
|
||||
// Engage the clutch if the input spinning faster than the output, but also if vice versa.
|
||||
float throttleInput = Controller.SwappedThrottle;
|
||||
float finalEngagementRPM = 500 + ThrottleEngagementOffsetRPM * (throttleInput * throttleInput);
|
||||
float referenceRPM = MathF.Max( InputRPM, OutputRPM );
|
||||
|
||||
ClutchInput = (referenceRPM - finalEngagementRPM) / EngagementRange;
|
||||
ClutchInput = Math.Clamp( ClutchInput, 0f, 1f );
|
||||
// Avoid disconnecting clutch at high speed
|
||||
if ( engine.OutputRPM > engine.IdleRPM * 1.1f && Controller.TotalSpeed > 3f )
|
||||
{
|
||||
ClutchInput = 1f;
|
||||
}
|
||||
|
||||
if ( Controller.SwappedBrakes > 0 )
|
||||
{
|
||||
ClutchInput = 0;
|
||||
}
|
||||
}
|
||||
if ( Controller.IsClutching > 0 )
|
||||
{
|
||||
ClutchInput = 1 - Controller.IsClutching;
|
||||
}
|
||||
|
||||
}
|
||||
else if ( СontrolType == ClutchControlType.Manual )
|
||||
{
|
||||
// Manual clutch engagement through user input
|
||||
ClutchInput = Controller.IsClutching;
|
||||
}
|
||||
|
||||
OutputAngularVelocity = InputAngularVelocity * _clutchEngagement;
|
||||
float Wout = Output.QueryAngularVelocity( OutputAngularVelocity, dt ) * _clutchEngagement;
|
||||
float Win = angularVelocity * (1f - _clutchEngagement);
|
||||
return Wout + Win;
|
||||
}
|
||||
public float Pressing { get; set; } = 1; // todo
|
||||
|
||||
public override float QueryInertia()
|
||||
{
|
||||
if ( OutputNameHash == 0 )
|
||||
{
|
||||
if ( !HasOutput )
|
||||
return Inertia;
|
||||
}
|
||||
|
||||
float I = Inertia + Output.QueryInertia() * _clutchEngagement;
|
||||
return I;
|
||||
return Inertia + Output.QueryInertia() * Pressing;
|
||||
}
|
||||
|
||||
public override float QueryAngularVelocity( float angularVelocity )
|
||||
{
|
||||
this.angularVelocity = angularVelocity;
|
||||
|
||||
if ( !HasOutput )
|
||||
return angularVelocity;
|
||||
|
||||
|
||||
float outputW = Output.QueryAngularVelocity( angularVelocity ) * Pressing;
|
||||
float inputW = angularVelocity * (1 - Pressing);
|
||||
|
||||
return outputW + inputW;
|
||||
}
|
||||
|
||||
|
||||
public override float ForwardStep( float torque, float inertiaSum, float dt )
|
||||
public override float ForwardStep( float torque, float inertia )
|
||||
{
|
||||
|
||||
InputTorque = torque;
|
||||
InputInertia = inertiaSum;
|
||||
|
||||
if ( OutputNameHash == 0 )
|
||||
if ( !HasOutput )
|
||||
return torque;
|
||||
|
||||
Torque = Math.Clamp( torque, -SlipTorque, SlipTorque );
|
||||
|
||||
// Get the clutch engagement point from the input value
|
||||
// Do not use the clutchEnagement directly for any calculations!
|
||||
_clutchEngagement = EngagementCurve.Evaluate( ClutchInput );
|
||||
_clutchEngagement = Math.Clamp( _clutchEngagement, 0, 1 );
|
||||
// Calculate output inertia and torque based on the clutch engagement
|
||||
Torque = torque * (1 - (1 - MathF.Pow( Pressing, 0.3f )));
|
||||
|
||||
// Assume half of the inertia is on the input plate and the other half is on the output clutch plate.
|
||||
float halfClutchInertia = Inertia * 0.5f;
|
||||
OutputInertia = (inertiaSum + halfClutchInertia) * _clutchEngagement + halfClutchInertia;
|
||||
float returnTorque = Output.ForwardStep( Torque, inertia * Pressing + Inertia ) * Pressing;
|
||||
|
||||
// Allow the torque output to be only up to the slip torque valu
|
||||
float outputTorqueClamp = SlipTorque * _clutchEngagement;
|
||||
|
||||
OutputTorque = InputTorque;
|
||||
OutputTorque = Math.Clamp( OutputTorque, 0, outputTorqueClamp );
|
||||
float slipOverflowTorque = -Math.Min( outputTorqueClamp - OutputTorque, 0 );
|
||||
|
||||
// Apply the creep torque commonly caused by torque converter drag in automatic transmissions
|
||||
//ApplyCreepTorque( ref OutputTorque, CreepTorque );
|
||||
|
||||
// Send the torque downstream
|
||||
float returnTorque = _output.ForwardStep( OutputTorque, OutputInertia, dt ) * _clutchEngagement;
|
||||
|
||||
|
||||
// Clamp the return torque to the slip torque of the clutch once again
|
||||
//returnTorque = Math.Clamp( returnTorque, -SlipTorque, SlipTorque );
|
||||
|
||||
// Torque returned to the input is a combination of torque returned by the powertrain and the torque that
|
||||
// was possibly never sent downstream
|
||||
return returnTorque + slipOverflowTorque;
|
||||
return Math.Clamp( returnTorque, -SlipTorque, SlipTorque );
|
||||
}
|
||||
|
||||
|
||||
private void ApplyCreepTorque( ref float torque, float creepTorque )
|
||||
{
|
||||
// Apply creep torque to forward torque
|
||||
if ( creepTorque != 0 && Controller.Engine.IsRunning && Controller.TotalSpeed < CreepSpeedLimit )
|
||||
{
|
||||
bool torqueWithinCreepRange = torque < creepTorque && torque > -creepTorque;
|
||||
|
||||
if ( torqueWithinCreepRange )
|
||||
{
|
||||
torque = creepTorque;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,234 +0,0 @@
|
||||
using Sandbox;
|
||||
using System;
|
||||
|
||||
namespace VeloX;
|
||||
|
||||
|
||||
public partial class Differential : PowertrainComponent
|
||||
{
|
||||
|
||||
|
||||
/// <param name="T">Input torque</param>
|
||||
/// <param name="Wa">Angular velocity of the outputA</param>
|
||||
/// <param name="Wb">Angular velocity of the outputB</param>
|
||||
/// <param name="Ia">Inertia of the outputA</param>
|
||||
/// <param name="Ib">Inertia of the outputB</param>
|
||||
/// <param name="dt">Time step</param>
|
||||
/// <param name="biasAB">Torque bias between outputA and outputB. 0 = all torque goes to A, 1 = all torque goes to B</param>
|
||||
/// <param name="stiffness">Stiffness of the limited slip or locked differential 0-1</param>
|
||||
/// <param name="powerRamp">Stiffness under power</param>
|
||||
/// <param name="coastRamp">Stiffness under braking</param>
|
||||
/// <param name="slipTorque">Slip torque of the limited slip differential</param>
|
||||
/// <param name="Ta">Torque output towards outputA</param>
|
||||
/// <param name="Tb">Torque output towards outputB</param>
|
||||
public delegate void SplitTorque( float T, float Wa, float Wb, float Ia, float Ib, float dt, float biasAB,
|
||||
float stiffness, float powerRamp, float coastRamp, float slipTorque, out float Ta, out float Tb );
|
||||
|
||||
protected override void OnAwake()
|
||||
{
|
||||
base.OnAwake();
|
||||
Name ??= "Differential";
|
||||
AssignDifferentialDelegate();
|
||||
}
|
||||
public enum DifferentialType
|
||||
{
|
||||
Open,
|
||||
Locked,
|
||||
LimitedSlip,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Differential type.
|
||||
/// </summary>
|
||||
[Property]
|
||||
public DifferentialType Type
|
||||
{
|
||||
get => _differentialType;
|
||||
set
|
||||
{
|
||||
_differentialType = value;
|
||||
AssignDifferentialDelegate();
|
||||
}
|
||||
}
|
||||
private DifferentialType _differentialType;
|
||||
|
||||
/// <summary>
|
||||
/// Torque bias between left (A) and right (B) output in [0,1] range.
|
||||
/// </summary>
|
||||
[Property, Range( 0, 1 )] public float BiasAB { get; set; } = 0.5f;
|
||||
|
||||
/// <summary>
|
||||
/// Stiffness of locking differential [0,1]. Higher value
|
||||
/// will result in lower difference in rotational velocity between left and right wheel.
|
||||
/// Too high value might introduce slight oscillation due to drivetrain windup and a vehicle that is hard to steer.
|
||||
/// </summary>
|
||||
[Property, Range( 0, 1 ), HideIf( nameof( _differentialType ), DifferentialType.Open )] public float Stiffness { get; set; } = 0.5f;
|
||||
|
||||
/// <summary>
|
||||
/// Stiffness of the LSD differential under acceleration.
|
||||
/// </summary>
|
||||
[Property, Range( 0, 1 ), ShowIf( nameof( _differentialType ), DifferentialType.LimitedSlip )] public float PowerRamp { get; set; } = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// Stiffness of the LSD differential under braking.
|
||||
/// </summary>
|
||||
[Property, Range( 0, 1 ), ShowIf( nameof( _differentialType ), DifferentialType.LimitedSlip )] public float CoastRamp { get; set; } = 0.5f;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Second output of differential.
|
||||
/// </summary>
|
||||
[Property]
|
||||
public PowertrainComponent OutputB
|
||||
{
|
||||
get { return _outputB; }
|
||||
set
|
||||
{
|
||||
if ( value == this )
|
||||
{
|
||||
Log.Warning( $"{Name}: PowertrainComponent Output can not be self." );
|
||||
OutputBNameHash = 0;
|
||||
_output = null;
|
||||
return;
|
||||
}
|
||||
if ( _outputB != null )
|
||||
{
|
||||
_outputB.InputNameHash = 0;
|
||||
_outputB.Input = null;
|
||||
}
|
||||
|
||||
_outputB = value;
|
||||
|
||||
if ( _outputB != null )
|
||||
{
|
||||
_outputB.Input = this;
|
||||
OutputBNameHash = _outputB.ToString().GetHashCode();
|
||||
}
|
||||
else
|
||||
{
|
||||
OutputBNameHash = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected PowertrainComponent _outputB;
|
||||
public int OutputBNameHash;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Slip torque of limited slip differentials.
|
||||
/// </summary>
|
||||
[Property, ShowIf( nameof( _differentialType ), DifferentialType.LimitedSlip )] public float SlipTorque { get; set; } = 400f;
|
||||
|
||||
/// <summary>
|
||||
/// Function delegate that will be used to split the torque between output(A) and outputB.
|
||||
/// </summary>
|
||||
public SplitTorque SplitTorqueDelegate;
|
||||
|
||||
private void AssignDifferentialDelegate()
|
||||
{
|
||||
SplitTorqueDelegate = _differentialType switch
|
||||
{
|
||||
DifferentialType.Open => OpenDiffTorqueSplit,
|
||||
DifferentialType.Locked => LockingDiffTorqueSplit,
|
||||
DifferentialType.LimitedSlip => LimitedDiffTorqueSplit,
|
||||
_ => OpenDiffTorqueSplit,
|
||||
};
|
||||
}
|
||||
|
||||
public static void OpenDiffTorqueSplit( float T, float Wa, float Wb, float Ia, float Ib, float dt, float biasAB,
|
||||
float stiffness, float powerRamp, float coastRamp, float slipTorque, out float Ta, out float Tb )
|
||||
{
|
||||
Ta = T * (1f - biasAB);
|
||||
Tb = T * biasAB;
|
||||
}
|
||||
|
||||
|
||||
public static void LockingDiffTorqueSplit( float T, float Wa, float Wb, float Ia, float Ib, float dt, float biasAB,
|
||||
float stiffness, float powerRamp, float coastRamp, float slipTorque, out float Ta, out float Tb )
|
||||
{
|
||||
Ta = T * (1f - biasAB);
|
||||
Tb = T * biasAB;
|
||||
|
||||
float syncTorque = (Wa - Wb) * stiffness * (Ia + Ib) * 0.5f / dt;
|
||||
|
||||
Ta -= syncTorque;
|
||||
Tb += syncTorque;
|
||||
}
|
||||
|
||||
|
||||
public static void LimitedDiffTorqueSplit( float T, float Wa, float Wb, float Ia, float Ib, float dt, float biasAB,
|
||||
float stiffness, float powerRamp, float coastRamp, float slipTorque, out float Ta, out float Tb )
|
||||
{
|
||||
if ( Wa < 0 || Wb < 0 )
|
||||
{
|
||||
Ta = T * (1f - biasAB);
|
||||
Tb = T * biasAB;
|
||||
return;
|
||||
}
|
||||
|
||||
// Минимальный момент трения, даже если разницы скоростей нет
|
||||
float preloadTorque = MathF.Abs( T ) * 0.5f;
|
||||
float speedDiff = Wa - Wb;
|
||||
float ramp = T > 0 ? powerRamp : coastRamp;
|
||||
|
||||
// Основной момент трения LSD (зависит от разницы скоростей и preload)
|
||||
float frictionTorque = ramp * (slipTorque * MathF.Abs( speedDiff ) + preloadTorque);
|
||||
frictionTorque = MathF.Min( frictionTorque, MathF.Abs( T ) * 0.5f );
|
||||
|
||||
Ta = T * (1f - biasAB) - MathF.Sign( speedDiff ) * frictionTorque;
|
||||
Tb = T * biasAB + MathF.Sign( speedDiff ) * frictionTorque;
|
||||
}
|
||||
public override float QueryAngularVelocity( float angularVelocity, float dt )
|
||||
{
|
||||
InputAngularVelocity = angularVelocity;
|
||||
|
||||
if ( OutputNameHash == 0 || OutputBNameHash == 0 )
|
||||
return angularVelocity;
|
||||
|
||||
OutputAngularVelocity = InputAngularVelocity;
|
||||
float Wa = _output.QueryAngularVelocity( OutputAngularVelocity, dt );
|
||||
float Wb = _outputB.QueryAngularVelocity( OutputAngularVelocity, dt );
|
||||
return (Wa + Wb) * 0.5f;
|
||||
}
|
||||
|
||||
public override float QueryInertia()
|
||||
{
|
||||
if ( OutputNameHash == 0 || OutputBNameHash == 0 )
|
||||
return Inertia;
|
||||
|
||||
float Ia = _output.QueryInertia();
|
||||
float Ib = _outputB.QueryInertia();
|
||||
float I = Inertia + (Ia + Ib);
|
||||
|
||||
return I;
|
||||
}
|
||||
|
||||
|
||||
public override float ForwardStep( float torque, float inertiaSum, float dt )
|
||||
{
|
||||
InputTorque = torque;
|
||||
InputInertia = inertiaSum;
|
||||
|
||||
if ( OutputNameHash == 0 || OutputBNameHash == 0 )
|
||||
return torque;
|
||||
|
||||
float Wa = _output.QueryAngularVelocity( OutputAngularVelocity, dt );
|
||||
float Wb = _outputB.QueryAngularVelocity( OutputAngularVelocity, dt );
|
||||
|
||||
float Ia = _output.QueryInertia();
|
||||
float Ib = _outputB.QueryInertia();
|
||||
SplitTorqueDelegate.Invoke( torque, Wa, Wb, Ia, Ib, dt, BiasAB, Stiffness, PowerRamp,
|
||||
CoastRamp, SlipTorque, out float Ta, out float Tb );
|
||||
|
||||
float outAInertia = inertiaSum * 0.5f + Ia;
|
||||
float outBInertia = inertiaSum * 0.5f + Ib;
|
||||
|
||||
OutputTorque = Ta + Tb;
|
||||
OutputInertia = outAInertia + outBInertia;
|
||||
|
||||
return _output.ForwardStep( Ta, outAInertia, dt ) + _outputB.ForwardStep( Tb, outBInertia, dt );
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
79
Code/Base/Powertrain/Differential/BaseDifferential.cs
Normal file
79
Code/Base/Powertrain/Differential/BaseDifferential.cs
Normal file
@ -0,0 +1,79 @@
|
||||
using Sandbox;
|
||||
using System;
|
||||
|
||||
namespace VeloX.Powertrain;
|
||||
|
||||
[Category( "VeloX/Powertrain/Differential" )]
|
||||
public abstract class BaseDifferential : PowertrainComponent
|
||||
{
|
||||
[Property] public float FinalDrive { get; set; } = 3.392f;
|
||||
[Property] public override float Inertia { get; set; } = 0.01f;
|
||||
//[Property] public float CoastRamp { get; set; } = 1f;
|
||||
//[Property] public float PowerRamp { get; set; } = 1f;
|
||||
//[Property] public float Stiffness { get; set; } = 0.1f;
|
||||
//[Property] public float SlipTorque { get; set; } = 0f;
|
||||
//[Property] public float SteerLock { get; set; } = 45f;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The PowertrainComponent this component will output to.
|
||||
/// </summary>
|
||||
[Property]
|
||||
public PowertrainComponent OutputB
|
||||
{
|
||||
get => _outputb;
|
||||
set
|
||||
{
|
||||
if ( value == this )
|
||||
{
|
||||
_outputb = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_outputb = value;
|
||||
if ( _outputb != null )
|
||||
_outputb.Input = this;
|
||||
}
|
||||
|
||||
}
|
||||
private PowertrainComponent _outputb;
|
||||
|
||||
public override bool HasOutput => Output.IsValid() && OutputB.IsValid();
|
||||
|
||||
public override float QueryAngularVelocity( float angularVelocity )
|
||||
{
|
||||
this.angularVelocity = angularVelocity;
|
||||
|
||||
if ( !HasOutput )
|
||||
return angularVelocity;
|
||||
|
||||
float aW = Output.QueryAngularVelocity( angularVelocity );
|
||||
float bW = OutputB.QueryAngularVelocity( angularVelocity );
|
||||
|
||||
return (aW + bW) * FinalDrive * 0.5f;
|
||||
}
|
||||
|
||||
public abstract void SplitTorque( float aW, float bW, float aI, float bI, out float tqA, out float tqB );
|
||||
|
||||
public override float ForwardStep( float torque, float inertia )
|
||||
{
|
||||
if ( !HasOutput )
|
||||
return torque;
|
||||
|
||||
float aW = Output.QueryAngularVelocity( angularVelocity );
|
||||
float bW = OutputB.QueryAngularVelocity( angularVelocity );
|
||||
|
||||
float aI = Output.QueryInertia();
|
||||
float bI = OutputB.QueryInertia();
|
||||
|
||||
Torque = torque * FinalDrive;
|
||||
|
||||
SplitTorque( aW, bW, aI, bI, out float tqA, out float tqB );
|
||||
|
||||
tqA = Output.ForwardStep( tqA, inertia * 0.5f * MathF.Pow( FinalDrive, 2 ) + aI );
|
||||
tqB = OutputB.ForwardStep( tqB, inertia * 0.5f * MathF.Pow( FinalDrive, 2 ) + bI );
|
||||
|
||||
return tqA + tqB;
|
||||
}
|
||||
|
||||
}
|
||||
13
Code/Base/Powertrain/Differential/OpenDifferential.cs
Normal file
13
Code/Base/Powertrain/Differential/OpenDifferential.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using Sandbox;
|
||||
|
||||
namespace VeloX.Powertrain;
|
||||
|
||||
public class OpenDifferential : BaseDifferential
|
||||
{
|
||||
[Property] public float BiasAB { get; set; } = 0.5f;
|
||||
public override void SplitTorque( float aW, float bW, float aI, float bI, out float tqA, out float tqB )
|
||||
{
|
||||
tqA = Torque * (1 - BiasAB);
|
||||
tqB = Torque * BiasAB;
|
||||
}
|
||||
}
|
||||
@ -1,663 +1,109 @@
|
||||
using Sandbox;
|
||||
using Sandbox.Audio;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using VeloX.Utils;
|
||||
using static Sandbox.VertexLayout;
|
||||
using static VeloX.EngineStream;
|
||||
|
||||
namespace VeloX;
|
||||
namespace VeloX.Powertrain;
|
||||
|
||||
public class Engine : PowertrainComponent, IScenePhysicsEvents
|
||||
public class Engine : PowertrainComponent
|
||||
{
|
||||
protected override void OnAwake()
|
||||
{
|
||||
base.OnAwake();
|
||||
Name ??= "Engine";
|
||||
UpdatePeakPowerAndTorque();
|
||||
[Property, Group( "Settings" )] public float IdleRPM { get; set; } = 900f;
|
||||
[Property, Group( "Settings" )] public float MaxRPM { get; set; } = 7000f;
|
||||
[Property, Group( "Settings" )] public override float Inertia { get; set; } = 0.151f;
|
||||
[Property, Group( "Settings" )] public float LimiterDuration { get; set; } = 0.05f;
|
||||
[Property, Group( "Settings" )] public Curve TorqueMap { get; set; }
|
||||
[Property, Group( "Settings" )] public EngineStream Stream { get; set; }
|
||||
|
||||
}
|
||||
[Sync] public float Throttle { get; internal set; }
|
||||
|
||||
[Hide] public new bool Input { get; set; }
|
||||
[Property] public bool IsRedlining => !limiterTimer;
|
||||
[Property] public float RPMPercent => Math.Clamp( (RPM - IdleRPM) / (MaxRPM - IdleRPM), 0, 1 );
|
||||
|
||||
public delegate float CalculateTorque( float angularVelocity, float dt );
|
||||
/// <summary>
|
||||
/// Delegate for a function that modifies engine power.
|
||||
/// </summary>
|
||||
public delegate float PowerModifier();
|
||||
|
||||
public enum EngineType
|
||||
{
|
||||
ICE,
|
||||
Electric,
|
||||
}
|
||||
/// <summary>
|
||||
/// If true starter will be ran for [starterRunTime] seconds if engine receives any throttle input.
|
||||
/// </summary>
|
||||
[Property] public bool AutoStartOnThrottle { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Assign your own delegate to use different type of torque calculation.
|
||||
/// </summary>
|
||||
public CalculateTorque CalculateTorqueDelegate;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Engine type. ICE (Internal Combustion Engine) supports features such as starter, stalling, etc.
|
||||
/// Electric engine (motor) can run in reverse, can not be stalled and does not use starter.
|
||||
/// </summary>
|
||||
[Property] public EngineType Type { get; set; } = EngineType.ICE;
|
||||
|
||||
/// <summary>
|
||||
/// Power generated by the engine in kW
|
||||
/// </summary>
|
||||
public float generatedPower;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// RPM at which idler circuit will try to keep RPMs when there is no input.
|
||||
/// </summary>
|
||||
[Property] public float IdleRPM { get; set; } = 900;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum engine power in [kW].
|
||||
/// </summary>
|
||||
[Property, Group( "Power" )] public float MaxPower { get; set; } = 120;
|
||||
|
||||
/// <summary>
|
||||
/// Loss power (pumping, friction losses) is calculated as the percentage of maxPower.
|
||||
/// Should be between 0 and 1 (100%).
|
||||
/// </summary>
|
||||
[Range( 0, 1 ), Property] public float EngineLossPercent { get; set; } = 0.8f;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// If true the engine will be started immediately, without running the starter, when the vehicle is enabled.
|
||||
/// Sets engine angular velocity to idle angular velocity.
|
||||
/// </summary>
|
||||
[Property] public bool FlyingStartEnabled { get; set; }
|
||||
|
||||
[Property] public bool Ignition { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Power curve with RPM range [0,1] on the X axis and power coefficient [0,1] on Y axis.
|
||||
/// Both values are represented as percentages and should be in 0 to 1 range.
|
||||
/// Power coefficient is multiplied by maxPower to get the final power at given RPM.
|
||||
/// </summary>
|
||||
[Property, Group( "Power" )] public Curve PowerCurve { get; set; } = new( new List<Curve.Frame>() { new( 0, 0.4f ), new( 0.16f, 0.65f ), new( 0.32f, 0.85f ), new( 0.64f, 1f ), new( 0.8f, 0.9f ), new( 0.9f, 0.6f ), new( 1f, 0.2f ) } );
|
||||
|
||||
/// <summary>
|
||||
/// Is the engine currently hitting the rev limiter?
|
||||
/// </summary>
|
||||
public bool RevLimiterActive;
|
||||
|
||||
/// <summary>
|
||||
/// If engine RPM rises above revLimiterRPM, how long should fuel cutoff last?
|
||||
/// Higher values make hitting rev limiter more rough and choppy.
|
||||
/// </summary>
|
||||
[Property] public float RevLimiterCutoffDuration { get; set; } = 0.12f;
|
||||
|
||||
/// <summary>
|
||||
/// Engine RPM at which rev limiter activates.
|
||||
/// </summary>
|
||||
[Property] public float RevLimiterRPM { get; set; } = 6700;
|
||||
|
||||
/// <summary>
|
||||
/// Is the starter currently active?
|
||||
/// </summary>
|
||||
[Property, ReadOnly, Group( "Info" )] public bool StarterActive = false;
|
||||
|
||||
/// <summary>
|
||||
/// Torque starter motor can put out. Make sure that this torque is more than loss torque
|
||||
/// at the starter RPM limit. If too low the engine will fail to start.
|
||||
/// </summary>
|
||||
[Property] public float StartDuration = 0.5f;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Peak power as calculated from the power curve. If the power curve peaks at Y=1 peak power will equal max power field value.
|
||||
/// After changing power, power curve or RPM range call UpdatePeakPowerAndTorque() to get update the value.
|
||||
/// </summary>
|
||||
[Property, ReadOnly, Group( "Info" )]
|
||||
public float EstimatedPeakPower => _peakPower;
|
||||
private float _peakPower;
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// RPM at which the peak power is achieved.
|
||||
/// After changing power, power curve or RPM range call UpdatePeakPowerAndTorque() to get update the value.
|
||||
/// </summary>
|
||||
[Property, ReadOnly, Group( "Info" )]
|
||||
public float EstimatedPeakPowerRPM => _peakPowerRpm;
|
||||
|
||||
private float _peakPowerRpm;
|
||||
|
||||
/// <summary>
|
||||
/// Peak torque value as calculated from the power curve.
|
||||
/// After changing power, power curve or RPM range call UpdatePeakPowerAndTorque() to get update the value.
|
||||
/// </summary>
|
||||
[Property, ReadOnly, Group( "Info" )]
|
||||
public float EstimatedPeakTorque => _peakTorque;
|
||||
|
||||
private float _peakTorque;
|
||||
|
||||
/// <summary>
|
||||
/// RPM at which the engine achieves the peak torque, calculated from the power curve.
|
||||
/// After changing power, power curve or RPM range call UpdatePeakPowerAndTorque() to get update the value.
|
||||
/// </summary>
|
||||
[Property, ReadOnly, Group( "Info" )]
|
||||
public float EstimatedPeakTorqueRPM => _peakTorqueRpm;
|
||||
|
||||
private float _peakTorqueRpm;
|
||||
|
||||
/// <summary>
|
||||
/// RPM as a percentage of maximum RPM.
|
||||
/// </summary>
|
||||
[Property, ReadOnly, Group( "Info" )]
|
||||
public float RPMPercent => _rpmPercent;
|
||||
|
||||
[Sync] private float _rpmPercent { get; set; }
|
||||
/// <summary>
|
||||
/// Engine throttle position. 0 for no throttle and 1 for full throttle.
|
||||
/// </summary>
|
||||
[Property, ReadOnly, Group( "Info" )]
|
||||
public float ThrottlePosition { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is the engine currently running?
|
||||
/// Requires ignition to be enabled.
|
||||
/// </summary>
|
||||
[Property, ReadOnly, Group( "Info" )]
|
||||
public bool IsRunning { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is the engine currently running?
|
||||
/// Requires ignition to be enabled.
|
||||
/// </summary>
|
||||
[Property, ReadOnly, Group( "Info" )]
|
||||
public bool IsActive { get; private set; }
|
||||
|
||||
private float _idleAngularVelocity;
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Current load of the engine, based on the power produced.
|
||||
/// </summary>
|
||||
public float Load => _load;
|
||||
public event Action OnEngineStart;
|
||||
public event Action OnEngineStop;
|
||||
public event Action OnRevLimiter;
|
||||
|
||||
private float _load;
|
||||
private float masterThrottle;
|
||||
private TimeUntil limiterTimer;
|
||||
private float finalTorque;
|
||||
|
||||
private EngineStreamPlayer StreamPlayer;
|
||||
public float[] friction = [15.438f, 2.387f, 0.7958f];
|
||||
|
||||
protected override void OnStart()
|
||||
{
|
||||
base.OnStart();
|
||||
|
||||
if ( Type == EngineType.ICE )
|
||||
StreamPlayer = new( Stream );
|
||||
}
|
||||
|
||||
public float GetFrictionTorque( float throttle, float rpm )
|
||||
{
|
||||
float s = rpm < 0 ? -1f : 1f;
|
||||
float r = s * rpm * 0.001f;
|
||||
float f = friction[0] + friction[1] * r + friction[2] * r * r;
|
||||
return -s * f * (1 - throttle);
|
||||
}
|
||||
private float GenerateTorque()
|
||||
{
|
||||
float throttle = Throttle;
|
||||
float rpm = RPM;
|
||||
float friction = GetFrictionTorque( throttle, rpm );
|
||||
|
||||
float maxInitialTorque = TorqueMap.Evaluate( RPMPercent ) - friction;
|
||||
float idleFadeStart = Math.Clamp( MathX.Remap( rpm, IdleRPM - 300, IdleRPM, 1, 0 ), 0, 1 );
|
||||
float idleFadeEnd = Math.Clamp( MathX.Remap( rpm, IdleRPM, IdleRPM + 600, 1, 0 ), 0, 1 );
|
||||
float additionalEnergySupply = idleFadeEnd * (-friction / maxInitialTorque) + idleFadeStart;
|
||||
|
||||
|
||||
if ( rpm > MaxRPM )
|
||||
{
|
||||
CalculateTorqueDelegate = CalculateTorqueICE;
|
||||
throttle = 0;
|
||||
limiterTimer = LimiterDuration;
|
||||
}
|
||||
else if ( Type == EngineType.Electric )
|
||||
else if ( !limiterTimer )
|
||||
throttle = 0;
|
||||
|
||||
masterThrottle = Math.Clamp( additionalEnergySupply + throttle, 0, 1 );
|
||||
|
||||
float realInitialTorque = maxInitialTorque * masterThrottle;
|
||||
Torque = realInitialTorque + friction;
|
||||
|
||||
return Torque;
|
||||
}
|
||||
|
||||
public override float ForwardStep( float _, float __ )
|
||||
{
|
||||
if ( !HasOutput )
|
||||
{
|
||||
IdleRPM = 0f;
|
||||
FlyingStartEnabled = true;
|
||||
CalculateTorqueDelegate = CalculateTorqueElectric;
|
||||
StarterActive = false;
|
||||
StartDuration = 0.001f;
|
||||
RevLimiterCutoffDuration = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
public void StartEngine()
|
||||
{
|
||||
if ( IsRunning ) return;
|
||||
|
||||
Ignition = true;
|
||||
OnEngineStart?.Invoke();
|
||||
|
||||
if ( Type != EngineType.Electric )
|
||||
{
|
||||
if ( FlyingStartEnabled )
|
||||
{
|
||||
FlyingStart();
|
||||
}
|
||||
else if ( !StarterActive && Controller != null )
|
||||
{
|
||||
StarterCoroutine();
|
||||
}
|
||||
}
|
||||
}
|
||||
private async void StarterCoroutine()
|
||||
{
|
||||
if ( Type == EngineType.Electric || StarterActive )
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
float startTimer = 0f;
|
||||
StarterActive = true;
|
||||
|
||||
// Ensure safe start duration
|
||||
StartDuration = Math.Max( 0.1f, StartDuration );
|
||||
|
||||
_starterTorque = ((_idleAngularVelocity - OutputAngularVelocity) * Inertia) / StartDuration;
|
||||
|
||||
while ( startTimer <= StartDuration && StarterActive )
|
||||
{
|
||||
startTimer += 0.1f;
|
||||
await Task.DelaySeconds( 0.1f );
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_starterTorque = 0;
|
||||
StarterActive = false;
|
||||
IsActive = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void FlyingStart()
|
||||
{
|
||||
Ignition = true;
|
||||
StarterActive = false;
|
||||
OutputAngularVelocity = IdleRPM.RPMToAngularVelocity();
|
||||
}
|
||||
|
||||
public void StopEngine()
|
||||
{
|
||||
Ignition = false;
|
||||
IsActive = true;
|
||||
OnEngineStop?.Invoke();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Toggles engine state.
|
||||
/// </summary>
|
||||
public void StartStopEngine()
|
||||
{
|
||||
if ( IsRunning )
|
||||
StopEngine();
|
||||
else
|
||||
StartEngine();
|
||||
}
|
||||
|
||||
public void UpdatePeakPowerAndTorque()
|
||||
{
|
||||
GetPeakPower( out _peakPower, out _peakPowerRpm );
|
||||
GetPeakTorque( out _peakTorque, out _peakTorqueRpm );
|
||||
}
|
||||
|
||||
public void UpdateEngine( in float dt )
|
||||
{
|
||||
StreamEngineUpdate( dt, WorldPosition );
|
||||
if ( IsProxy )
|
||||
return;
|
||||
|
||||
// Cache values
|
||||
_userThrottleInput = _userThrottleInput.LerpTo( Controller.SwappedThrottle, 0.05f );
|
||||
|
||||
ThrottlePosition = _userThrottleInput;
|
||||
_idleAngularVelocity = IdleRPM.RPMToAngularVelocity();
|
||||
_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;
|
||||
if ( wasRunning && !IsRunning )
|
||||
StopEngine();
|
||||
|
||||
// Physics update
|
||||
if ( OutputNameHash == 0 )
|
||||
return;
|
||||
|
||||
float drivetrainInertia = _output.QueryInertia();
|
||||
float inertiaSum = Inertia + drivetrainInertia;
|
||||
if ( inertiaSum == 0 )
|
||||
return;
|
||||
|
||||
float drivetrainAngularVelocity = QueryAngularVelocity( OutputAngularVelocity, dt );
|
||||
float targetAngularVelocity = Inertia / inertiaSum * OutputAngularVelocity + drivetrainInertia / inertiaSum * drivetrainAngularVelocity;
|
||||
|
||||
// Calculate generated torque and power
|
||||
float generatedTorque = CalculateTorqueICE( OutputAngularVelocity, dt );
|
||||
generatedPower = TorqueToPowerInKW( in OutputAngularVelocity, in generatedTorque );
|
||||
|
||||
// Calculate reaction torque
|
||||
float reactionTorque = (targetAngularVelocity - OutputAngularVelocity) * Inertia / dt;
|
||||
|
||||
// Calculate/get torque returned from wheels
|
||||
|
||||
OutputTorque = generatedTorque - reactionTorque;
|
||||
float returnTorque = ForwardStep( OutputTorque, 0, dt );
|
||||
|
||||
float totalTorque = generatedTorque + returnTorque + reactionTorque;
|
||||
OutputAngularVelocity += totalTorque / inertiaSum * dt;
|
||||
|
||||
// Clamp the angular velocity to prevent any powertrain instabilities over the limits
|
||||
OutputAngularVelocity = Math.Clamp( OutputAngularVelocity, 0, _revLimiterAngularVelocity * 1.05f );
|
||||
|
||||
// Calculate cached values
|
||||
_rpmPercent = Math.Clamp( OutputAngularVelocity / _revLimiterAngularVelocity, 0, 1 );
|
||||
_load = Math.Clamp( generatedPower / MaxPower, 0, 1 );
|
||||
|
||||
|
||||
}
|
||||
|
||||
private float _starterTorque;
|
||||
private float _revLimiterAngularVelocity;
|
||||
private float _userThrottleInput;
|
||||
|
||||
private async void RevLimiter()
|
||||
{
|
||||
if ( RevLimiterActive || Type == EngineType.Electric || RevLimiterCutoffDuration == 0 )
|
||||
return;
|
||||
|
||||
RevLimiterActive = true;
|
||||
OnRevLimiter?.Invoke();
|
||||
await Task.DelayRealtimeSeconds( RevLimiterCutoffDuration );
|
||||
RevLimiterActive = false;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Calculates torque for electric engine type.
|
||||
/// </summary>
|
||||
public float CalculateTorqueElectric( float angularVelocity, float dt )
|
||||
{
|
||||
float absAngVel = Math.Abs( angularVelocity );
|
||||
|
||||
// Avoid angular velocity spikes while shifting
|
||||
if ( Controller.Transmission.IsShifting )
|
||||
ThrottlePosition = 0;
|
||||
|
||||
float maxLossPower = MaxPower * 0.3f;
|
||||
float lossPower = maxLossPower * (1f - ThrottlePosition) * RPMPercent;
|
||||
float genPower = MaxPower * ThrottlePosition;
|
||||
float totalPower = genPower - lossPower;
|
||||
totalPower = MathX.Lerp( totalPower * 0.1f, totalPower, RPMPercent * 10f );
|
||||
float clampedAngVel = absAngVel < 10f ? 10f : absAngVel;
|
||||
return PowerInKWToTorque( clampedAngVel, totalPower );
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Calculates torque for ICE (Internal Combustion Engine).
|
||||
/// </summary>
|
||||
public float CalculateTorqueICE( float angularVelocity, float dt )
|
||||
{
|
||||
// Set the throttle to 0 when shifting, but avoid doing so around idle RPM to prevent stalls.
|
||||
if ( Controller.Transmission.IsShifting && angularVelocity > _idleAngularVelocity )
|
||||
ThrottlePosition = 0f;
|
||||
|
||||
// Set throttle to 0 when starter active.
|
||||
if ( StarterActive )
|
||||
ThrottlePosition = 0f;
|
||||
// Apply idle throttle correction to keep the engine running
|
||||
else
|
||||
ApplyICEIdleCorrection();
|
||||
|
||||
// Trigger rev limiter if needed
|
||||
if ( angularVelocity >= _revLimiterAngularVelocity && !RevLimiterActive )
|
||||
RevLimiter();
|
||||
|
||||
// Calculate torque
|
||||
float generatedTorque;
|
||||
|
||||
// Do not generate any torque while starter is active to prevent RPM spike during startup
|
||||
// or while stalled to prevent accidental starts.
|
||||
if ( StarterActive )
|
||||
generatedTorque = 0f;
|
||||
else
|
||||
generatedTorque = CalculateICEGeneratedTorqueFromPowerCurve();
|
||||
|
||||
float lossTorque = (StarterActive || ThrottlePosition > 0.2f) ? 0f : CalculateICELossTorqueFromPowerCurve();
|
||||
|
||||
// Reduce the loss torque at rev limiter, but allow it to be >0 to prevent vehicle getting
|
||||
// stuck at rev limiter.
|
||||
if ( RevLimiterActive )
|
||||
lossTorque *= 0.25f;
|
||||
generatedTorque += _starterTorque + lossTorque;
|
||||
return generatedTorque;
|
||||
}
|
||||
|
||||
private float CalculateICELossTorqueFromPowerCurve()
|
||||
{
|
||||
// Avoid issues with large torque spike around 0 angular velocity.
|
||||
if ( OutputAngularVelocity < 10f )
|
||||
return -OutputAngularVelocity * MaxPower * 0.03f;
|
||||
|
||||
float angVelPercent = OutputAngularVelocity < 10f ? 0.1f : Math.Clamp( OutputAngularVelocity, 0, _revLimiterAngularVelocity ) / _revLimiterAngularVelocity;
|
||||
|
||||
float lossPower = angVelPercent * 3f * -MaxPower * Math.Clamp( _userThrottleInput + 0.5f, 0, 1 ) * EngineLossPercent;
|
||||
|
||||
return PowerInKWToTorque( OutputAngularVelocity, lossPower );
|
||||
}
|
||||
|
||||
private void ApplyICEIdleCorrection()
|
||||
{
|
||||
if ( Ignition && OutputAngularVelocity < _idleAngularVelocity * 1.1f )
|
||||
{
|
||||
// Apply a small correction to account for the error since the throttle is applied only
|
||||
// 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 );
|
||||
ThrottlePosition = Math.Max( _userThrottleInput, idleThrottlePosition );
|
||||
}
|
||||
}
|
||||
|
||||
private float CalculateICEGeneratedTorqueFromPowerCurve()
|
||||
{
|
||||
generatedPower = 0;
|
||||
float torque = 0;
|
||||
|
||||
if ( !Ignition && !StarterActive )
|
||||
angularVelocity += GenerateTorque() / Inertia * Time.Delta;
|
||||
angularVelocity = Math.Max( angularVelocity, 0 );
|
||||
return 0;
|
||||
if ( RevLimiterActive )
|
||||
ThrottlePosition = 0.2f;
|
||||
else
|
||||
{
|
||||
// Add maximum losses to the maximum power when calculating the generated power since the maxPower is net value (after losses).
|
||||
generatedPower = PowerCurve.Evaluate( _rpmPercent ) * (MaxPower * (1f + EngineLossPercent)) * ThrottlePosition;
|
||||
torque = PowerInKWToTorque( OutputAngularVelocity, generatedPower );
|
||||
}
|
||||
return torque;
|
||||
float outputInertia = Output.QueryInertia();
|
||||
float inertiaSum = Inertia + outputInertia;
|
||||
float outputW = Output.QueryAngularVelocity( angularVelocity );
|
||||
|
||||
float targetW = Inertia / inertiaSum * angularVelocity + outputInertia / inertiaSum * outputW;
|
||||
float generatedTorque = GenerateTorque();
|
||||
float reactTorque = (targetW - angularVelocity) * Inertia / Time.Delta;
|
||||
float returnedTorque = Output.ForwardStep( generatedTorque - reactTorque, Inertia );
|
||||
|
||||
finalTorque = generatedTorque + reactTorque + returnedTorque;
|
||||
angularVelocity += finalTorque / inertiaSum * Time.Delta;
|
||||
angularVelocity = Math.Max( angularVelocity, 0 );
|
||||
|
||||
UpdateStream();
|
||||
|
||||
return finalTorque;
|
||||
}
|
||||
|
||||
|
||||
public void GetPeakTorque( out float peakTorque, out float peakTorqueRpm )
|
||||
private void UpdateStream()
|
||||
{
|
||||
peakTorque = 0;
|
||||
peakTorqueRpm = 0;
|
||||
|
||||
for ( float i = 0.05f; i < 1f; i += 0.05f )
|
||||
{
|
||||
float rpm = i * RevLimiterRPM;
|
||||
float P = PowerCurve.Evaluate( i ) * MaxPower;
|
||||
if ( rpm < IdleRPM )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float W = rpm.RPMToAngularVelocity();
|
||||
float T = (P * 1000f) / W;
|
||||
|
||||
if ( T > peakTorque )
|
||||
{
|
||||
peakTorque = T;
|
||||
peakTorqueRpm = rpm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void GetPeakPower( out float peakPower, out float peakPowerRpm )
|
||||
{
|
||||
float maxY = 0f;
|
||||
float maxX = 1f;
|
||||
for ( float i = 0f; i < 1f; i += 0.05f )
|
||||
{
|
||||
float y = PowerCurve.Evaluate( i );
|
||||
if ( y > maxY )
|
||||
{
|
||||
maxY = y;
|
||||
maxX = i;
|
||||
}
|
||||
}
|
||||
|
||||
peakPower = maxY * MaxPower;
|
||||
peakPowerRpm = maxX * RevLimiterRPM;
|
||||
}
|
||||
|
||||
|
||||
|
||||
[Property, Group( "Parsing" )] string Clipboard { get; set; }
|
||||
[Button, Group( "Parsing" )]
|
||||
public void FromLUT()
|
||||
{
|
||||
if ( Clipboard == null )
|
||||
if ( StreamPlayer is null )
|
||||
return;
|
||||
|
||||
using var undo = Scene.Editor?.UndoScope( "From LUT" ).WithComponentChanges( this ).Push();
|
||||
StreamPlayer.Throttle = Throttle;
|
||||
StreamPlayer.RPMPercent = RPMPercent;
|
||||
StreamPlayer.EngineState = EngineState.Running;
|
||||
StreamPlayer.IsRedlining = IsRedlining;
|
||||
|
||||
var data = new List<(float RPM, float PowerHP)>();
|
||||
StreamPlayer.Update( Time.Delta, WorldPosition );
|
||||
|
||||
var lines = Clipboard.Split( ['\n', '\r'], StringSplitOptions.RemoveEmptyEntries );
|
||||
|
||||
foreach ( var line in lines )
|
||||
{
|
||||
var parts = line.Split( '|' );
|
||||
if ( parts.Length == 2 &&
|
||||
float.TryParse( parts[0], out float rpm ) &&
|
||||
float.TryParse( parts[1], out float powerHP ) )
|
||||
{
|
||||
data.Add( (rpm, powerHP) );
|
||||
}
|
||||
}
|
||||
|
||||
RevLimiterRPM = data.Max( x => x.RPM );
|
||||
MaxPower = data.Max( x => x.PowerHP ) * 0.746f;
|
||||
|
||||
var frames = data.Select( d =>
|
||||
new Curve.Frame(
|
||||
d.RPM / RevLimiterRPM,
|
||||
(d.PowerHP * 0.746f) / MaxPower
|
||||
)
|
||||
).ToList();
|
||||
PowerCurve = new( frames );
|
||||
Clipboard = null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
[Property] public EngineStream Stream { get; set; }
|
||||
|
||||
|
||||
public bool EngineSoundPaused => !IsRunning;
|
||||
private float _wobbleTime;
|
||||
|
||||
private static readonly Mixer EngineMixer = Mixer.FindMixerByName( "Car Engine" );
|
||||
private readonly Dictionary<Layer, SoundHandle> EngineSounds = [];
|
||||
|
||||
protected override void OnDestroy()
|
||||
{
|
||||
base.OnDestroy();
|
||||
|
||||
foreach ( var item in EngineSounds.Values )
|
||||
item.Dispose();
|
||||
}
|
||||
public void StreamEngineUpdate( float deltaTime, Vector3 position, bool isLocal = false )
|
||||
{
|
||||
var globalPitch = 1.0f;
|
||||
|
||||
// Gear wobble effect
|
||||
if ( _wobbleTime > 0 )
|
||||
{
|
||||
_wobbleTime -= deltaTime * (0.1f + ThrottlePosition);
|
||||
globalPitch += MathF.Cos( _wobbleTime * Stream.Parameters.WobbleFrequency ) * _wobbleTime * (1 - _wobbleTime) * Stream.Parameters.WobbleStrength;
|
||||
}
|
||||
|
||||
globalPitch *= Stream.Parameters.Pitch;
|
||||
|
||||
// Redline effect
|
||||
var redlineVolume = 1.0f;
|
||||
if ( RPMPercent >= 0.95f )
|
||||
{
|
||||
redlineVolume = 1 - Stream.Parameters.RedlineStrength +
|
||||
MathF.Cos( Time.Now * Stream.Parameters.RedlineFrequency ) *
|
||||
Stream.Parameters.RedlineStrength;
|
||||
}
|
||||
// Process layers
|
||||
foreach ( var (id, layer) in Stream.Layers )
|
||||
{
|
||||
EngineSounds.TryGetValue( layer, out var channel );
|
||||
|
||||
if ( !channel.IsValid() )
|
||||
{
|
||||
channel = Sound.PlayFile( layer.AudioPath );
|
||||
EngineSounds[layer] = channel;
|
||||
}
|
||||
|
||||
if ( channel.Paused && EngineSoundPaused )
|
||||
continue;
|
||||
|
||||
// Reset controller outputs
|
||||
float layerVolume = 1.0f;
|
||||
float layerPitch = 1.0f;
|
||||
|
||||
// Apply all controllers
|
||||
foreach ( var controller in layer.Controllers )
|
||||
{
|
||||
var inputValue = controller.InputParameter switch
|
||||
{
|
||||
Audio.Controller.InputTypes.Throttle => _userThrottleInput,
|
||||
Audio.Controller.InputTypes.RpmFraction => RPMPercent,
|
||||
_ => 0.0f
|
||||
};
|
||||
|
||||
var normalized = Math.Clamp( inputValue, controller.InputRange.Min, controller.InputRange.Max );
|
||||
var outputValue = controller.InputRange.Remap(
|
||||
normalized,
|
||||
controller.OutputRange.Min,
|
||||
controller.OutputRange.Max
|
||||
);
|
||||
|
||||
// Apply to correct parameter
|
||||
switch ( controller.OutputParameter )
|
||||
{
|
||||
case Audio.Controller.OutputTypes.Volume:
|
||||
layerVolume *= outputValue;
|
||||
break;
|
||||
case Audio.Controller.OutputTypes.Pitch:
|
||||
layerPitch *= outputValue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply redline effect if needed
|
||||
layerVolume *= layer.UseRedline ? redlineVolume : 1.0f;
|
||||
layerPitch *= globalPitch;
|
||||
// Update audio channel
|
||||
channel.Pitch = layerPitch;
|
||||
channel.Volume = layerVolume * Stream.Parameters.Volume;
|
||||
channel.Position = position;
|
||||
channel.ListenLocal = isLocal;
|
||||
channel.Paused = EngineSoundPaused;
|
||||
channel.TargetMixer = EngineMixer;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
45
Code/Base/Powertrain/Gearbox/BaseGearbox.cs
Normal file
45
Code/Base/Powertrain/Gearbox/BaseGearbox.cs
Normal file
@ -0,0 +1,45 @@
|
||||
using Sandbox;
|
||||
using System;
|
||||
|
||||
namespace VeloX.Powertrain;
|
||||
|
||||
[Category( "VeloX/Powertrain/Gearbox" )]
|
||||
public abstract class BaseGearbox : PowertrainComponent
|
||||
{
|
||||
[Property] public override float Inertia { get; set; } = 0.01f;
|
||||
|
||||
protected float ratio;
|
||||
|
||||
public override float QueryInertia()
|
||||
{
|
||||
if ( !HasOutput || ratio == 0 )
|
||||
return Inertia;
|
||||
|
||||
return Inertia + Output.QueryInertia() / MathF.Pow( ratio, 2 );
|
||||
}
|
||||
|
||||
public override float QueryAngularVelocity( float angularVelocity )
|
||||
{
|
||||
this.angularVelocity = angularVelocity;
|
||||
if ( !HasOutput || ratio == 0 )
|
||||
return angularVelocity;
|
||||
|
||||
return Output.QueryAngularVelocity( angularVelocity ) * ratio;
|
||||
}
|
||||
|
||||
public override float ForwardStep( float torque, float inertia )
|
||||
{
|
||||
Torque = torque * ratio;
|
||||
|
||||
if ( !HasOutput )
|
||||
return torque;
|
||||
|
||||
if ( ratio == 0 )
|
||||
{
|
||||
Output.ForwardStep( 0, Inertia * 0.5f );
|
||||
return torque;
|
||||
}
|
||||
|
||||
return Output.ForwardStep( Torque, (inertia + Inertia) * MathF.Pow( ratio, 2 ) ) / ratio;
|
||||
}
|
||||
}
|
||||
52
Code/Base/Powertrain/Gearbox/ManualGearbox.cs
Normal file
52
Code/Base/Powertrain/Gearbox/ManualGearbox.cs
Normal file
@ -0,0 +1,52 @@
|
||||
using Sandbox;
|
||||
|
||||
namespace VeloX.Powertrain;
|
||||
|
||||
public class ManualGearbox : BaseGearbox
|
||||
{
|
||||
[Property] public float[] Ratios { get; set; } = [3.626f, 2.200f, 1.541f, 1.213f, 1.000f, 0.767f];
|
||||
[Property] public float Reverse { get; set; } = 3.4f;
|
||||
[Property, InputAction] public string ForwardAction { get; set; } = "Attack1";
|
||||
[Property, InputAction] public string BackwardAction { get; set; } = "Attack2";
|
||||
|
||||
private int gear;
|
||||
|
||||
protected void SetGear( int gear )
|
||||
{
|
||||
if ( gear < -1 || gear >= Ratios.Length )
|
||||
return;
|
||||
this.gear = gear;
|
||||
|
||||
RecalcRatio();
|
||||
}
|
||||
|
||||
private void RecalcRatio()
|
||||
{
|
||||
if ( gear == -1 )
|
||||
ratio = -Reverse;
|
||||
else if ( gear == 0 )
|
||||
ratio = 0;
|
||||
else
|
||||
ratio = Ratios[gear - 1];
|
||||
}
|
||||
|
||||
public void Shift( int dir )
|
||||
{
|
||||
SetGear( gear + dir );
|
||||
}
|
||||
|
||||
private void InputResolve()
|
||||
{
|
||||
if ( Sandbox.Input.Pressed( ForwardAction ) )
|
||||
Shift( 1 );
|
||||
else if ( Sandbox.Input.Pressed( BackwardAction ) )
|
||||
Shift( -1 );
|
||||
}
|
||||
|
||||
public override float ForwardStep( float torque, float inertia )
|
||||
{
|
||||
InputResolve();
|
||||
|
||||
return base.ForwardStep( torque, inertia );
|
||||
}
|
||||
}
|
||||
34
Code/Base/Powertrain/PowerWheel.cs
Normal file
34
Code/Base/Powertrain/PowerWheel.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using Sandbox;
|
||||
using System;
|
||||
|
||||
namespace VeloX.Powertrain;
|
||||
|
||||
public class PowerWheel : PowertrainComponent
|
||||
{
|
||||
[Property] public VeloXWheel Wheel { get; set; }
|
||||
|
||||
public override float QueryInertia()
|
||||
{
|
||||
float dtScale = Math.Clamp( Time.Delta, 0.01f, 0.05f ) / 0.005f;
|
||||
return Wheel.BaseInertia * dtScale;
|
||||
}
|
||||
|
||||
public override float QueryAngularVelocity( float angularVelocity )
|
||||
{
|
||||
return Wheel.AngularVelocity;
|
||||
}
|
||||
|
||||
public override float ForwardStep( float torque, float inertia )
|
||||
{
|
||||
Wheel.AutoPhysics = false;
|
||||
Wheel.Torque = torque;
|
||||
Wheel.Brake = Vehicle.Brake;
|
||||
Inertia = Wheel.BaseInertia + inertia;
|
||||
Wheel.Inertia = inertia;
|
||||
Wheel.DoPhysics( Vehicle );
|
||||
|
||||
angularVelocity = Wheel.AngularVelocity;
|
||||
|
||||
return Wheel.CounterTorque;
|
||||
}
|
||||
}
|
||||
@ -1,31 +1,26 @@
|
||||
using System;
|
||||
using Sandbox;
|
||||
using Sandbox;
|
||||
using System;
|
||||
|
||||
namespace VeloX;
|
||||
namespace VeloX.Powertrain;
|
||||
|
||||
public abstract class PowertrainComponent : Component
|
||||
{
|
||||
|
||||
[Property] public VeloXBase Controller;
|
||||
/// <summary>
|
||||
/// Name of the component. Only unique names should be used on the same vehicle.
|
||||
/// </summary>
|
||||
[Property] public string Name { get; set; }
|
||||
protected override void OnAwake()
|
||||
{
|
||||
Vehicle ??= Components.Get<VeloXBase>( FindMode.EverythingInSelfAndAncestors );
|
||||
}
|
||||
|
||||
public const float RAD_TO_RPM = 60f / MathF.Tau;
|
||||
public const float RPM_TO_RAD = 1 / (60 / MathF.Tau);
|
||||
public const float UNITS_PER_METER = 39.37f;
|
||||
public const float UNITS_TO_METERS = 0.01905f;
|
||||
public const float KG_TO_N = 9.80665f;
|
||||
public const float KG_TO_KN = 0.00980665f;
|
||||
|
||||
/// <summary>
|
||||
/// Angular inertia of the component. Higher inertia value will result in a powertrain that is slower to spin up, but
|
||||
/// also slower to spin down. Too high values will result in (apparent) sluggish response while too low values will
|
||||
/// result in vehicle being easy to stall and possible powertrain instability / glitches.
|
||||
/// </summary>
|
||||
[Property, Range( 0.0002f, 2f )] public float Inertia { get; set; } = 0.05f;
|
||||
[Property] public VeloXBase Vehicle { get; set; }
|
||||
[Property] public virtual float Inertia { get; set; } = 0.02f;
|
||||
|
||||
[Property, ReadOnly] public float InputTorque;
|
||||
[Property, ReadOnly] public float OutputTorque;
|
||||
public float InputAngularVelocity;
|
||||
public float OutputAngularVelocity;
|
||||
public float InputInertia;
|
||||
public float OutputInertia;
|
||||
|
||||
/// <summary>
|
||||
/// Input component. Set automatically.
|
||||
@ -39,143 +34,67 @@ public abstract class PowertrainComponent : Component
|
||||
if ( value == null || value == this )
|
||||
{
|
||||
_input = null;
|
||||
InputNameHash = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
_input = value;
|
||||
InputNameHash = _input.GetHashCode();
|
||||
}
|
||||
}
|
||||
|
||||
protected PowertrainComponent _input;
|
||||
private PowertrainComponent _input;
|
||||
public int InputNameHash;
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The PowertrainComponent this component will output to.
|
||||
/// </summary>
|
||||
[Property]
|
||||
public PowertrainComponent Output
|
||||
{
|
||||
get { return _output; }
|
||||
get => _output;
|
||||
set
|
||||
{
|
||||
if ( value == this )
|
||||
{
|
||||
Log.Warning( $"{Name}: PowertrainComponent Output can not be self." );
|
||||
OutputNameHash = 0;
|
||||
_output = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_output = value;
|
||||
if ( _output != null )
|
||||
{
|
||||
_output.Input = this;
|
||||
OutputNameHash = _output.GetHashCode();
|
||||
}
|
||||
else
|
||||
{
|
||||
OutputNameHash = 0;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
protected PowertrainComponent _output;
|
||||
public int OutputNameHash { get; private set; }
|
||||
private PowertrainComponent _output;
|
||||
|
||||
public float RPM => angularVelocity * RAD_TO_RPM;
|
||||
|
||||
/// <summary>
|
||||
/// Input shaft RPM of component.
|
||||
/// </summary>
|
||||
[Property, ReadOnly]
|
||||
public float InputRPM => AngularVelocityToRPM( InputAngularVelocity );
|
||||
protected float angularVelocity;
|
||||
protected float Torque;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Output shaft RPM of component.
|
||||
/// </summary>
|
||||
[Property, ReadOnly]
|
||||
public float OutputRPM => AngularVelocityToRPM( OutputAngularVelocity );
|
||||
|
||||
|
||||
public virtual float QueryAngularVelocity( float angularVelocity, float dt )
|
||||
{
|
||||
InputAngularVelocity = angularVelocity;
|
||||
|
||||
if ( OutputNameHash == 0 )
|
||||
{
|
||||
return angularVelocity;
|
||||
}
|
||||
|
||||
OutputAngularVelocity = angularVelocity;
|
||||
return _output.QueryAngularVelocity( OutputAngularVelocity, dt );
|
||||
}
|
||||
public virtual bool HasOutput => Output.IsValid();
|
||||
|
||||
public virtual float QueryInertia()
|
||||
{
|
||||
if ( OutputNameHash == 0 )
|
||||
if ( !HasOutput )
|
||||
return Inertia;
|
||||
|
||||
float Ii = Inertia;
|
||||
float Ia = _output.QueryInertia();
|
||||
|
||||
return Ii + Ia;
|
||||
return Inertia + Output.QueryInertia();
|
||||
}
|
||||
public virtual float ForwardStep( float torque, float inertiaSum, float dt )
|
||||
|
||||
public virtual float QueryAngularVelocity( float angularVelocity )
|
||||
{
|
||||
InputTorque = torque;
|
||||
InputInertia = inertiaSum;
|
||||
if ( !HasOutput )
|
||||
return angularVelocity;
|
||||
|
||||
if ( OutputNameHash == 0 )
|
||||
return torque;
|
||||
|
||||
OutputTorque = InputTorque;
|
||||
OutputInertia = inertiaSum + Inertia;
|
||||
return _output.ForwardStep( OutputTorque, OutputInertia, dt );
|
||||
return Output.QueryAngularVelocity( angularVelocity );
|
||||
}
|
||||
|
||||
public static float TorqueToPowerInKW( in float angularVelocity, in float torque )
|
||||
public virtual float ForwardStep( float torque, float inertia )
|
||||
{
|
||||
// Power (W) = Torque (Nm) * Angular Velocity (rad/s)
|
||||
float powerInWatts = torque * angularVelocity;
|
||||
if ( !HasOutput )
|
||||
return Torque;
|
||||
|
||||
// Convert power from watts to kilowatts
|
||||
float powerInKW = powerInWatts / 1000f;
|
||||
|
||||
return powerInKW;
|
||||
return Output.ForwardStep( Torque, inertia + Inertia );
|
||||
}
|
||||
|
||||
public static float PowerInKWToTorque( in float angularVelocity, in float powerInKW )
|
||||
{
|
||||
// Convert power from kilowatts to watts
|
||||
float powerInWatts = powerInKW * 1000f;
|
||||
|
||||
// Torque (Nm) = Power (W) / Angular Velocity (rad/s)
|
||||
float absAngVel = Math.Abs( angularVelocity );
|
||||
float clampedAngularVelocity = (absAngVel > -1f && absAngVel < 1f) ? 1f : angularVelocity;
|
||||
float torque = powerInWatts / clampedAngularVelocity;
|
||||
return torque;
|
||||
}
|
||||
|
||||
public float CalculateOutputPowerInKW()
|
||||
{
|
||||
return GetPowerInKW( OutputTorque, OutputAngularVelocity );
|
||||
}
|
||||
public static float GetPowerInKW( in float torque, in float angularVelocity )
|
||||
{
|
||||
// Power (W) = Torque (Nm) * Angular Velocity (rad/s)
|
||||
float powerInWatts = torque * angularVelocity;
|
||||
|
||||
// Convert power from watts to kilowatts
|
||||
float powerInKW = powerInWatts / 1000f;
|
||||
|
||||
return powerInKW;
|
||||
}
|
||||
|
||||
public static float AngularVelocityToRPM( float angularVelocity ) => angularVelocity * 9.5492965855137f;
|
||||
|
||||
public static float RPMToAngularVelocity( float RPM ) => RPM * 0.10471975511966f;
|
||||
}
|
||||
|
||||
@ -1,800 +0,0 @@
|
||||
using Sandbox;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
|
||||
namespace VeloX;
|
||||
|
||||
public class Transmission : PowertrainComponent
|
||||
{
|
||||
protected override void OnAwake()
|
||||
{
|
||||
base.OnAwake();
|
||||
Name ??= "Transmission";
|
||||
LoadGearsFromGearingProfile();
|
||||
}
|
||||
/// <summary>
|
||||
/// A class representing a single ground surface type.
|
||||
/// </summary>
|
||||
public partial class TransmissionGearingProfile
|
||||
{
|
||||
/// <summary>
|
||||
/// List of forward gear ratios starting from 1st forward gear.
|
||||
/// </summary>
|
||||
public List<float> ForwardGears { get; set; } = [3.59f, 2.02f, 1.38f, 1f, 0.87f];
|
||||
|
||||
/// <summary>
|
||||
/// List of reverse gear ratios starting from 1st reverse gear.
|
||||
/// </summary>
|
||||
public List<float> ReverseGears { get; set; } = [-4f,];
|
||||
}
|
||||
|
||||
public const float INPUT_DEADZONE = 0.05f;
|
||||
public float ReferenceShiftRPM => _referenceShiftRPM;
|
||||
|
||||
private float _referenceShiftRPM;
|
||||
|
||||
/// <summary>
|
||||
/// If true the gear input has to be held for the transmission to stay in gear, otherwise it goes to neutral.
|
||||
/// Used for hardware H-shifters.
|
||||
/// </summary>
|
||||
[Property] public bool HoldToKeepInGear { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Final gear multiplier. Each gear gets multiplied by this value.
|
||||
/// Equivalent to axle/differential ratio in real life.
|
||||
/// </summary>
|
||||
[Property] public float FinalGearRatio { get; set; } = 4.3f;
|
||||
/// <summary>
|
||||
/// [Obsolete, will be removed]
|
||||
/// Currently active gearing profile.
|
||||
/// Final gear ratio will be determined from this and final gear ratio.
|
||||
/// </summary>
|
||||
|
||||
[Property] public TransmissionGearingProfile GearingProfile { get; set; } = new();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// A list of gears ratios in order of negative, neutral and then positive.
|
||||
/// E.g. -4, 0, 6, 4, 3, 2 => one reverse, 4 forward gears.
|
||||
/// </summary>
|
||||
[Property, ReadOnly, Group( "Info" )] public List<float> Gears = new();
|
||||
|
||||
/// <summary>
|
||||
/// Number of forward gears.
|
||||
/// </summary>
|
||||
public int ForwardGearCount;
|
||||
|
||||
/// <summary>
|
||||
/// Number of reverse gears.
|
||||
/// </summary>
|
||||
public int ReverseGearCount;
|
||||
|
||||
/// <summary>
|
||||
/// How much inclines affect shift point position. Higher value will push the shift up and shift down RPM up depending
|
||||
/// on the current incline to prevent vehicle from upshifting at the wrong time.
|
||||
/// </summary>
|
||||
[Property, Range( 0, 4 )] public float InclineEffectCoeff { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Function that handles gear shifts.
|
||||
/// Use External transmission type and assign this delegate manually to use a custom
|
||||
/// gear shift function.
|
||||
/// </summary>
|
||||
public delegate void Shift( VeloXBase vc );
|
||||
|
||||
/// <summary>
|
||||
/// Function that changes the gears as required.
|
||||
/// Use transmissionType External and assign this delegate to use your own gear shift code.
|
||||
/// </summary>
|
||||
public Shift ShiftDelegate;
|
||||
|
||||
/// <summary>
|
||||
/// Event that gets triggered when transmission shifts down.
|
||||
/// </summary>
|
||||
public event Action OnGearDownShift;
|
||||
|
||||
/// <summary>
|
||||
/// Event that gets triggered when transmission shifts (up or down).
|
||||
/// </summary>
|
||||
public event Action OnGearShift;
|
||||
|
||||
/// <summary>
|
||||
/// Event that gets triggered when transmission shifts up.
|
||||
/// </summary>
|
||||
public event Action OnGearUpShift;
|
||||
|
||||
/// <summary>
|
||||
/// Time after shifting in which shifting can not be done again.
|
||||
/// </summary>
|
||||
[Property] public float PostShiftBan { get; set; } = 0.5f;
|
||||
|
||||
public enum AutomaticTransmissionDNRShiftType
|
||||
{
|
||||
Auto,
|
||||
RequireShiftInput,
|
||||
RepeatInput,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Behavior when switching from neutral to forward or reverse gear.
|
||||
/// </summary>
|
||||
[Property] public AutomaticTransmissionDNRShiftType DNRShiftType { get; set; } = AutomaticTransmissionDNRShiftType.Auto;
|
||||
|
||||
/// <summary>
|
||||
/// Speed at which the vehicle can switch between D/N/R gears.
|
||||
/// </summary>
|
||||
[Property] public float DnrSpeedThreshold { get; set; } = 0.4f;
|
||||
|
||||
/// <summary>
|
||||
/// If set to >0, the clutch will need to be released to the value below the set number
|
||||
/// for gear shifts to occur.
|
||||
/// </summary>
|
||||
[Property, Range( 0, 1 )] public float ClutchInputShiftThreshold { get; set; } = 1f;
|
||||
|
||||
/// <summary>
|
||||
/// Time it takes transmission to shift between gears.
|
||||
/// </summary>
|
||||
[Property] public float ShiftDuration { get; set; } = 0.2f;
|
||||
|
||||
/// <summary>
|
||||
/// Intensity of variable shift point. Higher value will result in shift point moving higher up with higher engine
|
||||
/// load.
|
||||
/// </summary>
|
||||
[Property, Range( 0, 1 )] public float VariableShiftIntensity { get; set; } = 0.3f;
|
||||
|
||||
/// <summary>
|
||||
/// If enabled transmission will adjust both shift up and down points to match current load.
|
||||
/// </summary>
|
||||
[Property] public bool VariableShiftPoint { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Current gear ratio.
|
||||
/// </summary>
|
||||
[Property, ReadOnly] public float CurrentGearRatio { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is the transmission currently in the post-shift phase in which the shifting is disabled/banned to prevent gear hunting?
|
||||
/// </summary>
|
||||
[Property, ReadOnly] public bool IsPostShiftBanActive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is a gear shift currently in progress.
|
||||
/// </summary>
|
||||
[Property, ReadOnly] public bool IsShifting { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Progress of the current gear shift in range of 0 to 1.
|
||||
/// </summary>
|
||||
[Property, ReadOnly] public float ShiftProgress { get; set; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Current RPM at which transmission will aim to downshift. All the modifiers are taken into account.
|
||||
/// This value changes with driving conditions.
|
||||
/// </summary>
|
||||
[Property]
|
||||
public float DownshiftRPM
|
||||
{
|
||||
get => _downshiftRPM;
|
||||
set { _downshiftRPM = Math.Clamp( value, 0, float.MaxValue ); }
|
||||
}
|
||||
|
||||
private float _downshiftRPM = 1400;
|
||||
/// <summary>
|
||||
/// RPM at which the transmission will try to downshift, but the value might get changed by shift modifier such
|
||||
/// as incline modifier.
|
||||
/// To get actual downshift RPM use DownshiftRPM.
|
||||
/// </summary>
|
||||
[Property]
|
||||
public float TargetDownshiftRPM => _targetDownshiftRPM;
|
||||
|
||||
private float _targetDownshiftRPM;
|
||||
/// <summary>
|
||||
/// RPM at which automatic transmission will shift up. If dynamic shift point is enabled this value will change
|
||||
/// depending on load.
|
||||
/// </summary>
|
||||
[Property]
|
||||
public float UpshiftRPM
|
||||
{
|
||||
get => _upshiftRPM;
|
||||
set { _upshiftRPM = Math.Clamp( value, 0, float.MaxValue ); }
|
||||
}
|
||||
private float _upshiftRPM = 2800;
|
||||
|
||||
/// <summary>
|
||||
/// RPM at which the transmission will try to upshift, but the value might get changed by shift modifier such
|
||||
/// as incline modifier.
|
||||
/// To get actual upshift RPM use UpshiftRPM.
|
||||
/// </summary>
|
||||
[Property]
|
||||
public float TargetUpshiftRPM => _targetUpshiftRPM;
|
||||
private float _targetUpshiftRPM;
|
||||
|
||||
public enum TransmissionShiftType
|
||||
{
|
||||
Manual,
|
||||
Automatic,
|
||||
//CVT,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines in which way gears can be changed.
|
||||
/// Manual - gears can only be shifted by manual user input.
|
||||
/// Automatic - automatic gear changing. Allows for gear skipping (e.g. 3rd->5th) which can be useful in trucks and
|
||||
/// other high gear count vehicles.
|
||||
/// AutomaticSequential - automatic gear changing but only one gear at the time can be shifted (e.g. 3rd->4th)
|
||||
/// </summary>
|
||||
[Property]
|
||||
public TransmissionShiftType TransmissionType
|
||||
{
|
||||
get => transmissionType; set
|
||||
{
|
||||
if ( value == transmissionType )
|
||||
return;
|
||||
transmissionType = value;
|
||||
AssignShiftDelegate();
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Is the automatic gearbox sequential?
|
||||
/// Has no effect on manual transmission.
|
||||
/// </summary>
|
||||
[Property] public bool IsSequential { get; set; } = false;
|
||||
|
||||
[Property] public bool AllowUpshiftGearSkipping { get; set; }
|
||||
|
||||
[Property] public bool AllowDownshiftGearSkipping { get; set; } = true;
|
||||
|
||||
private bool _repeatInputFlag;
|
||||
private float _smoothedThrottleInput;
|
||||
|
||||
/// <summary>
|
||||
/// Timer needed to prevent manual transmission from slipping out of gear too soon when hold in gear is enabled,
|
||||
/// which could happen in FixedUpdate() runs twice for one Update() and the shift flag is reset
|
||||
/// resulting in gearbox thinking it has no shift input.
|
||||
/// </summary>
|
||||
private float _slipOutOfGearTimer = -999f;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 0 for neutral, less than 0 for reverse gears and lager than 0 for forward gears.
|
||||
/// Use 'ShiftInto' to set gear.
|
||||
/// </summary>
|
||||
[Property, Sync]
|
||||
public int Gear
|
||||
{
|
||||
get => IndexToGear( gearIndex );
|
||||
set => gearIndex = GearToIndex( value );
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Current gear index in the gears list.
|
||||
/// Different from gear because gear uses -1 = R, 0 = N and D = 1, while this is the apsolute index
|
||||
/// in the range of 0 to gear list size minus one.
|
||||
/// Use Gear to get the actual gear.
|
||||
/// </summary>
|
||||
public int gearIndex;
|
||||
private TransmissionShiftType transmissionType = TransmissionShiftType.Automatic;
|
||||
|
||||
private int GearToIndex( int g )
|
||||
{
|
||||
return g + ReverseGearCount;
|
||||
}
|
||||
|
||||
private int IndexToGear( int g )
|
||||
{
|
||||
return g - ReverseGearCount;
|
||||
}
|
||||
/// <summary>
|
||||
/// Returns current gear name as a string, e.g. "R", "R2", "N" or "1"
|
||||
/// </summary>
|
||||
public string GearName
|
||||
{
|
||||
get
|
||||
{
|
||||
int gear = Gear;
|
||||
|
||||
if ( _gearNameCache.TryGetValue( gear, out string gearName ) )
|
||||
return gearName;
|
||||
|
||||
if ( gear == 0 )
|
||||
gearName = "N";
|
||||
else if ( gear > 0 )
|
||||
gearName = Gear.ToString();
|
||||
else
|
||||
gearName = "R" + (ReverseGearCount > 1 ? -gear : "");
|
||||
|
||||
_gearNameCache[gear] = gearName;
|
||||
return gearName;
|
||||
}
|
||||
}
|
||||
|
||||
private readonly Dictionary<int, string> _gearNameCache = new();
|
||||
|
||||
public void LoadGearsFromGearingProfile()
|
||||
{
|
||||
if ( GearingProfile == null )
|
||||
return;
|
||||
|
||||
int totalGears = GearingProfile.ReverseGears.Count + 1 + GearingProfile.ForwardGears.Count;
|
||||
if ( Gears == null )
|
||||
Gears = new( totalGears );
|
||||
else
|
||||
{
|
||||
Gears.Clear();
|
||||
Gears.Capacity = totalGears;
|
||||
}
|
||||
|
||||
Gears.AddRange( GearingProfile.ReverseGears );
|
||||
Gears.Add( 0 );
|
||||
Gears.AddRange( GearingProfile.ForwardGears );
|
||||
}
|
||||
protected override void OnStart()
|
||||
{
|
||||
base.OnStart();
|
||||
LoadGearsFromGearingProfile();
|
||||
UpdateGearCounts();
|
||||
Gear = 0;
|
||||
|
||||
AssignShiftDelegate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Total gear ratio of the transmission for current gear.
|
||||
/// </summary>
|
||||
private float CalculateTotalGearRatio()
|
||||
{
|
||||
|
||||
//if ( TransmissionType == TransmissionShiftType.CVT )
|
||||
//{
|
||||
// float minRatio = Gears[gearIndex];
|
||||
// float maxRatio = minRatio * 40f;
|
||||
// float t = Math.Clamp( Controller.Engine.RPMPercent + (1f - Controller.Engine.ThrottlePosition), 0, 1 );
|
||||
// float ratio = MathX.Lerp( maxRatio, minRatio, t ) * FinalGearRatio;
|
||||
// return MathX.Lerp( CurrentGearRatio, ratio, Time.Delta * 5f );
|
||||
//}
|
||||
//else
|
||||
return Gears[gearIndex] * FinalGearRatio;
|
||||
}
|
||||
public override float QueryAngularVelocity( float angularVelocity, float dt )
|
||||
{
|
||||
InputAngularVelocity = angularVelocity;
|
||||
|
||||
if ( CurrentGearRatio == 0 || OutputNameHash == 0 )
|
||||
{
|
||||
OutputAngularVelocity = 0f;
|
||||
return angularVelocity;
|
||||
}
|
||||
|
||||
OutputAngularVelocity = InputAngularVelocity / CurrentGearRatio;
|
||||
return _output.QueryAngularVelocity( OutputAngularVelocity, dt ) * CurrentGearRatio;
|
||||
}
|
||||
|
||||
public override float QueryInertia()
|
||||
{
|
||||
if ( OutputNameHash == 0 || CurrentGearRatio == 0 )
|
||||
return Inertia;
|
||||
|
||||
return Inertia + _output.QueryInertia() / (CurrentGearRatio * CurrentGearRatio);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the would-be RPM if none of the wheels was slipping.
|
||||
/// </summary>
|
||||
/// <returns>RPM as it would be if the wheels are not slipping or in the air.</returns>
|
||||
private float CalculateNoSlipRPM()
|
||||
{
|
||||
float vehicleLocalVelocity = Controller.LocalVelocity.x.InchToMeter();
|
||||
|
||||
// Get the average no-slip wheel RPM
|
||||
// Use the vehicle velocity as the friction velocities for the wheel are 0 when in air and
|
||||
// because the shift RPM is not really required to be extremely precise, so slight offset
|
||||
// between the vehicle position and velocity and the wheel ones is not important.
|
||||
// Still, calculate for each wheel since radius might be different.
|
||||
float angVelSum = 0f;
|
||||
foreach ( VeloXWheel wheelComponent in Controller.MotorWheels )
|
||||
{
|
||||
angVelSum += vehicleLocalVelocity / wheelComponent.Radius;
|
||||
}
|
||||
|
||||
// Apply total gear ratio to get the no-slip condition RPM
|
||||
return AngularVelocityToRPM( angVelSum / Controller.MotorWheels.Count ) * CurrentGearRatio;
|
||||
}
|
||||
|
||||
public override float ForwardStep( float torque, float inertiaSum, float dt )
|
||||
{
|
||||
InputTorque = torque;
|
||||
InputInertia = inertiaSum;
|
||||
|
||||
UpdateGearCounts();
|
||||
|
||||
if ( _output == null )
|
||||
return InputTorque;
|
||||
|
||||
// Update current gear ratio
|
||||
CurrentGearRatio = CalculateTotalGearRatio();
|
||||
|
||||
// Run the shift function
|
||||
_referenceShiftRPM = CalculateNoSlipRPM();
|
||||
ShiftDelegate?.Invoke( Controller );
|
||||
|
||||
// Reset any input related to shifting, now that the shifting has been processed
|
||||
//Controller.Input.ResetShiftFlags();
|
||||
|
||||
// Run the physics step
|
||||
// No output, simply return the torque to the sender
|
||||
if ( OutputNameHash == 0 )
|
||||
return torque;
|
||||
|
||||
// In neutral, do not send any torque but update components downstram
|
||||
if ( CurrentGearRatio < 1e-5f && CurrentGearRatio > -1e-5f )
|
||||
{
|
||||
OutputTorque = 0;
|
||||
OutputInertia = InputInertia;
|
||||
_output.ForwardStep( OutputTorque, OutputInertia, dt );
|
||||
return torque;
|
||||
}
|
||||
|
||||
// Always send torque to keep wheels updated
|
||||
OutputTorque = torque * CurrentGearRatio;
|
||||
OutputInertia = (inertiaSum + Inertia) * (CurrentGearRatio * CurrentGearRatio);
|
||||
return _output.ForwardStep( torque * CurrentGearRatio, OutputInertia, dt ) / CurrentGearRatio;
|
||||
}
|
||||
private void UpdateGearCounts()
|
||||
{
|
||||
ForwardGearCount = 0;
|
||||
ReverseGearCount = 0;
|
||||
int gearCount = Gears.Count;
|
||||
for ( int i = 0; i < gearCount; i++ )
|
||||
{
|
||||
float gear = Gears[i];
|
||||
if ( gear > 0 )
|
||||
ForwardGearCount++;
|
||||
else if ( gear < 0 )
|
||||
ReverseGearCount++;
|
||||
}
|
||||
}
|
||||
|
||||
private void AssignShiftDelegate()
|
||||
{
|
||||
if ( TransmissionType == TransmissionShiftType.Manual )
|
||||
ShiftDelegate = ManualShift;
|
||||
else if ( TransmissionType == TransmissionShiftType.Automatic )
|
||||
ShiftDelegate = AutomaticShift;
|
||||
//else if ( TransmissionType == TransmissionShiftType.CVT )
|
||||
// ShiftDelegate = CVTShift;
|
||||
}
|
||||
private void ManualShift( VeloXBase car )
|
||||
{
|
||||
if ( car.IsShiftingUp )
|
||||
{
|
||||
ShiftInto( Gear + 1 );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( car.IsShiftingDown )
|
||||
{
|
||||
ShiftInto( Gear - 1 );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( HoldToKeepInGear )
|
||||
{
|
||||
_slipOutOfGearTimer += Time.Delta;
|
||||
if ( Gear != 0 && _slipOutOfGearTimer > 0.1f )
|
||||
ShiftInto( 0 );
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Shifts into given gear. 0 for neutral, less than 0 for reverse and above 0 for forward gears.
|
||||
/// Does nothing if the target gear is equal to current gear.
|
||||
/// </summary>
|
||||
public void ShiftInto( int targetGear, bool instant = false )
|
||||
{
|
||||
// Clutch is not pressed above the set threshold, exit and do not shift.
|
||||
if ( Controller.IsClutching > ClutchInputShiftThreshold )
|
||||
return;
|
||||
|
||||
int currentGear = Gear;
|
||||
bool isShiftFromOrToNeutral = targetGear == 0 || currentGear == 0;
|
||||
|
||||
//Debug.Log($"Shift from {currentGear} into {targetGear}");
|
||||
|
||||
// Check if shift can happen at all
|
||||
if ( targetGear == currentGear || targetGear < -100 )
|
||||
return;
|
||||
|
||||
// Convert gear to gear list index
|
||||
int targetIndex = GearToIndex( targetGear );
|
||||
|
||||
// Check for gear list bounds
|
||||
if ( targetIndex < 0 || targetIndex >= Gears.Count )
|
||||
return;
|
||||
|
||||
if ( !IsShifting && (isShiftFromOrToNeutral || !IsPostShiftBanActive) )
|
||||
{
|
||||
ShiftCoroutine( currentGear, targetGear, isShiftFromOrToNeutral || instant );
|
||||
|
||||
// If in neutral reset the repeated input flat required for repeat input reverse
|
||||
if ( targetGear == 0 )
|
||||
_repeatInputFlag = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async void ShiftCoroutine( int currentGear, int targetGear, bool instant )
|
||||
{
|
||||
if ( IsShifting )
|
||||
return;
|
||||
|
||||
float dt = Time.Delta;
|
||||
bool isManual = TransmissionType == TransmissionShiftType.Manual;
|
||||
|
||||
//Debug.Log($"Shift from {currentGear} to {targetGear}, instant: {instant}");
|
||||
|
||||
// Immediately start shift ban to prevent repeated shifts while this one has not finished
|
||||
if ( !isManual )
|
||||
IsPostShiftBanActive = true;
|
||||
|
||||
IsShifting = true;
|
||||
ShiftProgress = 0f;
|
||||
|
||||
// Run the first half of shift timer
|
||||
float shiftTimer = 0;
|
||||
float halfDuration = ShiftDuration * 0.5f;
|
||||
if ( !instant )
|
||||
while ( shiftTimer < halfDuration )
|
||||
{
|
||||
ShiftProgress = shiftTimer / ShiftDuration;
|
||||
shiftTimer += dt;
|
||||
await Task.DelayRealtimeSeconds( dt );
|
||||
}
|
||||
|
||||
// Do the shift at the half point of shift duration
|
||||
Gear = targetGear;
|
||||
if ( currentGear < targetGear )
|
||||
OnGearUpShift?.Invoke();
|
||||
else
|
||||
OnGearDownShift?.Invoke();
|
||||
|
||||
OnGearShift?.Invoke();
|
||||
|
||||
// Run the second half of the shift timer
|
||||
if ( !instant )
|
||||
while ( shiftTimer < ShiftDuration )
|
||||
{
|
||||
ShiftProgress = shiftTimer / ShiftDuration;
|
||||
shiftTimer += dt;
|
||||
await Task.DelayRealtimeSeconds( dt );
|
||||
}
|
||||
|
||||
|
||||
// Shift has finished
|
||||
ShiftProgress = 1f;
|
||||
IsShifting = false;
|
||||
|
||||
// Run post shift ban only if not manual as blocking user input feels unresponsive and post shift ban
|
||||
// exists to prevent auto transmission from hunting.
|
||||
if ( !isManual )
|
||||
{
|
||||
// Post shift ban timer
|
||||
float postShiftBanTimer = 0;
|
||||
while ( postShiftBanTimer < PostShiftBan )
|
||||
{
|
||||
postShiftBanTimer += dt;
|
||||
await Task.DelayRealtimeSeconds( dt );
|
||||
}
|
||||
|
||||
// Post shift ban has finished
|
||||
IsPostShiftBanActive = false;
|
||||
}
|
||||
}
|
||||
private void CVTShift( VeloXBase car ) => AutomaticShift( car );
|
||||
|
||||
/// <summary>
|
||||
/// Handles automatic and automatic sequential shifting.
|
||||
/// </summary>
|
||||
private void AutomaticShift( VeloXBase car )
|
||||
{
|
||||
float vehicleSpeed = car.ForwardSpeed;
|
||||
|
||||
float throttleInput = car.SwappedThrottle;
|
||||
float brakeInput = car.SwappedBrakes;
|
||||
int currentGear = Gear;
|
||||
// Assign base shift points
|
||||
_targetDownshiftRPM = _downshiftRPM;
|
||||
_targetUpshiftRPM = _upshiftRPM;
|
||||
|
||||
// Calculate shift points for variable shift RPM
|
||||
if ( VariableShiftPoint )
|
||||
{
|
||||
// Smooth throttle input so that the variable shift point does not shift suddenly and cause gear hunting
|
||||
_smoothedThrottleInput = MathX.Lerp( _smoothedThrottleInput, throttleInput, Time.Delta * 2f );
|
||||
float revLimiterRPM = car.Engine.RevLimiterRPM;
|
||||
|
||||
_targetUpshiftRPM = _upshiftRPM + Math.Clamp( _smoothedThrottleInput * VariableShiftIntensity, 0f, 1f ) * _upshiftRPM;
|
||||
_targetUpshiftRPM = Math.Clamp( _targetUpshiftRPM, _upshiftRPM, revLimiterRPM * 0.97f );
|
||||
|
||||
_targetDownshiftRPM = _downshiftRPM + Math.Clamp( _smoothedThrottleInput * VariableShiftIntensity, 0f, 1f ) * _downshiftRPM;
|
||||
_targetDownshiftRPM = Math.Clamp( _targetDownshiftRPM, car.Engine.IdleRPM * 1.1f, _targetUpshiftRPM * 0.7f );
|
||||
|
||||
// Add incline modifier
|
||||
float inclineModifier = Math.Clamp( car.WorldRotation.Forward.Dot( Vector3.Up ) * InclineEffectCoeff, 0f, 1f );
|
||||
|
||||
_targetUpshiftRPM += revLimiterRPM * inclineModifier;
|
||||
_targetDownshiftRPM += revLimiterRPM * inclineModifier;
|
||||
}
|
||||
|
||||
|
||||
// In neutral
|
||||
if ( currentGear == 0 )
|
||||
{
|
||||
if ( DNRShiftType == AutomaticTransmissionDNRShiftType.Auto )
|
||||
{
|
||||
if ( throttleInput > INPUT_DEADZONE )
|
||||
ShiftInto( 1 );
|
||||
else if ( brakeInput > INPUT_DEADZONE )
|
||||
ShiftInto( -1 );
|
||||
}
|
||||
else if ( DNRShiftType == AutomaticTransmissionDNRShiftType.RequireShiftInput )
|
||||
{
|
||||
if ( car.IsShiftingUp )
|
||||
ShiftInto( 1 );
|
||||
else if ( car.IsShiftingDown )
|
||||
ShiftInto( -1 );
|
||||
}
|
||||
else if ( DNRShiftType == AutomaticTransmissionDNRShiftType.RepeatInput )
|
||||
{
|
||||
if ( _repeatInputFlag == false && throttleInput < INPUT_DEADZONE && brakeInput < INPUT_DEADZONE )
|
||||
_repeatInputFlag = true;
|
||||
|
||||
if ( _repeatInputFlag )
|
||||
{
|
||||
if ( throttleInput > INPUT_DEADZONE )
|
||||
ShiftInto( 1 );
|
||||
else if ( brakeInput > INPUT_DEADZONE )
|
||||
ShiftInto( -1 );
|
||||
}
|
||||
}
|
||||
}
|
||||
// In reverse
|
||||
else if ( currentGear < 0 )
|
||||
{
|
||||
// Shift into neutral
|
||||
if ( DNRShiftType == AutomaticTransmissionDNRShiftType.RequireShiftInput )
|
||||
{
|
||||
if ( car.IsShiftingUp )
|
||||
ShiftInto( 0 );
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( vehicleSpeed < DnrSpeedThreshold && (brakeInput > INPUT_DEADZONE || throttleInput < INPUT_DEADZONE) )
|
||||
ShiftInto( 0 );
|
||||
}
|
||||
|
||||
// Reverse upshift
|
||||
float absGearMinusOne = currentGear - 1;
|
||||
absGearMinusOne = absGearMinusOne < 0 ? -absGearMinusOne : absGearMinusOne;
|
||||
if ( _referenceShiftRPM > TargetUpshiftRPM && absGearMinusOne < ReverseGearCount )
|
||||
ShiftInto( currentGear - 1 );
|
||||
// Reverse downshift
|
||||
else if ( _referenceShiftRPM < TargetDownshiftRPM && currentGear < -1 )
|
||||
ShiftInto( currentGear + 1 );
|
||||
}
|
||||
// In forward
|
||||
else
|
||||
{
|
||||
if ( vehicleSpeed > 0.4f )
|
||||
{
|
||||
// Upshift
|
||||
if ( currentGear < ForwardGearCount && _referenceShiftRPM > TargetUpshiftRPM )
|
||||
{
|
||||
if ( !IsSequential && AllowUpshiftGearSkipping )
|
||||
{
|
||||
int g = currentGear;
|
||||
|
||||
while ( g < ForwardGearCount )
|
||||
{
|
||||
g++;
|
||||
|
||||
float wouldBeEngineRPM = ReverseTransmitRPM( _referenceShiftRPM / CurrentGearRatio, g );
|
||||
float shiftDurationPadding = Math.Clamp( ShiftDuration, 0, 1 ) * (_targetUpshiftRPM - _targetDownshiftRPM) * 0.25f;
|
||||
|
||||
if ( wouldBeEngineRPM < _targetDownshiftRPM + shiftDurationPadding )
|
||||
{
|
||||
g--;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( g != currentGear )
|
||||
{
|
||||
ShiftInto( g );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ShiftInto( currentGear + 1 );
|
||||
}
|
||||
}
|
||||
// Downshift
|
||||
else if ( _referenceShiftRPM < TargetDownshiftRPM )
|
||||
{
|
||||
// Non-sequential
|
||||
if ( !IsSequential && AllowDownshiftGearSkipping )
|
||||
{
|
||||
if ( currentGear != 1 )
|
||||
{
|
||||
int g = currentGear;
|
||||
while ( g > 1 )
|
||||
{
|
||||
g--;
|
||||
float wouldBeEngineRPM = ReverseTransmitRPM( _referenceShiftRPM / CurrentGearRatio, g );
|
||||
if ( wouldBeEngineRPM > _targetUpshiftRPM )
|
||||
{
|
||||
g++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( g != currentGear )
|
||||
{
|
||||
ShiftInto( g );
|
||||
}
|
||||
}
|
||||
else if ( vehicleSpeed < DnrSpeedThreshold && throttleInput < INPUT_DEADZONE
|
||||
&& DNRShiftType !=
|
||||
AutomaticTransmissionDNRShiftType
|
||||
.RequireShiftInput )
|
||||
{
|
||||
ShiftInto( 0 );
|
||||
}
|
||||
}
|
||||
// Sequential
|
||||
else
|
||||
{
|
||||
if ( currentGear != 1 )
|
||||
{
|
||||
ShiftInto( currentGear - 1 );
|
||||
}
|
||||
else if ( vehicleSpeed < DnrSpeedThreshold && throttleInput < INPUT_DEADZONE &&
|
||||
brakeInput < INPUT_DEADZONE
|
||||
&& DNRShiftType !=
|
||||
AutomaticTransmissionDNRShiftType.RequireShiftInput )
|
||||
{
|
||||
ShiftInto( 0 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Shift into neutral
|
||||
else
|
||||
{
|
||||
if ( DNRShiftType != AutomaticTransmissionDNRShiftType.RequireShiftInput )
|
||||
{
|
||||
if ( throttleInput < INPUT_DEADZONE )
|
||||
{
|
||||
ShiftInto( 0 );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( car.IsShiftingDown )
|
||||
{
|
||||
ShiftInto( 0 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts axle RPM to engine RPM for given gear in Gears list.
|
||||
/// </summary>
|
||||
public float ReverseTransmitRPM( float inputRPM, int g )
|
||||
{
|
||||
float outRpm = inputRPM * Gears[GearToIndex( g )] * FinalGearRatio;
|
||||
return Math.Abs( outRpm );
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,67 +0,0 @@
|
||||
using Sandbox;
|
||||
using System;
|
||||
|
||||
namespace VeloX;
|
||||
|
||||
public partial class WheelPowertrain : PowertrainComponent
|
||||
{
|
||||
protected override void OnAwake()
|
||||
{
|
||||
base.OnAwake();
|
||||
Name ??= Wheel.ToString();
|
||||
}
|
||||
[Property] public VeloXWheel Wheel { get; set; }
|
||||
|
||||
protected override void OnStart()
|
||||
{
|
||||
_initialRollingResistance = Wheel.RollingResistanceTorque;
|
||||
}
|
||||
private float _initialRollingResistance;
|
||||
|
||||
public override float QueryAngularVelocity( float angularVelocity, float dt )
|
||||
{
|
||||
InputAngularVelocity = OutputAngularVelocity = Wheel.AngularVelocity;
|
||||
|
||||
return OutputAngularVelocity;
|
||||
}
|
||||
|
||||
public override float QueryInertia()
|
||||
{
|
||||
// Calculate the base inertia of the wheel and scale it by the inverse of the dt.
|
||||
float dtScale = Math.Clamp( Time.Delta, 0.01f, 0.05f ) / 0.005f;
|
||||
float radius = Wheel.Radius;
|
||||
return 0.5f * Wheel.Mass * radius * radius * dtScale;
|
||||
}
|
||||
|
||||
public void ApplyRollingResistanceMultiplier( float multiplier )
|
||||
{
|
||||
|
||||
Wheel.RollingResistanceTorque = _initialRollingResistance * multiplier;
|
||||
}
|
||||
public override float ForwardStep( float torque, float inertiaSum, float dt )
|
||||
{
|
||||
InputTorque = torque;
|
||||
InputInertia = inertiaSum;
|
||||
|
||||
OutputTorque = InputTorque;
|
||||
OutputInertia = Wheel.BaseInertia + inertiaSum;
|
||||
|
||||
Wheel.DriveTorque = OutputTorque;
|
||||
Wheel.Inertia = OutputInertia;
|
||||
|
||||
Wheel.AutoSimulate = false;
|
||||
Wheel.StepPhys( Controller, dt );
|
||||
|
||||
return Math.Abs( Wheel.CounterTorque );
|
||||
}
|
||||
|
||||
protected override void DrawGizmos()
|
||||
{
|
||||
|
||||
if ( !Gizmo.IsSelected )
|
||||
return;
|
||||
Gizmo.Transform = Wheel.WorldTransform;
|
||||
Wheel?.GizmoDraw();
|
||||
|
||||
}
|
||||
}
|
||||
@ -1,128 +0,0 @@
|
||||
using Sandbox;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
namespace VeloX;
|
||||
|
||||
public abstract partial class VeloXBase
|
||||
{
|
||||
|
||||
[Property, Feature( "Powertrain" )] public Engine Engine { get; set; }
|
||||
[Property, Feature( "Powertrain" )] public Clutch Clutch { get; set; }
|
||||
[Property, Feature( "Powertrain" )] public Transmission Transmission { get; set; }
|
||||
[Property, Feature( "Powertrain" )] public Differential Differential { get; set; }
|
||||
|
||||
[Property, Feature( "Powertrain" )] public List<VeloXWheel> MotorWheels { get; set; }
|
||||
|
||||
private GameObject powertrainGameObject;
|
||||
|
||||
[Button, Feature( "Powertrain" )]
|
||||
internal void CreatePowertrain()
|
||||
{
|
||||
using var undoScope = Scene.Editor?.UndoScope( "Create Powertrain" ).WithComponentCreations().WithGameObjectCreations().Push();
|
||||
if ( !powertrainGameObject.IsValid() )
|
||||
{
|
||||
powertrainGameObject = null;
|
||||
}
|
||||
powertrainGameObject ??= new GameObject( true, "Powertrain" );
|
||||
|
||||
if ( !Engine.IsValid() )
|
||||
Engine = new GameObject( powertrainGameObject, true, "Engine" ).GetOrAddComponent<Engine>();
|
||||
|
||||
Engine.Controller = this;
|
||||
Engine.Inertia = 0.25f;
|
||||
Engine.Ignition = false;
|
||||
|
||||
if ( !Clutch.IsValid() )
|
||||
Clutch = new GameObject( Engine.GameObject, true, "Clutch" ).GetOrAddComponent<Clutch>();
|
||||
|
||||
Clutch.Controller = this;
|
||||
Clutch.Inertia = 0.02f;
|
||||
|
||||
Engine.Output = Clutch;
|
||||
|
||||
if ( !Transmission.IsValid() )
|
||||
Transmission = new GameObject( Clutch.GameObject, true, "Transmission" ).GetOrAddComponent<Transmission>();
|
||||
|
||||
Transmission.Controller = this;
|
||||
Transmission.Inertia = 0.01f;
|
||||
|
||||
Clutch.Output = Transmission;
|
||||
|
||||
Differential = new TreeBuilder( Transmission, MotorWheels ).Root.Diff;
|
||||
|
||||
Transmission.Output = Differential;
|
||||
|
||||
//PowertrainWheels = Differential.Components.GetAll<WheelPowertrain>( FindMode.InDescendants ).ToList();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
internal class TreeNode
|
||||
{
|
||||
internal TreeNode Left { get; set; }
|
||||
internal TreeNode Right { get; set; }
|
||||
public WheelPowertrain Item { get; set; }
|
||||
public Differential Diff { get; set; }
|
||||
public bool IsLeaf => Left == null && Right == null;
|
||||
}
|
||||
|
||||
internal class TreeBuilder
|
||||
{
|
||||
internal TreeNode Root { get; private set; }
|
||||
|
||||
internal TreeBuilder( PowertrainComponent parent, List<VeloXWheel> items )
|
||||
{
|
||||
if ( items == null || items.Count == 0 )
|
||||
throw new ArgumentException( "Items list cannot be null or empty." );
|
||||
|
||||
Root = BuildTree( parent, items, 0, items.Count - 1 );
|
||||
}
|
||||
|
||||
private static TreeNode BuildTree( PowertrainComponent parent, List<VeloXWheel> items, int start, int end )
|
||||
{
|
||||
if ( start > end )
|
||||
return null;
|
||||
|
||||
if ( start == end )
|
||||
{
|
||||
|
||||
var leaf = new TreeNode() { Item = new GameObject( parent.GameObject, true, $"Wheel {items[start].GameObject.Name}" ).GetOrAddComponent<WheelPowertrain>() };
|
||||
leaf.Item.Controller = parent.Controller;
|
||||
leaf.Item.Wheel = items[start];
|
||||
leaf.Item.Inertia = 0.01f;
|
||||
var parentd = parent as Differential;
|
||||
if ( (start + 1) % 2 == 0 )
|
||||
{
|
||||
GameTask.RunInThreadAsync( () =>
|
||||
{
|
||||
parentd.OutputB = leaf.Item;
|
||||
} );
|
||||
}
|
||||
else
|
||||
{
|
||||
GameTask.RunInThreadAsync( () =>
|
||||
{
|
||||
parent.Output = leaf.Item;
|
||||
} );
|
||||
}
|
||||
|
||||
return leaf;
|
||||
}
|
||||
|
||||
int mid = (start + end) / 2;
|
||||
var diff = new GameObject( parent.GameObject, true, "Differential" ).GetOrAddComponent<Differential>();
|
||||
diff.Controller = parent.Controller;
|
||||
diff.Inertia = 0.1f;
|
||||
var node = new TreeNode
|
||||
{
|
||||
Left = BuildTree( diff, items, start, mid ),
|
||||
Right = BuildTree( diff, items, mid + 1, end ),
|
||||
Diff = diff
|
||||
};
|
||||
diff.Output = node.Left.Diff;
|
||||
diff.OutputB = node.Right.Diff;
|
||||
return node;
|
||||
}
|
||||
}
|
||||
@ -4,150 +4,20 @@ 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; }
|
||||
[Property, Feature( "Input" )] internal InputResolver Input { get; set; } = new();
|
||||
|
||||
private Guid _guid;
|
||||
|
||||
private bool IsDriverActive => Driver is not null;
|
||||
|
||||
public Vector2 MouseDelta => IsDriverActive ? Input.MouseDelta : default;
|
||||
public Vector2 MouseWheel => IsDriverActive ? Input.MouseWheel : default;
|
||||
public Angles AnalogLook => IsDriverActive ? Input.AnalogLook : default;
|
||||
public Vector3 AnalogMove => IsDriverActive ? Input.AnalogMove : default;
|
||||
|
||||
private float throttle;
|
||||
private float brakes;
|
||||
private float steerAngle;
|
||||
private float handbrake;
|
||||
|
||||
public bool CanInputSwapping { get; set; } = true;
|
||||
public bool IsInputSwapped => CanInputSwapping && Transmission.Gear < 0;
|
||||
|
||||
public bool IsShiftingDown { get; private set; }
|
||||
|
||||
|
||||
public bool IsShiftingUp { get; private set; }
|
||||
|
||||
public float IsClutching { get; private set; }
|
||||
|
||||
public float SwappedThrottle => IsInputSwapped ? Brakes : Throttle;
|
||||
public float SwappedBrakes => IsInputSwapped ? Throttle : Brakes;
|
||||
|
||||
|
||||
[Sync]
|
||||
public float VerticalInput
|
||||
[Sync( SyncFlags.FromHost )]
|
||||
public Guid ConnectionID
|
||||
{
|
||||
get => throttle - brakes;
|
||||
get => _guid;
|
||||
set
|
||||
{
|
||||
float clampedValue = Math.Clamp( value, -1, 1 );
|
||||
|
||||
if ( value > 0 )
|
||||
{
|
||||
throttle = clampedValue;
|
||||
brakes = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
throttle = 0;
|
||||
brakes = -clampedValue;
|
||||
}
|
||||
_guid = value;
|
||||
Input.Driver = Connection.Find( _guid );
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throttle axis.
|
||||
/// For combined throttle/brake input use 'VerticalInput' instead.
|
||||
/// </summary>
|
||||
[Sync( SyncFlags.Interpolate ), Range( 0, 1 ), Property]
|
||||
public float Throttle
|
||||
{
|
||||
get => throttle;
|
||||
set => throttle = Math.Clamp( value, 0, 1 );
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Brake axis.
|
||||
/// For combined throttle/brake input use 'VerticalInput' instead.
|
||||
/// </summary>
|
||||
[Sync]
|
||||
public float Brakes
|
||||
{
|
||||
get => brakes;
|
||||
set => brakes = Math.Clamp( value, 0, 1 );
|
||||
}
|
||||
|
||||
[Sync]
|
||||
public float SteeringAngle
|
||||
{
|
||||
get => steerAngle;
|
||||
set => steerAngle = Math.Clamp( value, -1, 1 );
|
||||
}
|
||||
[Sync]
|
||||
public float Handbrake
|
||||
{
|
||||
get => handbrake;
|
||||
set => handbrake = Math.Clamp( value, 0, 1 );
|
||||
}
|
||||
|
||||
|
||||
public void ResetInput()
|
||||
{
|
||||
VerticalInput = 0;
|
||||
Handbrake = 0;
|
||||
|
||||
SteeringAngle = 0;
|
||||
|
||||
IsClutching = 0;
|
||||
|
||||
IsShiftingUp = false;
|
||||
IsShiftingDown = false;
|
||||
}
|
||||
private void UpdateInput()
|
||||
{
|
||||
VerticalInput = TotalSpeed < 10 ? Input.AnalogMove.x * 0.5f : Input.AnalogMove.x;
|
||||
Handbrake = Input.Down( "Jump" ) ? 1 : 0;
|
||||
|
||||
SteeringAngle = Input.AnalogMove.y;
|
||||
|
||||
IsClutching = (Input.Down( "Run" ) || Input.Down( "Jump" )) ? 1 : 0;
|
||||
|
||||
IsShiftingUp = Input.Pressed( "Attack1" );
|
||||
IsShiftingDown = Input.Pressed( "Attack2" );
|
||||
|
||||
if ( TotalSpeed < 150 && Driver is null )
|
||||
Handbrake = 1;
|
||||
|
||||
}
|
||||
|
||||
|
||||
public bool Down( string action )
|
||||
{
|
||||
return IsDriverActive && Input.Down( action );
|
||||
}
|
||||
|
||||
public bool Pressed( string action )
|
||||
{
|
||||
return IsDriverActive && Input.Pressed( action );
|
||||
}
|
||||
|
||||
public bool Released( string action )
|
||||
{
|
||||
return IsDriverActive && Input.Released( action );
|
||||
}
|
||||
|
||||
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 );
|
||||
}
|
||||
}
|
||||
|
||||
public void StopAllHaptics()
|
||||
{
|
||||
if ( IsDriverActive )
|
||||
Input.StopAllHaptics();
|
||||
}
|
||||
[Property, Feature( "Input" )] public bool IsDriver => ConnectionID == Connection.Local.Id;
|
||||
|
||||
}
|
||||
|
||||
@ -1,58 +1,41 @@
|
||||
using Sandbox;
|
||||
using System;
|
||||
|
||||
namespace VeloX;
|
||||
|
||||
public abstract partial class VeloXBase
|
||||
{
|
||||
|
||||
private Vector3 linForce;
|
||||
private Vector3 angForce;
|
||||
[Property] float BrakeForce { get; set; } = 1500f;
|
||||
[Property] float HandbrakeForce { get; set; } = 3500f;
|
||||
|
||||
private void PhysicsSimulate()
|
||||
{
|
||||
if ( Body.Sleeping && Input.AnalogMove.x == 0 )
|
||||
return;
|
||||
|
||||
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;
|
||||
|
||||
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 )
|
||||
foreach ( var v in Wheels )
|
||||
if ( v.AutoPhysics ) v.DoPhysics( this );
|
||||
|
||||
var totalSpeed = TotalSpeed + Math.Abs( Body.AngularVelocity.z );
|
||||
|
||||
var factor = 1 - Math.Clamp( totalSpeed / 30, 0, 1 );
|
||||
|
||||
if ( factor > 0.1f )
|
||||
{
|
||||
Vector3 vehVel = Body.Velocity;
|
||||
Vector3 vehAngVel = Body.AngularVelocity;
|
||||
var vel = Body.Velocity;
|
||||
|
||||
var dt = Time.Delta;
|
||||
CombinedLoad = 0;
|
||||
foreach ( var v in Wheels )
|
||||
CombinedLoad += v.Fz;
|
||||
foreach ( var v in Wheels )
|
||||
{
|
||||
v.BrakeTorque = SwappedBrakes * BrakeForce;
|
||||
if ( !v.IsFront )
|
||||
v.BrakeTorque += Handbrake * HandbrakeForce;
|
||||
var rt = WorldRotation.Right;
|
||||
|
||||
v.Update( this, in dt );
|
||||
v.DoPhysics( in dt );
|
||||
}
|
||||
|
||||
|
||||
|
||||
Body.Velocity = vehVel;
|
||||
Body.AngularVelocity = vehAngVel;
|
||||
var force = rt.Dot( vel ) / Time.Delta * mass * factor * rt;
|
||||
Body.ApplyForce( -force );
|
||||
}
|
||||
|
||||
Body.ApplyForce( linForce );
|
||||
Body.ApplyTorque( angForce );
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,16 +1,13 @@
|
||||
using Sandbox;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace VeloX;
|
||||
|
||||
public abstract partial class VeloXBase
|
||||
{
|
||||
|
||||
public float CombinedLoad { get; protected set; }
|
||||
[Property] public List<VeloXWheel> Wheels { get; set; }
|
||||
[Property] public TagSet WheelIgnoredTags { get; set; }
|
||||
public bool IsOnGround => Wheels.Any( x => x.IsOnGround );
|
||||
|
||||
public List<VeloXWheel> FindWheels() => [.. Components.GetAll<VeloXWheel>()];
|
||||
|
||||
@ -19,4 +16,5 @@ public abstract partial class VeloXBase
|
||||
{
|
||||
Wheels = FindWheels();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -7,7 +7,8 @@ public abstract partial class VeloXBase : Component
|
||||
{
|
||||
[Sync] public WaterState WaterState { get; set; }
|
||||
[Sync] public bool IsEngineOnFire { get; set; }
|
||||
[Property, Sync] public EngineState EngineState { 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;
|
||||
@ -22,7 +23,7 @@ public abstract partial class VeloXBase : Component
|
||||
|
||||
protected override void OnFixedUpdate()
|
||||
{
|
||||
if ( IsProxy )
|
||||
if ( !IsDriver )
|
||||
return;
|
||||
|
||||
LocalVelocity = WorldTransform.PointToLocal( WorldPosition + Body.Velocity );
|
||||
@ -30,9 +31,7 @@ public abstract partial class VeloXBase : Component
|
||||
TotalSpeed = LocalVelocity.Length;
|
||||
Body.PhysicsBody.Mass = Mass;
|
||||
|
||||
UpdateInput();
|
||||
PhysicsSimulate();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
43
Code/Base/Wheel/Pacejka.cs
Normal file
43
Code/Base/Wheel/Pacejka.cs
Normal file
@ -0,0 +1,43 @@
|
||||
using Sandbox;
|
||||
using Sandbox.Services;
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace VeloX;
|
||||
public class Pacejka
|
||||
{
|
||||
public class PacejkaPreset
|
||||
{
|
||||
[KeyProperty] public float B { get; set; } = 10.86f;
|
||||
[KeyProperty] public float C { get; set; } = 2.15f;
|
||||
[KeyProperty] public float D { get; set; } = 0.933f;
|
||||
[KeyProperty] public float E { get; set; } = 0.992f;
|
||||
|
||||
public float Evaluate( float slip ) => D * MathF.Sin( C * MathF.Atan( B * slip - E * (B * slip - MathF.Atan( B * slip )) ) );
|
||||
|
||||
public float GetPeakSlip()
|
||||
{
|
||||
float peakSlip = -1;
|
||||
float yMax = 0;
|
||||
|
||||
for ( float i = 0; i < 1f; i += 0.01f )
|
||||
{
|
||||
float y = Evaluate( i );
|
||||
if ( y > yMax )
|
||||
{
|
||||
yMax = y;
|
||||
peakSlip = i;
|
||||
}
|
||||
}
|
||||
|
||||
return peakSlip;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public PacejkaPreset Lateral { get; set; } = new();
|
||||
public PacejkaPreset Longitudinal { get; set; } = new();
|
||||
|
||||
public float PacejkaFx( float slip ) => Longitudinal.Evaluate( slip );
|
||||
public float PacejkaFy( float slip ) => Lateral.Evaluate( slip );
|
||||
}
|
||||
18
Code/Base/Wheel/Suspension/BasicSuspension.cs
Normal file
18
Code/Base/Wheel/Suspension/BasicSuspension.cs
Normal file
@ -0,0 +1,18 @@
|
||||
namespace VeloX;
|
||||
|
||||
public class BasicSuspension
|
||||
{
|
||||
private readonly Hinge Hinge;
|
||||
|
||||
public BasicSuspension( Vector3 wheel, Vector3 hingeBody, Vector3 hingeWheel )
|
||||
{
|
||||
Vector3 hingePoint = wheel - (hingeWheel - hingeBody);
|
||||
Hinge = new Hinge( hingePoint, hingeWheel - hingeBody );
|
||||
}
|
||||
|
||||
public virtual void GetWheelTransform( float travel, out Rotation rotation, out Vector3 position )
|
||||
{
|
||||
rotation = Rotation.Identity;
|
||||
position = Hinge.Rotate( travel );
|
||||
}
|
||||
}
|
||||
26
Code/Base/Wheel/Suspension/Hinge.cs
Normal file
26
Code/Base/Wheel/Suspension/Hinge.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using System;
|
||||
|
||||
namespace VeloX;
|
||||
|
||||
internal readonly struct Hinge( Vector3 hinge_anchor, Vector3 hinge_arm )
|
||||
{
|
||||
[Description( "the point that the wheels are rotated around as the suspension compresses" )]
|
||||
public readonly Vector3 Anchor = hinge_anchor;
|
||||
|
||||
[Description( "anchor to wheel vector" )]
|
||||
public readonly Vector3 Arm = hinge_arm;
|
||||
|
||||
[Description( "arm length squared" )]
|
||||
public readonly float LengthSquared = hinge_arm.Dot( hinge_arm );
|
||||
|
||||
[Description( "1 / arm length in hinge axis normal plane" )]
|
||||
public readonly float NormXY = 1 / MathF.Sqrt( hinge_arm.x * hinge_arm.x + hinge_arm.y * hinge_arm.y );
|
||||
|
||||
public readonly Vector3 Rotate( float travel )
|
||||
{
|
||||
float z = Arm.z + travel;
|
||||
float lengthSq = MathF.Max( LengthSquared - z * z, 0.0f );
|
||||
float nxy = NormXY * MathF.Sqrt( lengthSq );
|
||||
return Anchor + new Vector3( Arm.x * nxy, Arm.y * nxy, z );
|
||||
}
|
||||
}
|
||||
32
Code/Base/Wheel/Suspension/MacPhersonSuspension.cs
Normal file
32
Code/Base/Wheel/Suspension/MacPhersonSuspension.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using Sandbox;
|
||||
using System;
|
||||
|
||||
namespace VeloX;
|
||||
public class MacPhersonSuspension
|
||||
{
|
||||
public readonly Vector3 WheelOffset;
|
||||
public readonly Vector3 UprightTop;
|
||||
public readonly Vector3 UprightAxis;
|
||||
private readonly Hinge Hinge;
|
||||
|
||||
public MacPhersonSuspension( Vector3 wheel, Vector3 strutBody, Vector3 strutWheel, Vector3 hingeBody )
|
||||
{
|
||||
WheelOffset = wheel - strutWheel;
|
||||
UprightTop = strutBody;
|
||||
UprightAxis = (strutBody - strutWheel).Normal;
|
||||
Hinge = new( hingeBody, strutWheel - hingeBody );
|
||||
}
|
||||
|
||||
public void GetWheelTransform( float travel, out Rotation rotation, out Vector3 position )
|
||||
{
|
||||
Vector3 hingeEnd = Hinge.Rotate( travel );
|
||||
Vector3 uprightAxisNew = (UprightTop - hingeEnd).Normal;
|
||||
|
||||
rotation = Rotation.FromAxis(
|
||||
Vector3.Cross( UprightAxis, uprightAxisNew ),
|
||||
MathF.Acos( Vector3.Dot( UprightAxis, uprightAxisNew ) ).RadianToDegree()
|
||||
);
|
||||
|
||||
position = hingeEnd + WheelOffset.Transform( rotation );
|
||||
}
|
||||
}
|
||||
@ -4,45 +4,57 @@ using System;
|
||||
namespace VeloX;
|
||||
|
||||
|
||||
[AssetType( Name = "Wheel Friction", Extension = "tire", Category = "VeloX" )]
|
||||
[GameResource( "Wheel Friction", "tire", "Wheel Friction", Category = "VeloX", Icon = "radio_button_checked" )]
|
||||
public class TirePreset : GameResource
|
||||
{
|
||||
public static TirePreset Default { get; } = ResourceLibrary.Get<TirePreset>( "frictions/default.tire" );
|
||||
|
||||
private float peakSlip = -1;
|
||||
[Property] public Pacejka Pacejka { get; set; }
|
||||
|
||||
[Property] public float B { get; set; } = 10.86f;
|
||||
[Property] public float C { get; set; } = 2.15f;
|
||||
[Property] public float D { get; set; } = 0.933f;
|
||||
[Property] public float E { get; set; } = 0.992f;
|
||||
public float RollResistanceLin { get; set; } = 1E-3f;
|
||||
public float RollResistanceQuad { get; set; } = 1E-6f;
|
||||
|
||||
public float GetPeakSlip()
|
||||
{
|
||||
if ( peakSlip == -1 )
|
||||
peakSlip = CalcPeakSlip();
|
||||
public float GetRollingResistance( float velocity, float resistance_factor )
|
||||
{ // surface influence on rolling resistance
|
||||
float resistance = resistance_factor * RollResistanceLin;
|
||||
|
||||
return peakSlip;
|
||||
// heat due to tire deformation increases rolling resistance
|
||||
// approximate by quadratic function
|
||||
resistance += velocity * velocity * RollResistanceQuad;
|
||||
|
||||
return resistance;
|
||||
}
|
||||
|
||||
|
||||
public float Evaluate( float t ) => D * MathF.Sin( C * MathF.Atan( B * t - E * (B * t - MathF.Atan( B * t )) ) );
|
||||
|
||||
private float CalcPeakSlip()
|
||||
public static void ComputeSlip( float lon_velocity, float lat_velocity, float rot_velocity, float wheel_radius, out float slip_ratio, out float slip_angle )
|
||||
{
|
||||
float peakSlip = -1;
|
||||
float yMax = 0;
|
||||
var abs_lon = Math.Max( MathF.Abs( lon_velocity ), 1e-3f );
|
||||
|
||||
for ( float i = 0; i < 1f; i += 0.01f )
|
||||
{
|
||||
float y = Evaluate( i );
|
||||
if ( y > yMax )
|
||||
{
|
||||
yMax = y;
|
||||
peakSlip = i;
|
||||
}
|
||||
}
|
||||
slip_ratio = lon_velocity - rot_velocity * wheel_radius;
|
||||
|
||||
if ( abs_lon >= 0.005f )
|
||||
slip_ratio /= abs_lon;
|
||||
else
|
||||
slip_ratio *= abs_lon;
|
||||
|
||||
if ( abs_lon >= 0.5f )
|
||||
slip_angle = MathF.Atan2( -lat_velocity, abs_lon ).RadianToDegree() / 50f;
|
||||
else
|
||||
slip_angle = -lat_velocity * (0.01f / Time.Delta);
|
||||
|
||||
slip_ratio = Math.Clamp( slip_ratio, -1, 1 );
|
||||
slip_angle = Math.Clamp( slip_angle, -1, 1 );
|
||||
|
||||
return peakSlip;
|
||||
}
|
||||
|
||||
/// approximate asin(x) = x + x^3/6 for +-18 deg range
|
||||
public static float ComputeCamberAngle( float sin_camber )
|
||||
{
|
||||
float sc = Math.Clamp( sin_camber, -0.3f, 0.3f );
|
||||
return ((1 / 6.0f) * (sc * sc) + 1) * sc;
|
||||
}
|
||||
|
||||
public static float ComputeCamberVelocity( float sa, float vx )
|
||||
{
|
||||
float tansa = (1 / 3.0f * (sa * sa) + 1) * sa;
|
||||
return tansa * vx;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,295 +0,0 @@
|
||||
using Sandbox;
|
||||
using System;
|
||||
|
||||
namespace VeloX;
|
||||
|
||||
|
||||
public partial class VeloXWheel
|
||||
{
|
||||
/// <summary>
|
||||
/// Constant torque acting similar to brake torque.
|
||||
/// Imitates rolling resistance.
|
||||
/// </summary>
|
||||
[Property, Range( 0, 500 ), Sync] public float RollingResistanceTorque { get; set; } = 30f;
|
||||
|
||||
/// <summary>
|
||||
/// The percentage this wheel is contributing to the total vehicle load bearing.
|
||||
/// </summary>
|
||||
public float LoadContribution { get; set; } = 0.25f;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum load the tire is rated for in [N].
|
||||
/// Used to calculate friction.Default value is adequate for most cars but
|
||||
/// larger and heavier vehicles such as semi trucks will use higher values.
|
||||
/// A good rule of the thumb is that this value should be 2x the Load
|
||||
/// while vehicle is stationary.
|
||||
/// </summary>
|
||||
[Property, Sync] public float LoadRating { get; set; } = 5400;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// The amount of torque returned by the wheel.
|
||||
/// Under no-slip conditions this will be equal to the torque that was input.
|
||||
/// When there is wheel spin, the value will be less than the input torque.
|
||||
/// </summary>
|
||||
public float CounterTorque { get; private set; }
|
||||
|
||||
[Property, Range( 0, 2 )] public float BrakeMult { get; set; } = 1f;
|
||||
public Friction ForwardFriction = new();
|
||||
public Friction SidewayFriction = new();
|
||||
public Vector3 FrictionForce;
|
||||
|
||||
|
||||
private Vector3 hitContactVelocity;
|
||||
private Vector3 hitForwardDirection;
|
||||
private Vector3 hitSidewaysDirection;
|
||||
|
||||
public Vector3 ContactRight => hitSidewaysDirection;
|
||||
public Vector3 ContactForward => hitForwardDirection;
|
||||
|
||||
[Sync]
|
||||
public float LongitudinalSlip
|
||||
{
|
||||
get => ForwardFriction.Slip;
|
||||
private set => ForwardFriction.Slip = value;
|
||||
}
|
||||
|
||||
public float LongitudinalSpeed => ForwardFriction.Speed;
|
||||
public bool IsSkiddingLongitudinally => NormalizedLongitudinalSlip > 0.35f;
|
||||
public float NormalizedLongitudinalSlip => Math.Clamp( Math.Abs( LongitudinalSlip ), 0, 1 );
|
||||
|
||||
|
||||
[Sync]
|
||||
public float LateralSlip
|
||||
{
|
||||
get => SidewayFriction.Slip;
|
||||
private set => SidewayFriction.Slip = value;
|
||||
}
|
||||
|
||||
public float LateralSpeed => SidewayFriction.Speed;
|
||||
public bool IsSkiddingLaterally => NormalizedLateralSlip > 0.35f;
|
||||
public float NormalizedLateralSlip => Math.Clamp( Math.Abs( LateralSlip ), 0, 1 );
|
||||
public bool IsSkidding => IsSkiddingLaterally || IsSkiddingLongitudinally;
|
||||
public float NormalizedSlip => (NormalizedLateralSlip + NormalizedLongitudinalSlip) / 2f;
|
||||
|
||||
private void UpdateHitVariables()
|
||||
{
|
||||
if ( IsOnGround )
|
||||
{
|
||||
hitContactVelocity = Vehicle.Body.GetVelocityAtPoint( ContactPosition + Vehicle.Body.MassCenter );
|
||||
|
||||
hitForwardDirection = ContactNormal.Cross( TransformRotationSteer.Right ).Normal;
|
||||
hitSidewaysDirection = Rotation.FromAxis( ContactNormal, 90f ) * hitForwardDirection;
|
||||
|
||||
ForwardFriction.Speed = hitContactVelocity.Dot( hitForwardDirection ).InchToMeter();
|
||||
SidewayFriction.Speed = hitContactVelocity.Dot( hitSidewaysDirection ).InchToMeter();
|
||||
}
|
||||
else
|
||||
{
|
||||
ForwardFriction.Speed = 0f;
|
||||
SidewayFriction.Speed = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private Vector3 lowSpeedReferencePosition;
|
||||
private bool lowSpeedReferenceIsSet;
|
||||
private Vector3 currentPosition;
|
||||
private Vector3 referenceError;
|
||||
private Vector3 correctiveForce;
|
||||
private void UpdateFriction( float dt )
|
||||
{
|
||||
var motorTorque = DriveTorque;
|
||||
var brakeTorque = BrakeTorque * BrakeMult;
|
||||
|
||||
float allWheelLoadSum = Vehicle.CombinedLoad;
|
||||
|
||||
LoadContribution = allWheelLoadSum == 0 ? 1f : Fz / allWheelLoadSum;
|
||||
|
||||
float mRadius = Radius;
|
||||
|
||||
float invDt = 1f / dt;
|
||||
float invRadius = 1f / mRadius;
|
||||
float inertia = Inertia;
|
||||
float invInertia = 1f / Inertia;
|
||||
|
||||
float loadClamped = Math.Clamp( Fz, 0, LoadRating );
|
||||
|
||||
float forwardLoadFactor = loadClamped * 1.35f;
|
||||
float sideLoadFactor = loadClamped * 1.9f;
|
||||
|
||||
float loadPercent = Math.Clamp( Fz / LoadRating, 0f, 1f );
|
||||
float slipLoadModifier = 1f - loadPercent * 0.4f;
|
||||
//DebugOverlay.Text( WorldPosition, SidewayFriction.Speed.ToString(), overlay: true );
|
||||
|
||||
|
||||
float mass = Vehicle.Body.Mass;
|
||||
float absForwardSpeed = Math.Abs( ForwardFriction.Speed );
|
||||
float forwardForceClamp = mass * LoadContribution * absForwardSpeed * invDt;
|
||||
float absSideSpeed = Math.Abs( SidewayFriction.Speed );
|
||||
float sideForceClamp = mass * LoadContribution * absSideSpeed * invDt;
|
||||
|
||||
float forwardSpeedClamp = 1.5f * (dt / 0.005f);
|
||||
forwardSpeedClamp = Math.Clamp( forwardSpeedClamp, 1.5f, 10f );
|
||||
float clampedAbsForwardSpeed = Math.Max( absForwardSpeed, forwardSpeedClamp );
|
||||
|
||||
// Calculate effect of camber on friction
|
||||
float camberFrictionCoeff = Math.Max( 0, Vehicle.WorldRotation.Up.Dot( ContactNormal ) );
|
||||
|
||||
float peakForwardFrictionForce = forwardLoadFactor;
|
||||
float absCombinedBrakeTorque = Math.Max( 0, brakeTorque + RollingResistanceTorque );
|
||||
|
||||
float signedCombinedBrakeTorque = absCombinedBrakeTorque * -Math.Sign( ForwardFriction.Speed );
|
||||
float signedCombinedBrakeForce = signedCombinedBrakeTorque * invRadius;
|
||||
float motorForce = motorTorque * invRadius;
|
||||
float forwardInputForce = motorForce + signedCombinedBrakeForce;
|
||||
float absMotorTorque = Math.Abs( motorTorque );
|
||||
float absBrakeTorque = Math.Abs( brakeTorque );
|
||||
|
||||
float maxForwardForce = Math.Min( peakForwardFrictionForce, forwardForceClamp );
|
||||
|
||||
maxForwardForce = absMotorTorque < absBrakeTorque ? maxForwardForce : peakForwardFrictionForce;
|
||||
ForwardFriction.Force = forwardInputForce > maxForwardForce ? maxForwardForce
|
||||
: forwardInputForce < -maxForwardForce ? -maxForwardForce : forwardInputForce;
|
||||
|
||||
bool wheelIsBlocked = false;
|
||||
if ( IsOnGround )
|
||||
{
|
||||
float combinedWheelForce = motorForce + absCombinedBrakeTorque * invRadius * -Math.Sign( AngularVelocity );
|
||||
float wheelForceClampOverflow = 0;
|
||||
if ( (combinedWheelForce >= 0 && AngularVelocity < 0) || (combinedWheelForce < 0 && AngularVelocity > 0) )
|
||||
{
|
||||
float absWheelForceClamp = Math.Abs( AngularVelocity ) * inertia * invRadius * invDt;
|
||||
float absCombinedWheelForce = combinedWheelForce < 0 ? -combinedWheelForce : combinedWheelForce;
|
||||
float wheelForceDiff = absCombinedWheelForce - absWheelForceClamp;
|
||||
wheelForceClampOverflow = Math.Max( 0, wheelForceDiff ) * Math.Sign( combinedWheelForce );
|
||||
combinedWheelForce = Math.Clamp( combinedWheelForce, -absWheelForceClamp, absWheelForceClamp );
|
||||
}
|
||||
AngularVelocity += combinedWheelForce * mRadius * invInertia * dt;
|
||||
|
||||
// Surface (corrective) force
|
||||
float noSlipAngularVelocity = ForwardFriction.Speed * invRadius;
|
||||
float angularVelocityError = AngularVelocity - noSlipAngularVelocity;
|
||||
|
||||
float angularVelocityCorrectionForce = Math.Clamp( -angularVelocityError * inertia * invRadius * invDt, -maxForwardForce, maxForwardForce );
|
||||
if ( absMotorTorque < absBrakeTorque && Math.Abs( wheelForceClampOverflow ) > Math.Abs( angularVelocityCorrectionForce ) )
|
||||
{
|
||||
wheelIsBlocked = true;
|
||||
AngularVelocity += ForwardFriction.Speed > 0 ? 1e-10f : -1e-10f;
|
||||
}
|
||||
else
|
||||
{
|
||||
AngularVelocity += angularVelocityCorrectionForce * mRadius * invInertia * dt;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
float maxBrakeTorque = AngularVelocity * inertia * invDt + motorTorque;
|
||||
maxBrakeTorque = maxBrakeTorque < 0 ? -maxBrakeTorque : maxBrakeTorque;
|
||||
float brakeTorqueSign = AngularVelocity < 0f ? -1f : 1f;
|
||||
float clampedBrakeTorque = Math.Clamp( absCombinedBrakeTorque, -maxBrakeTorque, maxBrakeTorque );
|
||||
AngularVelocity += (motorTorque - brakeTorqueSign * clampedBrakeTorque) * invInertia * dt;
|
||||
}
|
||||
|
||||
float absAngularVelocity = AngularVelocity < 0 ? -AngularVelocity : AngularVelocity;
|
||||
float maxCounterTorque = inertia * absAngularVelocity;
|
||||
CounterTorque = Math.Clamp( (signedCombinedBrakeForce - ForwardFriction.Force) * mRadius, -maxCounterTorque, maxCounterTorque );
|
||||
|
||||
|
||||
ForwardFriction.Slip = (ForwardFriction.Speed - AngularVelocity * mRadius) / clampedAbsForwardSpeed;
|
||||
ForwardFriction.Slip *= slipLoadModifier;
|
||||
|
||||
SidewayFriction.Slip = MathF.Atan2( SidewayFriction.Speed, clampedAbsForwardSpeed );
|
||||
SidewayFriction.Slip *= slipLoadModifier;
|
||||
|
||||
float sideSlipSign = SidewayFriction.Slip > 0 ? 1 : -1;
|
||||
float absSideSlip = Math.Abs( SidewayFriction.Slip );
|
||||
float peakSideFrictionForce = sideLoadFactor;
|
||||
|
||||
float sideForce = -sideSlipSign * Tire.Evaluate( absSideSlip ) * sideLoadFactor;
|
||||
SidewayFriction.Force = Math.Clamp( sideForce, -sideForceClamp, sideForceClamp );
|
||||
SidewayFriction.Force *= camberFrictionCoeff;
|
||||
|
||||
if ( IsOnGround && absForwardSpeed < 0.12f && absSideSpeed < 0.12f )
|
||||
{
|
||||
float verticalOffset = RestLength + mRadius;
|
||||
var transformPosition = WorldPosition;
|
||||
|
||||
var transformUp = TransformRotationSteer.Up;
|
||||
|
||||
currentPosition.x = transformPosition.x - transformUp.x * verticalOffset;
|
||||
currentPosition.y = transformPosition.y - transformUp.y * verticalOffset;
|
||||
currentPosition.z = transformPosition.z - transformUp.z * verticalOffset;
|
||||
|
||||
if ( !lowSpeedReferenceIsSet )
|
||||
{
|
||||
lowSpeedReferenceIsSet = true;
|
||||
lowSpeedReferencePosition = currentPosition;
|
||||
}
|
||||
else
|
||||
{
|
||||
referenceError.x = lowSpeedReferencePosition.x - currentPosition.x;
|
||||
referenceError.y = lowSpeedReferencePosition.y - currentPosition.y;
|
||||
referenceError.z = lowSpeedReferencePosition.z - currentPosition.z;
|
||||
|
||||
correctiveForce.x = invDt * LoadContribution * mass * referenceError.x;
|
||||
correctiveForce.y = invDt * LoadContribution * mass * referenceError.y;
|
||||
correctiveForce.z = invDt * LoadContribution * mass * referenceError.z;
|
||||
|
||||
if ( wheelIsBlocked && absAngularVelocity < 0.5f )
|
||||
{
|
||||
ForwardFriction.Force += correctiveForce.Dot( hitForwardDirection );
|
||||
}
|
||||
SidewayFriction.Force += correctiveForce.Dot( hitSidewaysDirection );
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
lowSpeedReferenceIsSet = false;
|
||||
}
|
||||
ForwardFriction.Force = Math.Clamp( ForwardFriction.Force, -peakForwardFrictionForce, peakForwardFrictionForce );
|
||||
|
||||
SidewayFriction.Force = Math.Clamp( SidewayFriction.Force, -peakSideFrictionForce, peakSideFrictionForce );
|
||||
|
||||
if ( absForwardSpeed > 2f || absAngularVelocity > 4f )
|
||||
{
|
||||
|
||||
float forwardSlipPercent = ForwardFriction.Slip / Tire.GetPeakSlip();
|
||||
float sideSlipPercent = SidewayFriction.Slip / Tire.GetPeakSlip();
|
||||
float slipCircleLimit = MathF.Sqrt( forwardSlipPercent * forwardSlipPercent + sideSlipPercent * sideSlipPercent );
|
||||
if ( slipCircleLimit > 1f )
|
||||
{
|
||||
float beta = MathF.Atan2( sideSlipPercent, forwardSlipPercent * 0.9f );
|
||||
float sinBeta = MathF.Sin( beta );
|
||||
float cosBeta = MathF.Cos( beta );
|
||||
|
||||
float absForwardForce = ForwardFriction.Force < 0 ? -ForwardFriction.Force : ForwardFriction.Force;
|
||||
|
||||
float absSideForce = SidewayFriction.Force < 0 ? -SidewayFriction.Force : SidewayFriction.Force;
|
||||
float f = absForwardForce * cosBeta * cosBeta + absSideForce * sinBeta * sinBeta;
|
||||
|
||||
ForwardFriction.Force = 0.5f * ForwardFriction.Force - f * cosBeta;
|
||||
SidewayFriction.Force = 0.5f * SidewayFriction.Force - f * sinBeta;
|
||||
}
|
||||
}
|
||||
|
||||
if ( IsOnGround )
|
||||
{
|
||||
FrictionForce.x = (hitSidewaysDirection.x * SidewayFriction.Force + hitForwardDirection.x * ForwardFriction.Force).MeterToInch();
|
||||
FrictionForce.y = (hitSidewaysDirection.y * SidewayFriction.Force + hitForwardDirection.y * ForwardFriction.Force).MeterToInch();
|
||||
FrictionForce.z = (hitSidewaysDirection.z * SidewayFriction.Force + hitForwardDirection.z * ForwardFriction.Force).MeterToInch();
|
||||
//DebugOverlay.Normal( WorldPosition, hitSidewaysDirection * 10, overlay: true );
|
||||
//DebugOverlay.Normal( WorldPosition, hitForwardDirection * 10, overlay: true );
|
||||
//DebugOverlay.Normal( WorldPosition, FrictionForce / 100, overlay: true );
|
||||
//DebugOverlay.Normal( ContactPosition, Vector3.Up * AngularVelocity, overlay: true );
|
||||
|
||||
//DebugOverlay.Sphere( new( ContactPosition, 4 ), overlay: true );
|
||||
//Vehicle.Body.ApplyForceAt( ContactPosition, FrictionForce );
|
||||
|
||||
}
|
||||
else
|
||||
FrictionForce = Vector3.Zero;
|
||||
}
|
||||
}
|
||||
@ -5,9 +5,7 @@ namespace VeloX;
|
||||
|
||||
public partial class VeloXWheel : Component
|
||||
{
|
||||
protected override void DrawGizmos() => GizmoDraw();
|
||||
|
||||
public void GizmoDraw()
|
||||
protected override void DrawGizmos()
|
||||
{
|
||||
|
||||
if ( !Gizmo.IsSelected )
|
||||
@ -20,7 +18,7 @@ public partial class VeloXWheel : Component
|
||||
//
|
||||
{
|
||||
var suspensionStart = Vector3.Zero;
|
||||
var suspensionEnd = Vector3.Zero + Vector3.Down * RestLength.MeterToInch();
|
||||
var suspensionEnd = Vector3.Zero + Vector3.Down * SuspensionLength;
|
||||
|
||||
Gizmo.Draw.Color = Color.Cyan;
|
||||
Gizmo.Draw.LineThickness = 0.25f;
|
||||
@ -30,7 +28,7 @@ public partial class VeloXWheel : Component
|
||||
Gizmo.Draw.Line( suspensionStart + Vector3.Forward, suspensionStart + Vector3.Backward );
|
||||
Gizmo.Draw.Line( suspensionEnd + Vector3.Forward, suspensionEnd + Vector3.Backward );
|
||||
}
|
||||
var widthOffset = Vector3.Right * Width.MeterToInch() * 0.5f;
|
||||
var widthOffset = Vector3.Right * Width * 0.5f;
|
||||
//
|
||||
// Wheel radius
|
||||
//
|
||||
@ -38,7 +36,7 @@ public partial class VeloXWheel : Component
|
||||
Gizmo.Draw.LineThickness = 0.5f;
|
||||
Gizmo.Draw.Color = Color.White;
|
||||
|
||||
Gizmo.Draw.LineCylinder( widthOffset, -widthOffset, Radius.MeterToInch(), Radius.MeterToInch(), 16 );
|
||||
Gizmo.Draw.LineCylinder( widthOffset, -widthOffset, Radius, Radius, 16 );
|
||||
}
|
||||
|
||||
//
|
||||
@ -53,14 +51,24 @@ public partial class VeloXWheel : Component
|
||||
for ( float i = 0; i < 16; i++ )
|
||||
{
|
||||
|
||||
var pos = circlePosition + Vector3.Up.RotateAround( Vector3.Zero, new Angles( i / 16 * 360, 0, 0 ) ) * Radius.MeterToInch();
|
||||
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.MeterToInch();
|
||||
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 );
|
||||
//}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,91 +0,0 @@
|
||||
using Sandbox;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace VeloX;
|
||||
|
||||
|
||||
public partial class VeloXWheel
|
||||
{
|
||||
|
||||
[ConCmd( "clear_skids" )]
|
||||
public static void ClearSkids()
|
||||
{
|
||||
while ( SkidMarks.Count > 1 )
|
||||
SkidMarks.Dequeue()?.DestroyGameObject();
|
||||
}
|
||||
|
||||
[ConVar( "skidmark_max_skid" )]
|
||||
public static float MaxSkid { get; set; } = 50.0f;
|
||||
|
||||
[ConVar( "skidmark_min_slide" )]
|
||||
public static float MinSlide { get; set; } = 0.1f;
|
||||
|
||||
|
||||
private static readonly Queue<LineRenderer> SkidMarks = [];
|
||||
|
||||
private LineRenderer _skidMark;
|
||||
|
||||
private void ResetSkid()
|
||||
{
|
||||
_skidMark = null;
|
||||
}
|
||||
|
||||
private void CreateSkid()
|
||||
{
|
||||
GameObject go = new()
|
||||
{
|
||||
WorldPosition = ContactPosition + Vehicle.WorldRotation.Down * (Radius.MeterToInch() - 0.01f),
|
||||
WorldRotation = Rotation.LookAt( hitSidewaysDirection )
|
||||
};
|
||||
_skidMark = go.AddComponent<LineRenderer>();
|
||||
_skidMark.Face = SceneLineObject.FaceMode.Normal;
|
||||
_skidMark.Points = [go];
|
||||
_skidMark.Color = Color.Black.WithAlpha( 0.5f );
|
||||
_skidMark.Opaque = false;
|
||||
_skidMark.CastShadows = false;
|
||||
_skidMark.Width = Width.MeterToInch() / 2;
|
||||
_skidMark.AutoCalculateNormals = false;
|
||||
_skidMark.SplineInterpolation = 4;
|
||||
go.Flags = go.Flags.WithFlag( GameObjectFlags.Hidden, true );
|
||||
go.Flags = go.Flags.WithFlag( GameObjectFlags.NotNetworked, true );
|
||||
SkidMarks.Enqueue( _skidMark );
|
||||
}
|
||||
|
||||
protected void UpdateSkid()
|
||||
{
|
||||
if ( IsProxy )
|
||||
return;
|
||||
while ( SkidMarks.Count > MaxSkid )
|
||||
{
|
||||
SkidMarks.Dequeue()?.DestroyGameObject();
|
||||
}
|
||||
|
||||
if ( !IsOnGround )
|
||||
{
|
||||
ResetSkid();
|
||||
return;
|
||||
}
|
||||
|
||||
var slideAmount = NormalizedSlip;
|
||||
|
||||
if ( slideAmount < MinSlide * 2 )
|
||||
{
|
||||
ResetSkid();
|
||||
return;
|
||||
}
|
||||
|
||||
if ( !_skidMark.IsValid() )
|
||||
{
|
||||
CreateSkid();
|
||||
}
|
||||
GameObject go = new()
|
||||
{
|
||||
Parent = _skidMark.GameObject,
|
||||
WorldPosition = ContactPosition + Vehicle.WorldRotation.Down * Radius.MeterToInch(),
|
||||
WorldRotation = Rotation.LookAt( ContactNormal.RotateAround( Vector3.Zero, Rotation.FromRoll( 90 ) ) )
|
||||
};
|
||||
go.Flags = go.Flags.WithFlag( GameObjectFlags.Hidden, true );
|
||||
go.Flags = go.Flags.WithFlag( GameObjectFlags.NotNetworked, true );
|
||||
_skidMark.Points.Add( go );
|
||||
}
|
||||
}
|
||||
@ -1,108 +0,0 @@
|
||||
using Sandbox;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
namespace VeloX;
|
||||
|
||||
|
||||
public partial class VeloXWheel
|
||||
{
|
||||
private static readonly GameObject SmokePrefab = GameObject.GetPrefab( "prefabs/particles/tire_smoke.prefab" );
|
||||
|
||||
private GameObject SmokeObject { get; set; }
|
||||
public const float MIN_DRIFT_ANGLE = 10f;
|
||||
public const float MIN_DRIFT_SPEED = 30f;
|
||||
public const float MAX_DRIFT_ANGLE = 110f;
|
||||
private float smokeMul;
|
||||
private float timeMul;
|
||||
protected override void OnStart()
|
||||
{
|
||||
base.OnStart();
|
||||
if ( IsProxy )
|
||||
return;
|
||||
|
||||
if ( !SmokeObject.IsValid() )
|
||||
SmokeObject = GameObject.GetPrefab( "prefabs/particles/tire_smoke.prefab" ).Clone( new CloneConfig() { Parent = GameObject, StartEnabled = true } );
|
||||
}
|
||||
public float GetSlip()
|
||||
{
|
||||
if ( !IsOnGround )
|
||||
return 0;
|
||||
var val = Math.Abs( LateralSlip ) + Math.Abs( LongitudinalSlip );
|
||||
timeMul = timeMul.LerpTo( val, 0.1f );
|
||||
|
||||
//if ( timeMul > 2 )
|
||||
// return val;
|
||||
return val * 5;
|
||||
}
|
||||
protected override void OnUpdate()
|
||||
{
|
||||
base.OnUpdate();
|
||||
if ( IsProxy )
|
||||
return;
|
||||
UpdateSkid();
|
||||
SmokeObject.WorldPosition = ContactPosition + Vehicle.WorldRotation.Down * Radius.MeterToInch();
|
||||
smokeMul = Math.Max( 0, GetSlip() - 3 );
|
||||
|
||||
bool density = false;
|
||||
|
||||
if ( smokeMul > 0 )
|
||||
density = true;
|
||||
var emitter = SmokeObject.Components.Get<ParticleSphereEmitter>( FindMode.EverythingInSelfAndDescendants );
|
||||
emitter.Enabled = density;
|
||||
emitter.Radius = 0f;
|
||||
emitter.Velocity = 0;
|
||||
emitter.Duration = 5;
|
||||
emitter.Burst = 5;
|
||||
float sizeMul = 0.7f + smokeMul * 0.3f;
|
||||
emitter.Rate = smokeMul * 100f;
|
||||
emitter.RateOverDistance = smokeMul * 10f;
|
||||
|
||||
var effect = SmokeObject.Components.Get<ParticleEffect>( FindMode.EverythingInSelfAndDescendants );
|
||||
|
||||
effect.MaxParticles = 500;
|
||||
effect.Damping = 0.9f;
|
||||
effect.ApplyRotation = true;
|
||||
effect.ApplyShape = true;
|
||||
|
||||
effect.Roll = new()
|
||||
{
|
||||
Type = ParticleFloat.ValueType.Range,
|
||||
Evaluation = ParticleFloat.EvaluationType.Seed,
|
||||
ConstantA = 0,
|
||||
ConstantB = 360,
|
||||
};
|
||||
effect.Scale = new()
|
||||
{
|
||||
Type = ParticleFloat.ValueType.Curve,
|
||||
Evaluation = ParticleFloat.EvaluationType.Life,
|
||||
CurveA = new( new List<Curve.Frame>() { new( 0, 10f ), new( 0.8f, 50f ), new( 1f, 160f ) } ),
|
||||
};
|
||||
effect.StartDelay = 0.025f + (1 - smokeMul) * 0.03f;
|
||||
|
||||
effect.ApplyColor = true;
|
||||
effect.Gradient = new()
|
||||
{
|
||||
Type = ParticleGradient.ValueType.Range,
|
||||
Evaluation = ParticleGradient.EvaluationType.Particle,
|
||||
ConstantA = Color.White,
|
||||
ConstantB = Color.Transparent,
|
||||
};
|
||||
effect.StartVelocity = new()
|
||||
{
|
||||
Type = ParticleFloat.ValueType.Range,
|
||||
Evaluation = ParticleFloat.EvaluationType.Seed,
|
||||
ConstantA = 10,
|
||||
ConstantB = 70,
|
||||
};
|
||||
effect.Force = true;
|
||||
effect.InitialVelocity = Vehicle.Body.Velocity / 3 + hitForwardDirection * LongitudinalSlip * 10f;
|
||||
effect.ForceDirection = 0;
|
||||
effect.SheetSequence = true;
|
||||
effect.SequenceSpeed = 0.5f;
|
||||
effect.SequenceTime = 1f;
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,16 @@
|
||||
using Sandbox;
|
||||
using Sandbox.Rendering;
|
||||
using Sandbox.Services;
|
||||
using Sandbox.UI;
|
||||
using System;
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Numerics;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using static Sandbox.CameraComponent;
|
||||
using static Sandbox.Package;
|
||||
using static Sandbox.SkinnedModelRenderer;
|
||||
|
||||
namespace VeloX;
|
||||
|
||||
@ -7,56 +18,72 @@ namespace VeloX;
|
||||
[Title( "VeloX - Wheel" )]
|
||||
public partial class VeloXWheel : Component
|
||||
{
|
||||
[Property, Group( "Suspension" )] public float Radius { get; set; } = 0.35f;
|
||||
[Property, Group( "Suspension" )] public float Width { get; set; } = 0.1f;
|
||||
[Property] public float Mass { get; set; } = 5;
|
||||
[Property, Group( "Suspension" )] float RestLength { get; set; } = 0.22f;
|
||||
[Property, Group( "Suspension" )] public float SpringStiffness { get; set; } = 20000.0f;
|
||||
[Property, Group( "Suspension" )] float ReboundStiffness { get; set; } = 2200;
|
||||
[Property, Group( "Suspension" )] float CompressionStiffness { get; set; } = 2400;
|
||||
[Property, Group( "Suspension" )] float BumpStopStiffness { get; set; } = 5000;
|
||||
|
||||
[Property, Group( "Traction" )] public TirePreset Tire { get; set; } = ResourceLibrary.Get<TirePreset>( "frictions/default.tire" );
|
||||
[Property, Group( "Traction" )] public float SurfaceGrip { get; set; } = 1f;
|
||||
[Property, Group( "Traction" )] public float SurfaceResistance { get; set; } = 0.05f;
|
||||
public bool AutoSimulate = true;
|
||||
|
||||
|
||||
public float BaseInertia => Mass * (Radius * Radius); // kg·m²
|
||||
[Property] public float Inertia { get; set; } = 1.5f; // kg·m²
|
||||
[Property] public float Radius { get; set; } = 15;
|
||||
[Property] public float Mass { get; set; } = 20;
|
||||
[Property] public float RollingResistance { get; set; } = 20;
|
||||
[Property] public float SlipCircleShape { get; set; } = 1.05f;
|
||||
[Property] public TirePreset TirePreset { get; set; }
|
||||
[Property] public float Width { get; set; } = 6;
|
||||
public float SideSlip { get; private set; }
|
||||
public float ForwardSlip { get; private set; }
|
||||
[Sync] public float Torque { get; set; }
|
||||
[Sync, Range( 0, 1 )] public float Brake { get; set; }
|
||||
[Property] float BrakePowerMax { get; set; } = 3000;
|
||||
[Property] public bool IsFront { get; protected set; }
|
||||
[Property] public float SteerMultiplier { get; set; }
|
||||
[Property] public float CasterAngle { get; set; } = 7; // todo
|
||||
[Property] public float CamberAngle { get; set; } = -3;
|
||||
[Property] public float ToeAngle { get; set; } = 0.5f;
|
||||
[Property] public float Ackermann { get; set; } = 0;
|
||||
|
||||
public float RPM { get => AngularVelocity * 60f / MathF.Tau; set => AngularVelocity = value / (60 / MathF.Tau); }
|
||||
[Property, Group( "Suspension" )] float SuspensionLength { get; set; } = 10;
|
||||
[Property, Group( "Suspension" )] float SpringStrength { get; set; } = 800;
|
||||
[Property, Group( "Suspension" )] float SpringDamper { get; set; } = 3000;
|
||||
|
||||
[Property] public bool AutoPhysics { get; set; } = true;
|
||||
|
||||
public float Spin { get; private set; }
|
||||
|
||||
public float RPM { get => angularVelocity * 30f / MathF.PI; set => angularVelocity = value / (30f / MathF.PI); }
|
||||
public float AngularVelocity { get => angularVelocity; set => angularVelocity = value; }
|
||||
|
||||
internal float DistributionFactor { get; set; }
|
||||
|
||||
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; }
|
||||
public SceneTraceResult Trace { get; private set; }
|
||||
public bool IsOnGround => Trace.Hit;
|
||||
|
||||
private float lastSpringOffset;
|
||||
private float angularVelocity;
|
||||
private float load;
|
||||
private float lastFraction;
|
||||
|
||||
[Property] public float DriveTorque { get; set; }
|
||||
[Property] public float BrakeTorque { get; set; }
|
||||
private Vector3 contactPos;
|
||||
private Vector3 forward;
|
||||
private Vector3 right;
|
||||
private Vector3 up;
|
||||
|
||||
public float Compression { get; protected set; } // meters
|
||||
public float LastLength { get; protected set; } // meters
|
||||
public float Fz { get; protected set; } // N
|
||||
public float AngularVelocity { get; protected set; } // rad/s
|
||||
public float RollAngle { get; protected set; } // degrees
|
||||
private float forwardFriction;
|
||||
private float sideFriction;
|
||||
private Vector3 force;
|
||||
public float CounterTorque { get; private set; }
|
||||
|
||||
internal float BaseInertia => 0.5f * Mass * MathF.Pow( Radius.InchToMeter(), 2 );
|
||||
public float Inertia
|
||||
{
|
||||
get => BaseInertia + inertia;
|
||||
set => inertia = value;
|
||||
}
|
||||
|
||||
private VeloXBase Vehicle;
|
||||
[Sync] public Vector3 ContactNormal { get; protected set; }
|
||||
[Sync] public Vector3 ContactPosition { get; protected set; }
|
||||
Rotation TransformRotationSteer => Vehicle.WorldTransform.RotationToWorld( Vehicle.SteerAngle * SteerMultiplier );
|
||||
|
||||
protected override void OnAwake()
|
||||
{
|
||||
Vehicle = Components.Get<VeloXBase>( FindMode.EverythingInSelfAndAncestors );
|
||||
base.OnAwake();
|
||||
if ( StartPos.IsNearZeroLength )
|
||||
StartPos = LocalPosition;
|
||||
Inertia = BaseInertia;
|
||||
}
|
||||
|
||||
internal void Update( VeloXBase vehicle, in float dt )
|
||||
@ -66,159 +93,224 @@ public partial class VeloXWheel : Component
|
||||
|
||||
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();
|
||||
Spin -= angularVelocity.RadianToDegree() * dt;
|
||||
WorldRotation = vehicle.WorldTransform.RotationToWorld( GetSteer( vehicle.SteerAngle.yaw ) ) * Rotation.FromAxis( Vector3.Right, Spin );
|
||||
}
|
||||
|
||||
private struct WheelTraceData
|
||||
private Rotation GetSteer( float steer )
|
||||
{
|
||||
internal Vector3 ContactNormal;
|
||||
internal Vector3 ContactPosition;
|
||||
internal float Compression;
|
||||
internal float Force;
|
||||
}
|
||||
public void UpdateForce()
|
||||
{
|
||||
Vehicle.Body.ApplyForceAt( ContactPosition, FrictionForce + ContactNormal * Fz.MeterToInch() );
|
||||
|
||||
float angle = (-steer * SteerMultiplier).DegreeToRadian();
|
||||
|
||||
float t = MathF.Tan( (MathF.PI / 2) - angle ) - Ackermann;
|
||||
float steering_angle = MathF.CopySign( float.Pi / 2, t ) - MathF.Atan( t );
|
||||
var steering_axis = Vector3.Up * MathF.Cos( -CasterAngle.DegreeToRadian() ) +
|
||||
Vector3.Right * MathF.Sin( -CasterAngle.DegreeToRadian() );
|
||||
|
||||
return Rotation.FromAxis( Vector3.Forward, -CamberAngle ) * Rotation.FromAxis( steering_axis, steering_angle.RadianToDegree() );
|
||||
}
|
||||
|
||||
internal void StepPhys( VeloXBase vehicle, in float dt )
|
||||
|
||||
private static float GetLongitudinalLoadCoefficient( float load ) => 11000 * (1 - MathF.Exp( -0.00014f * load ));
|
||||
private static float GetLateralLoadCoefficient( float load ) => 18000 * (1 - MathF.Exp( -0.0001f * load ));
|
||||
private float inertia;
|
||||
|
||||
|
||||
private (float, float, float, float) StepLongitudinal( float Tm, float Tb, float Vx, float W, float Lc, float R, float I )
|
||||
{
|
||||
|
||||
const int numSamples = 3;
|
||||
float halfWidth = Width.MeterToInch() * 0.5f;
|
||||
|
||||
int hitCount = 0;
|
||||
WheelTraceData wheelTraceData = new();
|
||||
for ( int i = 0; i < numSamples; i++ )
|
||||
float wInit = W;
|
||||
float vxAbs = Math.Abs( Vx );
|
||||
float Sx;
|
||||
if ( Lc < 0.01f )
|
||||
{
|
||||
float t = (float)i / (numSamples - 1);
|
||||
float offset = MathX.Lerp( -halfWidth, halfWidth, t );
|
||||
Vector3 start = vehicle.WorldTransform.PointToWorld( StartPos + Vector3.Right * offset );
|
||||
Vector3 end = start + vehicle.WorldRotation.Down * RestLength.MeterToInch();
|
||||
if ( TraceWheel( vehicle, ref wheelTraceData, start, end, Width.MeterToInch() / numSamples, dt ) )
|
||||
hitCount++;
|
||||
Sx = 0;
|
||||
}
|
||||
|
||||
|
||||
if ( hitCount > 0 )
|
||||
else if ( vxAbs >= 0.01f )
|
||||
{
|
||||
|
||||
IsOnGround = true;
|
||||
|
||||
Fz = Math.Max( wheelTraceData.Force / hitCount, 0 );
|
||||
Compression = wheelTraceData.Compression / hitCount;
|
||||
ContactNormal = (wheelTraceData.ContactNormal / hitCount).Normal;
|
||||
ContactPosition = wheelTraceData.ContactPosition / hitCount;
|
||||
//DoSuspensionSounds( vehicle, (RestLength - Compression) * 0.8f);
|
||||
LastLength = RestLength - Compression;
|
||||
|
||||
UpdateHitVariables();
|
||||
UpdateFriction( dt );
|
||||
Sx = (W * R - Vx) / vxAbs;
|
||||
}
|
||||
else
|
||||
{
|
||||
IsOnGround = false;
|
||||
// Wheel is off the ground
|
||||
Compression = 0f;
|
||||
Fz = 0f;
|
||||
ContactNormal = Vector3.Up;
|
||||
ContactPosition = WorldPosition;
|
||||
Sx = (W * R - Vx) * 0.6f;
|
||||
}
|
||||
|
||||
Sx = Math.Clamp( Sx, -1, 1 );
|
||||
|
||||
W += Tm / I * Time.Delta;
|
||||
|
||||
Tb *= W > 0 ? -1 : 1;
|
||||
|
||||
float tbCap = Math.Abs( W ) * I / Time.Delta;
|
||||
float tbr = Math.Abs( Tb ) - Math.Abs( tbCap );
|
||||
tbr = Math.Max( tbr, 0 );
|
||||
|
||||
Tb = Math.Clamp( Tb, -tbCap, tbCap );
|
||||
|
||||
W += Tb / I * Time.Delta;
|
||||
|
||||
float maxTorque = TirePreset.Pacejka.PacejkaFx( Math.Abs( Sx ) ) * Lc * R;
|
||||
|
||||
float errorTorque = (W - Vx / R) * I / Time.Delta;
|
||||
float surfaceTorque = Math.Clamp( errorTorque, -maxTorque, maxTorque );
|
||||
|
||||
W -= surfaceTorque / I * Time.Delta;
|
||||
float Fx = surfaceTorque / R;
|
||||
|
||||
tbr *= (W > 0 ? -1 : 1);
|
||||
float TbCap2 = Math.Abs( W ) * I / Time.Delta;
|
||||
float Tb2 = Math.Clamp( tbr, -TbCap2, TbCap2 );
|
||||
W += Tb2 / I * Time.Delta;
|
||||
|
||||
float deltaOmegaTorque = (W - wInit) * I / Time.Delta;
|
||||
float Tcnt = -surfaceTorque + Tb + Tb2 - deltaOmegaTorque;
|
||||
|
||||
return (W, Sx, Fx, Tcnt);
|
||||
}
|
||||
|
||||
const string playerTag = "player";
|
||||
|
||||
private bool TraceWheel( VeloXBase vehicle, ref WheelTraceData wheelTraceData, Vector3 start, Vector3 end, float width, in float dt )
|
||||
private void StepLateral( float Vx, float Vy, float Lc, out float Sy, out float Fy )
|
||||
{
|
||||
float VxAbs = Math.Abs( Vx );
|
||||
|
||||
var trace = Scene.Trace
|
||||
.FromTo( start, end )
|
||||
.Cylinder( width, Radius.MeterToInch() )
|
||||
.Rotated( vehicle.WorldRotation * CylinderOffset )
|
||||
.UseHitPosition( false )
|
||||
.IgnoreGameObjectHierarchy( Vehicle.GameObject )
|
||||
.WithoutTags( playerTag )
|
||||
.Run();
|
||||
|
||||
//DebugOverlay.Trace( trace, overlay: true );
|
||||
if ( trace.Hit )
|
||||
if ( Lc < 0.01f )
|
||||
{
|
||||
Sy = 0;
|
||||
}
|
||||
else if ( VxAbs > 0.1f )
|
||||
{
|
||||
|
||||
Vector3 contactPos = trace.EndPosition;
|
||||
Vector3 contactNormal = trace.Normal;
|
||||
float currentLength = trace.Distance.InchToMeter();
|
||||
float compression = (RestLength - currentLength).Clamp( -RestLength, RestLength );
|
||||
Sy = MathX.RadianToDegree( MathF.Atan( Vy / VxAbs ) ) / 50;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
// Nonlinear spring
|
||||
float springForce = SpringStiffness * compression * (1f + 2f * compression / RestLength);
|
||||
|
||||
// Damping
|
||||
float damperVelocity = (LastLength - currentLength) / dt;
|
||||
float damperForce = damperVelocity > 0 ? damperVelocity * ReboundStiffness : damperVelocity * CompressionStiffness;
|
||||
|
||||
float FzPoint = springForce + damperForce;
|
||||
|
||||
// Bump stop
|
||||
float minCompression = -0.05f;
|
||||
if ( compression <= minCompression ) FzPoint += (minCompression - compression) * BumpStopStiffness;
|
||||
|
||||
wheelTraceData.ContactNormal += contactNormal;
|
||||
wheelTraceData.ContactPosition += contactPos;
|
||||
wheelTraceData.Compression += compression;
|
||||
wheelTraceData.Force += Math.Max( 0, FzPoint );
|
||||
|
||||
return true;
|
||||
Sy = Vy * (0.003f / Time.Delta);
|
||||
}
|
||||
|
||||
return false;
|
||||
Sy = Math.Clamp( Sy, -1, 1 );
|
||||
float slipSign = Sy < 0 ? -1 : 1;
|
||||
Fy = -slipSign * TirePreset.Pacejka.PacejkaFy( Math.Abs( Sy ) ) * Lc;
|
||||
}
|
||||
|
||||
|
||||
public void DoPhysics( in float dt )
|
||||
private void SlipCircle( float Sx, float Sy, float Fx, ref float Fy )
|
||||
{
|
||||
if ( IsProxy )
|
||||
float SxAbs = Math.Abs( Sx );
|
||||
if ( SxAbs > 0.01f )
|
||||
{
|
||||
float SxClamped = Math.Clamp( Sx, -1, 1 );
|
||||
float SyClamped = Math.Clamp( Sy, -1, 1 );
|
||||
Vector2 combinedSlip = new( SxClamped * 1.05f, SyClamped );
|
||||
Vector2 slipDir = combinedSlip.Normal;
|
||||
|
||||
float F = MathF.Sqrt( Fx * Fx + Fy * Fy );
|
||||
float absSlipDirY = Math.Abs( slipDir.y );
|
||||
|
||||
Fy = F * absSlipDirY * (Fy < 0 ? -1 : 1);
|
||||
}
|
||||
}
|
||||
|
||||
public void DoPhysics( VeloXBase vehicle )
|
||||
{
|
||||
var pos = vehicle.WorldTransform.PointToWorld( StartPos );
|
||||
|
||||
var ang = vehicle.WorldTransform.RotationToWorld( GetSteer( vehicle.SteerAngle.yaw ) );
|
||||
|
||||
var maxLen = SuspensionLength;
|
||||
|
||||
var endPos = pos + ang.Down * maxLen;
|
||||
Trace = Scene.Trace
|
||||
.IgnoreGameObjectHierarchy( vehicle.GameObject )
|
||||
.Cylinder( Width, Radius, pos, endPos )
|
||||
.Rotated( vehicle.WorldTransform.Rotation * CylinderOffset )
|
||||
.UseRenderMeshes( false )
|
||||
.UseHitPosition( true )
|
||||
.WithoutTags( vehicle.WheelIgnoredTags )
|
||||
.Run();
|
||||
|
||||
forward = Vector3.VectorPlaneProject( ang.Forward, Trace.Normal );
|
||||
right = Vector3.VectorPlaneProject( ang.Right, Trace.Normal );
|
||||
|
||||
var fraction = Trace.Fraction;
|
||||
|
||||
contactPos = pos - maxLen * fraction * ang.Up;
|
||||
|
||||
LocalPosition = vehicle.WorldTransform.PointToLocal( contactPos );
|
||||
|
||||
DoSuspensionSounds( vehicle, fraction - lastFraction );
|
||||
lastFraction = fraction;
|
||||
|
||||
if ( !IsOnGround )
|
||||
return;
|
||||
if ( AutoSimulate )
|
||||
StepPhys( Vehicle, dt );
|
||||
StepRotation( Vehicle, dt );
|
||||
|
||||
var vel = vehicle.Body.GetVelocityAtPoint( contactPos );
|
||||
|
||||
var offset = maxLen - (fraction * maxLen);
|
||||
var springForce = offset * SpringStrength;
|
||||
var damperForce = (lastSpringOffset - offset) * SpringDamper;
|
||||
|
||||
lastSpringOffset = offset;
|
||||
|
||||
|
||||
var velU = Trace.Normal.Dot( vel );
|
||||
|
||||
if ( velU < 0 && offset + Math.Abs( velU * Time.Delta ) > SuspensionLength )
|
||||
{
|
||||
vehicle.Body.CalculateVelocityOffset( -velU / Time.Delta * Trace.Normal, pos, out var linearImp, out var angularImp );
|
||||
|
||||
vehicle.Body.Velocity += linearImp;
|
||||
vehicle.Body.AngularVelocity += angularImp;
|
||||
vehicle.Body.CalculateVelocityOffset( Trace.HitPosition - (contactPos + Trace.Normal * velU * Time.Delta), pos, out var lin, out _ );
|
||||
|
||||
vehicle.WorldPosition += lin / Time.Delta;
|
||||
damperForce = 0;
|
||||
|
||||
}
|
||||
|
||||
force = (springForce - damperForce) * Trace.Normal;
|
||||
|
||||
load = Math.Max( springForce - damperForce, 0 );
|
||||
float R = Radius.InchToMeter();
|
||||
|
||||
float forwardSpeed = vel.Dot( forward ).InchToMeter();
|
||||
float sideSpeed = vel.Dot( right ).InchToMeter();
|
||||
|
||||
float longitudinalLoadCoefficient = GetLongitudinalLoadCoefficient( load );
|
||||
float lateralLoadCoefficient = GetLateralLoadCoefficient( load );
|
||||
|
||||
float F_roll = TirePreset.GetRollingResistance( angularVelocity * R, 1.0f ) * 10000;
|
||||
|
||||
( float W, float Sx, float Fx, float counterTq) = StepLongitudinal(
|
||||
Torque,
|
||||
Brake * BrakePowerMax + F_roll,
|
||||
forwardSpeed,
|
||||
angularVelocity,
|
||||
longitudinalLoadCoefficient,
|
||||
R,
|
||||
Inertia
|
||||
);
|
||||
|
||||
StepLateral( forwardSpeed, sideSpeed, lateralLoadCoefficient, out float Sy, out float Fy );
|
||||
|
||||
SlipCircle( Sx, Sy, Fx, ref Fy );
|
||||
|
||||
CounterTorque = counterTq;
|
||||
angularVelocity = W;
|
||||
|
||||
force += forward * Fx;
|
||||
force += right * Fy * Math.Clamp( vehicle.TotalSpeed * 0.005f, 0, 1 );
|
||||
ForwardSlip = Sx;
|
||||
SideSlip = Sy * Math.Clamp( vehicle.TotalSpeed * 0.005f, 0, 1 );
|
||||
|
||||
vehicle.Body.ApplyForceAt( pos, force / Time.Delta );
|
||||
|
||||
}
|
||||
|
||||
const float HubCoulombNm = 20f;
|
||||
const float HubViscous = 0.1f;
|
||||
private void StepRotation( VeloXBase vehicle, in float dt )
|
||||
#if DEBUG
|
||||
protected override void OnUpdate()
|
||||
{
|
||||
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;
|
||||
DebugOverlay.Normal( contactPos, forward * forwardFriction, Color.Red, overlay: true );
|
||||
DebugOverlay.Normal( contactPos, right * sideFriction, Color.Green, overlay: true );
|
||||
DebugOverlay.Normal( contactPos, up * force / 1000f, Color.Blue, overlay: true );
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
@ -1,58 +0,0 @@
|
||||
using Sandbox;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
namespace VeloX;
|
||||
|
||||
internal sealed class WheelManager : GameObjectSystem
|
||||
{
|
||||
|
||||
|
||||
public WheelManager( Scene scene ) : base( scene )
|
||||
{
|
||||
Listen( Stage.StartFixedUpdate, -99, UpdateWheels, "UpdateWheels" );
|
||||
Listen( Stage.StartFixedUpdate, -100, UpdateEngine, "UpdateEngine" );
|
||||
}
|
||||
|
||||
private void UpdateWheels()
|
||||
{
|
||||
if ( !Game.IsPlaying )
|
||||
return;
|
||||
//Stopwatch sw = Stopwatch.StartNew();
|
||||
|
||||
var wheels = Scene.GetAll<VeloXWheel>();
|
||||
if ( !wheels.Any() ) return;
|
||||
|
||||
var timeDelta = Time.Delta;
|
||||
Sandbox.Utility.Parallel.ForEach( wheels, item =>
|
||||
{
|
||||
if ( !item.IsProxy )
|
||||
item.DoPhysics( timeDelta );
|
||||
} );
|
||||
foreach ( var wheel in wheels )
|
||||
wheel.UpdateForce();
|
||||
|
||||
//sw.Stop();
|
||||
|
||||
//DebugOverlaySystem.Current.ScreenText( new Vector2( 120, 30 ), $"Wheel Sim: {sw.Elapsed.TotalMilliseconds,6:F2} ms", 24 );
|
||||
}
|
||||
private void UpdateEngine()
|
||||
{
|
||||
if ( !Game.IsPlaying )
|
||||
return;
|
||||
//Stopwatch sw = Stopwatch.StartNew();
|
||||
|
||||
var engines = Scene.GetAll<Engine>();
|
||||
if ( !engines.Any() ) return;
|
||||
|
||||
var timeDelta = Time.Delta;
|
||||
Sandbox.Utility.Parallel.ForEach( engines, item =>
|
||||
{
|
||||
if ( !item.IsProxy )
|
||||
item.UpdateEngine( timeDelta );
|
||||
} );
|
||||
//sw.Stop();
|
||||
|
||||
//DebugOverlaySystem.Current.ScreenText( new Vector2( 120, 54 ), $"Engine Sim: {sw.Elapsed.TotalMilliseconds,6:F2} ms", 24 );
|
||||
}
|
||||
}
|
||||
@ -1,60 +0,0 @@
|
||||
using Sandbox;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace VeloX;
|
||||
|
||||
public partial class VeloXCar
|
||||
{
|
||||
|
||||
private class DownforcePoint
|
||||
{
|
||||
[KeyProperty] public float MaxForce { get; set; }
|
||||
[KeyProperty] public Vector3 Position { get; set; }
|
||||
}
|
||||
public const float RHO = 1.225f;
|
||||
[Property, Feature( "Aerodinamics" )] public Vector3 Dimensions = new( 2f, 4.5f, 1.5f );
|
||||
[Property, Feature( "Aerodinamics" )] public float FrontalCd { get; set; } = 0.35f;
|
||||
[Property, Feature( "Aerodinamics" )] public float SideCd { get; set; } = 1.05f;
|
||||
[Property, Feature( "Aerodinamics" )] public float MaxDownforceSpeed { get; set; } = 80f;
|
||||
[Property, Feature( "Aerodinamics" )] private List<DownforcePoint> DownforcePoints { get; set; } = [];
|
||||
|
||||
private float _forwardSpeed;
|
||||
private float _frontalArea;
|
||||
|
||||
private float _sideArea;
|
||||
private float _sideSpeed;
|
||||
private float lateralDragForce;
|
||||
private float longitudinalDragForce;
|
||||
|
||||
private void SimulateAerodinamics( float dt )
|
||||
{
|
||||
if ( TotalSpeed < 1f )
|
||||
{
|
||||
longitudinalDragForce = 0;
|
||||
lateralDragForce = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
_frontalArea = Dimensions.x * Dimensions.z * 0.85f;
|
||||
_sideArea = Dimensions.y * Dimensions.z * 0.8f;
|
||||
_forwardSpeed = LocalVelocity.x.InchToMeter();
|
||||
_sideSpeed = LocalVelocity.y.InchToMeter();
|
||||
longitudinalDragForce = 0.5f * RHO * _frontalArea * FrontalCd * (_forwardSpeed * _forwardSpeed) * (_forwardSpeed > 0 ? -1f : 1f);
|
||||
lateralDragForce = 0.5f * RHO * _sideArea * SideCd * (_sideSpeed * _sideSpeed) * (_sideSpeed > 0 ? -1f : 1f);
|
||||
var force = new Vector3( longitudinalDragForce.MeterToInch(), lateralDragForce.MeterToInch(), 0 ).RotateAround( Vector3.Zero, WorldRotation );
|
||||
Body.ApplyForce( force );
|
||||
//DebugOverlay.Normal( WorldPosition, force );
|
||||
|
||||
float speedPercent = TotalSpeed / MaxDownforceSpeed;
|
||||
float forceCoeff = 1f - (1f - MathF.Pow( speedPercent, 2f ));
|
||||
|
||||
foreach ( DownforcePoint dp in DownforcePoints )
|
||||
Body.ApplyForceAt( Transform.World.PointToWorld( dp.Position ), forceCoeff.MeterToInch() * dp.MaxForce.MeterToInch() * -WorldRotation.Up );
|
||||
}
|
||||
|
||||
//protected override void DrawGizmos()
|
||||
//{
|
||||
// Gizmo.Draw.LineBBox( new BBox( -Dimensions / 2 * 39.37f, Dimensions / 2 * 39.37f ) );
|
||||
//}
|
||||
}
|
||||
@ -1,70 +0,0 @@
|
||||
|
||||
|
||||
using Sandbox;
|
||||
using System;
|
||||
|
||||
namespace VeloX;
|
||||
|
||||
|
||||
public partial class VeloXCar
|
||||
{
|
||||
public const float MIN_DRIFT_ANGLE = 10f;
|
||||
public const float MIN_DRIFT_SPEED = 10f;
|
||||
public const float MAX_DRIFT_ANGLE = 110f;
|
||||
|
||||
public static readonly SoundFile SkidSound = SoundFile.Load( "sounds/tire/skid.wav" );
|
||||
|
||||
private SoundHandle _skidHandle;
|
||||
private float targetPitch;
|
||||
private float targetVolume;
|
||||
public float GetDriftAngle()
|
||||
{
|
||||
if ( !IsOnGround )
|
||||
return 0;
|
||||
|
||||
var velocity = Body.Velocity;
|
||||
var forward = WorldRotation.Forward;
|
||||
|
||||
// Early exit if speed is too low
|
||||
if ( TotalSpeed < MIN_DRIFT_SPEED )
|
||||
return 0f;
|
||||
|
||||
// Normalize the dot product calculation
|
||||
float dotProduct = velocity.Normal.Dot( forward );
|
||||
// Handle potential floating point precision issues
|
||||
float cosAngle = dotProduct;
|
||||
cosAngle = MathX.Clamp( cosAngle, -1f, 1f );
|
||||
|
||||
// Calculate angle in degrees
|
||||
float angle = MathF.Abs( MathX.RadianToDegree( MathF.Acos( cosAngle ) ) );
|
||||
|
||||
|
||||
// Check if angle is within drift range
|
||||
if ( angle >= MIN_DRIFT_ANGLE && angle <= MAX_DRIFT_ANGLE )
|
||||
return angle;
|
||||
|
||||
return 0f;
|
||||
}
|
||||
|
||||
protected virtual void UpdateDrift( float dt )
|
||||
{
|
||||
float driftAngle = GetDriftAngle();
|
||||
|
||||
float mul = (driftAngle - MIN_DRIFT_ANGLE) / (90 - MIN_DRIFT_ANGLE);
|
||||
|
||||
if ( !_skidHandle.IsValid() )
|
||||
_skidHandle = Sound.PlayFile( SkidSound );
|
||||
|
||||
if ( !_skidHandle.IsValid() )
|
||||
return;
|
||||
|
||||
targetVolume = mul;
|
||||
targetPitch = 0.75f + 0.25f * mul;
|
||||
|
||||
_skidHandle.Pitch += (targetPitch - _skidHandle.Pitch) * dt * 5f;
|
||||
_skidHandle.Volume += (targetVolume - _skidHandle.Volume) * dt * 10f;
|
||||
_skidHandle.Position = WorldPosition;
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
365
Code/Car/VeloXCar.Engine.cs
Normal file
365
Code/Car/VeloXCar.Engine.cs
Normal file
@ -0,0 +1,365 @@
|
||||
using Sandbox;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using VeloX.Powertrain;
|
||||
|
||||
namespace VeloX;
|
||||
|
||||
public partial class VeloXCar
|
||||
{
|
||||
|
||||
[Property, Feature( "Engine" )] Engine Engine { get; set; }
|
||||
|
||||
private void EngineThink( float dt )
|
||||
{
|
||||
Engine.Throttle = Input.Down( "Forward" ) ? 1 : 0;
|
||||
Engine.ForwardStep( 0, 0 );
|
||||
}
|
||||
|
||||
|
||||
//[Property, Feature( "Engine" ), Sync] public EngineState EngineState { get; set; }
|
||||
//[Property, Feature( "Engine" )] public EngineStream Stream { get; set; }
|
||||
//public EngineStreamPlayer StreamPlayer { get; set; }
|
||||
|
||||
//[Property, Feature( "Engine" )] public float MinRPM { get; set; } = 800;
|
||||
//[Property, Feature( "Engine" )] public float MaxRPM { get; set; } = 7000;
|
||||
//[Property, Feature( "Engine" ), Range( -1, 1 )]
|
||||
//public float PowerDistribution
|
||||
//{
|
||||
// get => powerDistribution; set
|
||||
// {
|
||||
// powerDistribution = value;
|
||||
// UpdatePowerDistribution();
|
||||
// }
|
||||
//}
|
||||
//[Property, Feature( "Engine" )] public float FlyWheelMass { get; set; } = 80f;
|
||||
//[Property, Feature( "Engine" )] public float FlyWheelRadius { get; set; } = 0.5f;
|
||||
//[Property, Feature( "Engine" )] public float FlywheelFriction { get; set; } = -6000;
|
||||
//[Property, Feature( "Engine" )] public float FlywheelTorque { get; set; } = 20000;
|
||||
//[Property, Feature( "Engine" )] public float EngineBrakeTorque { get; set; } = 2000;
|
||||
//[Property, Feature( "Engine" )]
|
||||
//public Dictionary<int, float> Gears { get; set; } = new()
|
||||
//{
|
||||
// [-1] = 2.5f,
|
||||
// [0] = 0f,
|
||||
// [1] = 2.8f,
|
||||
// [2] = 1.7f,
|
||||
// [3] = 1.2f,
|
||||
// [4] = 0.9f,
|
||||
// [5] = 0.75f,
|
||||
// [6] = 0.7f
|
||||
//};
|
||||
//[Property, Feature( "Engine" )] public float DifferentialRatio { get; set; } = 1f;
|
||||
//[Property, Feature( "Engine" ), Range( 0, 1 )] public float TransmissionEfficiency { get; set; } = 0.8f;
|
||||
//[Property, Feature( "Engine" )] private float MinRPMTorque { get; set; } = 5000f;
|
||||
//[Property, Feature( "Engine" )] private float MaxRPMTorque { get; set; } = 8000f;
|
||||
//[Sync] public int Gear { get; set; } = 0;
|
||||
//[Sync] public float Clutch { get; set; } = 1;
|
||||
//[Sync( SyncFlags.Interpolate )] public float EngineRPM { get; set; }
|
||||
//public float RPMPercent => (EngineRPM - MinRPM) / MaxRPM;
|
||||
|
||||
//private const float TAU = MathF.Tau;
|
||||
|
||||
//private int MinGear { get; set; }
|
||||
//private int MaxGear { get; set; }
|
||||
|
||||
//[Sync] public bool IsRedlining { get; private set; }
|
||||
|
||||
//private float flywheelVelocity;
|
||||
//private TimeUntil switchCD = 0;
|
||||
//private float groundedCount;
|
||||
//private float burnout;
|
||||
|
||||
//private float frontBrake;
|
||||
//private float rearBrake;
|
||||
|
||||
//private float availableFrontTorque;
|
||||
//private float availableRearTorque;
|
||||
|
||||
//private float avgSideSlip;
|
||||
//private float avgPoweredRPM;
|
||||
//private float avgForwardSlip;
|
||||
|
||||
//private float inputThrottle, inputBrake;
|
||||
//private bool inputHandbrake;
|
||||
//private float transmissionRPM;
|
||||
//private float powerDistribution;
|
||||
|
||||
//public float FlywheelRPM
|
||||
//{
|
||||
// get => flywheelVelocity * 60 / TAU;
|
||||
// set
|
||||
// {
|
||||
// flywheelVelocity = value * TAU / 60; EngineRPM = value;
|
||||
// }
|
||||
//}
|
||||
//private void UpdateGearList()
|
||||
//{
|
||||
// int minGear = 0;
|
||||
// int maxGear = 0;
|
||||
|
||||
// foreach ( var (gear, ratio) in Gears )
|
||||
// {
|
||||
|
||||
// if ( gear < minGear )
|
||||
// minGear = gear;
|
||||
|
||||
// if ( gear > maxGear )
|
||||
// maxGear = gear;
|
||||
|
||||
// if ( minGear != 0 || maxGear != 0 )
|
||||
// {
|
||||
// SwitchGear( 0, false );
|
||||
// }
|
||||
// }
|
||||
|
||||
// MinGear = minGear;
|
||||
// MaxGear = maxGear;
|
||||
|
||||
//}
|
||||
|
||||
|
||||
//public void SwitchGear( int index, bool cooldown = true )
|
||||
//{
|
||||
// if ( Gear == index ) return;
|
||||
// index = Math.Clamp( index, MinGear, MaxGear );
|
||||
|
||||
// if ( index == 0 || !cooldown )
|
||||
// switchCD = 0;
|
||||
// else
|
||||
// switchCD = 0.3f;
|
||||
// Clutch = 1;
|
||||
// Gear = index;
|
||||
//}
|
||||
|
||||
//public float TransmissionToEngineRPM( int gear ) => avgPoweredRPM * Gears[gear] * DifferentialRatio * 60 / TAU;
|
||||
//public float GetTransmissionMaxRPM( int gear ) => FlywheelRPM / Gears[gear] / DifferentialRatio;
|
||||
|
||||
//private void UpdatePowerDistribution()
|
||||
//{
|
||||
// if ( Wheels is null ) return;
|
||||
// int frontCount = 0, rearCount = 0;
|
||||
|
||||
// foreach ( var wheel in Wheels )
|
||||
// {
|
||||
// if ( wheel.IsFront )
|
||||
// frontCount++;
|
||||
// else
|
||||
// rearCount++;
|
||||
|
||||
// }
|
||||
|
||||
// float frontDistribution = 0.5f + PowerDistribution * 0.5f;
|
||||
|
||||
// float rearDistribution = 1 - frontDistribution;
|
||||
|
||||
// frontDistribution /= frontCount;
|
||||
// rearDistribution /= rearCount;
|
||||
|
||||
// foreach ( var wheel in Wheels )
|
||||
// if ( wheel.IsFront )
|
||||
// wheel.DistributionFactor = frontDistribution;
|
||||
// else
|
||||
// wheel.DistributionFactor = rearDistribution;
|
||||
//}
|
||||
|
||||
//private void EngineAccelerate( float torque, float dt )
|
||||
//{
|
||||
// var inertia = 0.5f * FlyWheelMass * FlyWheelRadius * FlyWheelRadius;
|
||||
// var angularAcceleration = torque / inertia;
|
||||
|
||||
// flywheelVelocity += angularAcceleration * dt;
|
||||
//}
|
||||
|
||||
//private float GetTransmissionTorque( int gear, float minTorque, float maxTorque )
|
||||
//{
|
||||
// var torque = FlywheelRPM.Remap( MinRPM, MaxRPM, minTorque, maxTorque, true );
|
||||
// torque *= (1 - Clutch);
|
||||
// torque = torque * Gears[gear] * DifferentialRatio * TransmissionEfficiency;
|
||||
|
||||
// return gear == -1 ? -torque : torque;
|
||||
//}
|
||||
|
||||
//private void AutoGearSwitch()
|
||||
//{
|
||||
// if ( ForwardSpeed < 100 && Input.Down( "Backward" ) )
|
||||
// {
|
||||
// SwitchGear( -1, false );
|
||||
// return;
|
||||
// }
|
||||
|
||||
// var currentGear = Gear;
|
||||
|
||||
// if ( currentGear < 0 && ForwardSpeed < -100 )
|
||||
// return;
|
||||
|
||||
// if ( Math.Abs( avgForwardSlip ) > 10 )
|
||||
// return;
|
||||
|
||||
// var gear = Math.Clamp( currentGear, 1, MaxGear );
|
||||
|
||||
// float minRPM = MinRPM, maxRPM = MaxRPM;
|
||||
|
||||
|
||||
// maxRPM *= 0.98f;
|
||||
|
||||
// float gearRPM;
|
||||
|
||||
|
||||
// for ( int i = 1; i <= MaxGear; i++ )
|
||||
// {
|
||||
// gearRPM = TransmissionToEngineRPM( i );
|
||||
|
||||
// if ( (i == 1 && gearRPM < minRPM) || (gearRPM > minRPM && gearRPM < maxRPM) )
|
||||
// {
|
||||
// gear = i;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
// var threshold = minRPM + (maxRPM - minRPM) * (0.5 - Throttle * 0.3);
|
||||
// if ( gear < currentGear && gear > currentGear - 2 && EngineRPM > threshold )
|
||||
// return;
|
||||
|
||||
// SwitchGear( gear );
|
||||
//}
|
||||
|
||||
//private float EngineClutch( float dt )
|
||||
//{
|
||||
// if ( !switchCD )
|
||||
// {
|
||||
// inputThrottle = 0;
|
||||
|
||||
// return 0;
|
||||
// }
|
||||
|
||||
// if ( inputHandbrake )
|
||||
// return 1;
|
||||
|
||||
// var absForwardSpeed = Math.Abs( ForwardSpeed );
|
||||
|
||||
// if ( groundedCount < 1 && absForwardSpeed > 30 )
|
||||
// return 1;
|
||||
|
||||
// if ( ForwardSpeed < -50 && inputBrake > 0 && Gear < 0 )
|
||||
// return 1;
|
||||
|
||||
// if ( absForwardSpeed > 200 )
|
||||
// return 0;
|
||||
|
||||
// return inputThrottle > 0.1f ? 0 : 1;
|
||||
//}
|
||||
|
||||
//private void EngineThink( float dt )
|
||||
//{
|
||||
// inputThrottle = Input.Down( "Forward" ) ? 1 : 0;
|
||||
// inputBrake = Input.Down( "Backward" ) ? 1 : 0;
|
||||
// inputHandbrake = Input.Down( "Jump" );
|
||||
// if ( burnout > 0 )
|
||||
// {
|
||||
// SwitchGear( 1, false );
|
||||
// if ( inputThrottle < 0.1f || inputBrake < 0.1f )
|
||||
// burnout = 0;
|
||||
// }
|
||||
// else
|
||||
// AutoGearSwitch();
|
||||
|
||||
// if ( Gear < 0 )
|
||||
// (inputBrake, inputThrottle) = (inputThrottle, inputBrake);
|
||||
|
||||
// var rpm = FlywheelRPM;
|
||||
|
||||
// var clutch = EngineClutch( dt );
|
||||
|
||||
// if ( inputThrottle > 0.1 && inputBrake > 0.1 && Math.Abs( ForwardSpeed ) < 50 )
|
||||
// {
|
||||
// burnout = MathX.Approach( burnout, 1, dt * 2 );
|
||||
// Clutch = 0;
|
||||
|
||||
// }
|
||||
// else if ( inputHandbrake )
|
||||
// {
|
||||
// frontBrake = 0f;
|
||||
// rearBrake = 0.5f;
|
||||
// Clutch = 1;
|
||||
// clutch = 1;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// if ( (Gear == -1 || Gear == 1) && inputThrottle < 0.05f && inputBrake < 0.1f && groundedCount > 1 && rpm < MinRPM * 1.2f )
|
||||
// inputBrake = 0.2f;
|
||||
|
||||
// frontBrake = inputBrake * 0.5f;
|
||||
// rearBrake = inputBrake * 0.5f;
|
||||
// }
|
||||
|
||||
// clutch = MathX.Approach( Clutch, clutch, dt * ((Gear < 2 && inputThrottle > 0.1f) ? 6 : 2) );
|
||||
|
||||
// Clutch = clutch;
|
||||
|
||||
// var isRedlining = false;
|
||||
// transmissionRPM = 0;
|
||||
|
||||
// if ( Gear != 0 )
|
||||
// {
|
||||
// transmissionRPM = TransmissionToEngineRPM( Gear );
|
||||
// transmissionRPM = Gear < 0 ? -transmissionRPM : transmissionRPM;
|
||||
// rpm = (rpm * clutch) + (MathF.Max( 0, transmissionRPM ) * (1 - clutch));
|
||||
// }
|
||||
// var throttle = Throttle;
|
||||
|
||||
// var gearTorque = GetTransmissionTorque( Gear, MinRPMTorque, MaxRPMTorque );
|
||||
|
||||
// var availableTorque = gearTorque * throttle;
|
||||
|
||||
// if ( transmissionRPM < 0 )
|
||||
// {
|
||||
// availableTorque += gearTorque * 2;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// var engineBrakeTorque = GetTransmissionTorque( Gear, EngineBrakeTorque, EngineBrakeTorque );
|
||||
|
||||
// availableTorque -= engineBrakeTorque * (1 - throttle) * 0.5f;
|
||||
// }
|
||||
|
||||
// var maxRPM = MaxRPM;
|
||||
|
||||
// if ( rpm < MinRPM )
|
||||
// {
|
||||
// rpm = MinRPM;
|
||||
// }
|
||||
// else if ( rpm > maxRPM )
|
||||
// {
|
||||
|
||||
// if ( rpm > maxRPM * 1.2f )
|
||||
// availableTorque = 0;
|
||||
|
||||
// rpm = maxRPM;
|
||||
|
||||
// if ( Gear != MaxGear || groundedCount < Wheels.Count )
|
||||
// isRedlining = true;
|
||||
// }
|
||||
|
||||
// FlywheelRPM = Math.Clamp( rpm, 0, maxRPM );
|
||||
|
||||
// if ( burnout > 0 )
|
||||
// availableTorque += availableTorque * burnout * 0.1f;
|
||||
|
||||
// var front = 0.5f + PowerDistribution * 0.5f;
|
||||
// var rear = 1 - front;
|
||||
|
||||
// availableFrontTorque = availableTorque * front;
|
||||
// availableRearTorque = availableTorque * rear;
|
||||
|
||||
// throttle = MathX.Approach( throttle, inputThrottle, dt * 4 );
|
||||
|
||||
|
||||
// EngineAccelerate( FlywheelFriction + FlywheelTorque * throttle, dt );
|
||||
|
||||
// Throttle = throttle;
|
||||
|
||||
// IsRedlining = (isRedlining && inputThrottle > 0);
|
||||
//}
|
||||
|
||||
}
|
||||
@ -1,51 +1,53 @@
|
||||
using Sandbox;
|
||||
using System;
|
||||
using VeloX.Utils;
|
||||
using System.Threading;
|
||||
|
||||
namespace VeloX;
|
||||
|
||||
public partial class VeloXCar
|
||||
{
|
||||
public static float ExpDecay( float a, float b, float decay, float dt ) => b + (a - b) * MathF.Exp( -decay * dt );
|
||||
[Property, Feature( "Steer" )] public float MaxSteerAngle { get; set; } = 40f;
|
||||
|
||||
[ConVar( "steer_return_speed" )]
|
||||
[Property] public static float SteerReturnSpeed { get; set; } = 6f;
|
||||
|
||||
[ConVar( "steer_speed" )]
|
||||
public static float SteerInputResponse { get; set; } = 3f;
|
||||
|
||||
|
||||
[ConVar( "assist_mult" )]
|
||||
public static float MaxSteerAngleMultiplier { get; set; } = 1f;
|
||||
public int CarDirection { get { return ForwardSpeed < 1 ? 0 : (VelocityAngle < 90 && VelocityAngle > -90 ? 1 : -1); } }
|
||||
public float VelocityAngle { get; private set; }
|
||||
[Property, Feature( "Steer" )] public float SteerConeMaxSpeed { get; set; } = 1800;
|
||||
[Property, Feature( "Steer" )] public float SteerConeMaxAngle { get; set; } = 0.25f;
|
||||
[Property, Feature( "Steer" )] public float SteerConeChangeRate { get; set; } = 8;
|
||||
[Property, Feature( "Steer" )] public float CounterSteer { get; set; } = 0.1f;
|
||||
[Property, Feature( "Steer" )] public float MaxSteerAngle { get; set; } = 35f;
|
||||
|
||||
[Sync] public float Steering { get; private set; }
|
||||
|
||||
private float currentSteerAngle;
|
||||
private float inputSteer;
|
||||
|
||||
public static float SignedAngle( Vector3 from, Vector3 to, Vector3 axis )
|
||||
{
|
||||
float unsignedAngle = Vector3.GetAngle( from, to );
|
||||
|
||||
float cross_x = from.y * to.z - from.z * to.y;
|
||||
float cross_y = from.z * to.x - from.x * to.z;
|
||||
float cross_z = from.x * to.y - from.y * to.x;
|
||||
float sign = MathF.Sign( axis.x * cross_x + axis.y * cross_y + axis.z * cross_z );
|
||||
return unsignedAngle * sign;
|
||||
}
|
||||
public float VelocityAngle { get; private set; }
|
||||
public int CarDirection { get { return ForwardSpeed.InchToMeter() < 5 ? 0 : (VelocityAngle < 90 && VelocityAngle > -90 ? 1 : -1); } }
|
||||
private void UpdateSteering( float dt )
|
||||
{
|
||||
inputSteer = Input.AnalogMove.y;
|
||||
var inputSteer = Input.AnalogMove.y;
|
||||
|
||||
|
||||
float targetSteerAngle = inputSteer * MaxSteerAngle;
|
||||
VelocityAngle = 0;// -SignedAngle( Body.Velocity, WorldRotation.Forward, WorldRotation.Up );
|
||||
|
||||
if ( !Input.Down( "Jump" ) )
|
||||
targetSteerAngle *= Math.Clamp( 1 - Math.Clamp( TotalSpeed / 3000, 0.01f, 0.9f ), -1, 1 );
|
||||
//var steerConeFactor = Math.Clamp( TotalSpeed / SteerConeMaxSpeed, 0, 1 );
|
||||
//var steerCone = 1 - steerConeFactor * (1 - SteerConeMaxAngle);
|
||||
|
||||
VelocityAngle = -Body.Velocity.SignedAngle( WorldRotation.Forward, WorldRotation.Up );
|
||||
inputSteer = ExpDecay( this.inputSteer, inputSteer, SteerConeChangeRate, dt );
|
||||
this.inputSteer = inputSteer;
|
||||
|
||||
float targetAngle = 0;
|
||||
float target = -inputSteer * MaxSteerAngle;
|
||||
if ( CarDirection > 0 )
|
||||
target -= VelocityAngle * CounterSteer;
|
||||
|
||||
if ( TotalSpeed > 150 && CarDirection > 0 && IsOnGround )
|
||||
targetAngle = VelocityAngle * MaxSteerAngleMultiplier;
|
||||
|
||||
float lerpSpeed = Math.Abs( inputSteer ) < 0.1f ? SteerReturnSpeed : SteerInputResponse;
|
||||
|
||||
currentSteerAngle = ExpDecay( currentSteerAngle, targetSteerAngle, lerpSpeed, Time.Delta );
|
||||
Steering = currentSteerAngle + targetAngle;
|
||||
SteerAngle = new( 0, Math.Clamp( Steering, -MaxSteerAngle, MaxSteerAngle ), 0 );
|
||||
inputSteer = Math.Clamp( inputSteer, -1, 1 );
|
||||
Steering = inputSteer;
|
||||
SteerAngle = new( 0, target, 0 );
|
||||
}
|
||||
}
|
||||
|
||||
10
Code/Car/VeloXCar.Wheel.cs
Normal file
10
Code/Car/VeloXCar.Wheel.cs
Normal file
@ -0,0 +1,10 @@
|
||||
namespace VeloX;
|
||||
|
||||
public partial class VeloXCar
|
||||
{
|
||||
private void WheelThink( in float dt )
|
||||
{
|
||||
foreach ( var w in Wheels )
|
||||
w.Update( this, dt );
|
||||
}
|
||||
}
|
||||
@ -7,33 +7,21 @@ namespace VeloX;
|
||||
[Title( "VeloX - Car" )]
|
||||
public partial class VeloXCar : VeloXBase
|
||||
{
|
||||
|
||||
protected override void OnFixedUpdate()
|
||||
{
|
||||
if ( IsProxy )
|
||||
if ( !IsDriver )
|
||||
return;
|
||||
|
||||
base.OnFixedUpdate();
|
||||
|
||||
Brake = Math.Clamp( (Input.Down( "Jump" ) ? 1 : 0), 0, 1 );
|
||||
|
||||
var dt = Time.Delta;
|
||||
//EngineThink( dt );
|
||||
SimulateAerodinamics( dt );
|
||||
//WheelThink( dt );
|
||||
|
||||
EngineThink( dt );
|
||||
WheelThink( dt );
|
||||
UpdateSteering( dt );
|
||||
UpdateUnflip( dt );
|
||||
UpdateDrift( dt );
|
||||
|
||||
}
|
||||
|
||||
private void UpdateUnflip( float dt )
|
||||
{
|
||||
if ( Math.Abs( inputSteer ) < 0.1f )
|
||||
return;
|
||||
|
||||
if ( Math.Abs( WorldRotation.Angles().roll ) < 70 )
|
||||
return;
|
||||
|
||||
var angVel = Body.AngularVelocity;
|
||||
var force = inputSteer * Mass * Math.Clamp( 1 - angVel.x / 50, 0, 1 ) * 0.05f;
|
||||
Body.AngularVelocity -= Body.WorldRotation.Forward * force * dt;
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,8 +4,7 @@ using VeloX.Audio;
|
||||
|
||||
namespace VeloX;
|
||||
|
||||
[AssetType( Name = "Engine Stream", Extension = "engstr", Category = "VeloX" )]
|
||||
[Icon( "time_to_leave" )]
|
||||
[GameResource( "Engine Stream", "engstr", "Engine Sound", Category = "VeloX", Icon = "time_to_leave" )]
|
||||
public sealed class EngineStream : GameResource
|
||||
{
|
||||
public sealed class Layer
|
||||
|
||||
@ -26,7 +26,6 @@ public class EngineStreamPlayer( EngineStream stream ) : IDisposable
|
||||
|
||||
public void Update( float deltaTime, Vector3 position, bool isLocal = false )
|
||||
{
|
||||
|
||||
var globalPitch = 1.0f;
|
||||
|
||||
// Gear wobble effect
|
||||
@ -52,13 +51,13 @@ public class EngineStreamPlayer( EngineStream stream ) : IDisposable
|
||||
{
|
||||
EngineSounds.TryGetValue( layer, out var channel );
|
||||
|
||||
if ( !channel.IsValid() && layer.AudioPath.IsValid() )
|
||||
if ( !channel.IsValid() )
|
||||
{
|
||||
channel = Sound.PlayFile( layer.AudioPath );
|
||||
EngineSounds[layer] = channel;
|
||||
}
|
||||
|
||||
if ( !channel.IsValid() || channel.Paused && (EngineSoundPaused || layer.IsMuted) )
|
||||
if ( channel.Paused && (EngineSoundPaused || layer.IsMuted) )
|
||||
continue;
|
||||
|
||||
// Reset controller outputs
|
||||
|
||||
@ -1,29 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace VeloX.Utils;
|
||||
|
||||
public static class MathM
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Converts angular velocity (rad/s) to rotations per minute.
|
||||
/// </summary>
|
||||
public static float AngularVelocityToRPM( this float angularVelocity ) => angularVelocity * 9.5492965855137f;
|
||||
|
||||
/// <summary>
|
||||
/// Converts rotations per minute to angular velocity (rad/s).
|
||||
/// </summary>
|
||||
public static float RPMToAngularVelocity( this float RPM ) => RPM * 0.10471975511966f;
|
||||
|
||||
public static float SignedAngle( this Vector3 from, Vector3 to, Vector3 axis )
|
||||
{
|
||||
float unsignedAngle = Vector3.GetAngle( from, to );
|
||||
|
||||
float cross_x = from.y * to.z - from.z * to.y;
|
||||
float cross_y = from.z * to.x - from.x * to.z;
|
||||
float cross_z = from.x * to.y - from.y * to.x;
|
||||
float sign = MathF.Sign( axis.x * cross_x + axis.y * cross_y + axis.z * cross_z );
|
||||
return unsignedAngle * sign;
|
||||
}
|
||||
|
||||
}
|
||||
@ -81,11 +81,6 @@ internal sealed class EngineStreamEditor : Widget, IAssetInspector
|
||||
picker.OnAssetPicked += asset => CreateStreamEditor( asset[0].LoadResource<EngineStream>() );
|
||||
picker.Show();
|
||||
};
|
||||
var saveButton = a.Add( new Button( "Save File" ), 0 );
|
||||
saveButton.Clicked = () =>
|
||||
{
|
||||
ActiveStream.StateHasChanged();
|
||||
};
|
||||
}
|
||||
Layout RightLayout;
|
||||
SimulatedEngineWidget SimulatedEngineWidget;
|
||||
|
||||
@ -76,7 +76,7 @@ internal sealed class SimulatedEngineWidget : Widget
|
||||
[EditorEvent.Frame]
|
||||
public void OnFrame()
|
||||
{
|
||||
if ( Player is null || !Player.Stream.IsValid() )
|
||||
if ( Player.Stream is null )
|
||||
return;
|
||||
Player.EngineState = IsPlaying ? EngineState.Running : EngineState.Off;
|
||||
|
||||
@ -85,7 +85,7 @@ internal sealed class SimulatedEngineWidget : Widget
|
||||
IsRedlining = RPM.Value == 1 && !IsRedlining;
|
||||
Player.IsRedlining = IsRedlining;
|
||||
|
||||
Player?.Update( Time.Delta, Vector3.Zero, true );
|
||||
Player.Update( Time.Delta, Vector3.Zero, true );
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
48
Editor/Wheel/PacejkaWidget.cs
Normal file
48
Editor/Wheel/PacejkaWidget.cs
Normal file
@ -0,0 +1,48 @@
|
||||
using Editor;
|
||||
using Sandbox;
|
||||
|
||||
namespace VeloX;
|
||||
|
||||
[CustomEditor( typeof( Pacejka ) )]
|
||||
public class PacejkaWidget : ControlObjectWidget
|
||||
{
|
||||
public override bool SupportsMultiEdit => false;
|
||||
public override bool IncludeLabel => false;
|
||||
|
||||
[CustomEditor( typeof( Pacejka.PacejkaPreset ) )]
|
||||
private class LateralForceWidget : ControlObjectWidget
|
||||
{
|
||||
public LateralForceWidget( SerializedProperty property ) : base( property, true )
|
||||
{
|
||||
Layout = Layout.Column();
|
||||
Layout.Margin = 8f;
|
||||
Layout.Spacing = 8;
|
||||
foreach ( var item in TypeLibrary.GetType<Pacejka.PacejkaPreset>().Properties )
|
||||
{
|
||||
var row = Layout.AddRow();
|
||||
row.Spacing = 8;
|
||||
var propetry = SerializedObject.GetProperty( item.Name );
|
||||
row.Add( new Label( propetry.Name ) );
|
||||
row.Add( Create( propetry ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
private Pacejka Pacejka;
|
||||
public PacejkaWidget( SerializedProperty property ) : base( property, true )
|
||||
{
|
||||
|
||||
var obj = SerializedObject;
|
||||
Pacejka = obj.ParentProperty.GetValue<Pacejka>();
|
||||
|
||||
Layout = Layout.Column();
|
||||
Layout.Margin = 8f;
|
||||
Layout.Add( new Label.Body( $" {ToolTip}" ) { Color = Color.White } );
|
||||
var tabs = Layout.Add( new TabWidget( null ) );
|
||||
tabs.AddPage( nameof( Pacejka.Lateral ), null,
|
||||
Layout.Add( Create( obj.GetProperty( nameof( Pacejka.Lateral ) ) ) )
|
||||
);
|
||||
tabs.AddPage( nameof( Pacejka.Longitudinal ), null,
|
||||
Layout.Add( Create( obj.GetProperty( nameof( Pacejka.Longitudinal ) ) ) )
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -11,9 +11,17 @@ namespace VeloX;
|
||||
[AssetPreview( "tire" )]
|
||||
class TirePresetPreview : AssetPreview
|
||||
{
|
||||
private Texture texture;
|
||||
public override bool IsAnimatedPreview => false;
|
||||
[Range( 0.01f, 1 )] private float Zoom { get; set; } = 1;
|
||||
private TirePreset Tire;
|
||||
public AssetPreviewWidget Widget { get; private set; }
|
||||
public override Widget CreateWidget( Widget parent )
|
||||
{
|
||||
Widget = parent as AssetPreviewWidget;
|
||||
|
||||
return null;
|
||||
}
|
||||
public override Widget CreateToolbar()
|
||||
{
|
||||
var info = new IconButton( "settings" );
|
||||
@ -50,53 +58,55 @@ class TirePresetPreview : AssetPreview
|
||||
|
||||
using ( Scene.Push() )
|
||||
{
|
||||
PrimaryObject = new( true )
|
||||
PrimaryObject = new()
|
||||
{
|
||||
WorldTransform = Transform.Zero
|
||||
};
|
||||
|
||||
var spriteRenderer = PrimaryObject.AddComponent<SpriteRenderer>();
|
||||
|
||||
var bitmap = new Bitmap( 512, 512 );
|
||||
var tire = Asset.LoadResource<TirePreset>();
|
||||
Draw( bitmap, tire );
|
||||
|
||||
spriteRenderer.Sprite = new() { Animations = [new() { Frames = [new() { Texture = bitmap.ToTexture() }] },] }; // Set the texture on the renderer
|
||||
spriteRenderer.Size = 512;
|
||||
var plane = PrimaryObject.AddComponent<ModelRenderer>();
|
||||
plane.Model = Model.Plane;
|
||||
plane.LocalScale = new Vector3( 1, 1, 1 );
|
||||
plane.MaterialOverride = Material.Load( "materials/dev/reflectivity_30.vmat" );
|
||||
plane.Tint = new Color( 0.02f, 0.04f, 0.03f );
|
||||
|
||||
var bounds = PrimaryObject.GetBounds();
|
||||
SceneCenter = bounds.Center;
|
||||
SceneSize = bounds.Size;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
public override void UpdateScene( float cycle, float timeStep )
|
||||
{
|
||||
base.UpdateScene( cycle, timeStep );
|
||||
if ( !Widget.IsValid() )
|
||||
return;
|
||||
|
||||
Camera.WorldPosition = Vector3.Up * 300;
|
||||
Camera.Orthographic = true;
|
||||
Camera.OrthographicHeight = 512;
|
||||
Camera.WorldPosition = Vector3.Backward * 512;
|
||||
Camera.WorldRotation = Rotation.LookAt( Vector3.Forward );
|
||||
|
||||
Camera.WorldRotation = new Angles( 90, 0, 0 );
|
||||
|
||||
var bitmap = new Bitmap( 512, 512 );
|
||||
var tire = Asset.LoadResource<TirePreset>();
|
||||
Draw( bitmap, tire );
|
||||
|
||||
PrimaryObject.Components.Get<SpriteRenderer>().Sprite = new() { Animations = [new() { Frames = [new() { Texture = bitmap.ToTexture() }] },] }; // Set the texture on the renderer
|
||||
PrimaryObject.Components.Get<SpriteRenderer>().Size = 512;
|
||||
Draw( bitmap );
|
||||
|
||||
texture.Clear( Color.Black );
|
||||
//texture.Update( bitmap );
|
||||
DebugOverlaySystem.Current.Texture( texture, new Rect( 0, Widget.Size ) );
|
||||
|
||||
FrameScene();
|
||||
}
|
||||
|
||||
private readonly List<Vector2> pointCache = [];
|
||||
|
||||
public TirePresetPreview( Asset asset ) : base( asset )
|
||||
{
|
||||
texture = Texture.CreateRenderTarget().WithDynamicUsage().WithScreenFormat().WithSize( 512, 512 ).Create();
|
||||
Tire = Asset.LoadResource<TirePreset>();
|
||||
}
|
||||
|
||||
private void DrawPacejka( Bitmap bitmap, TirePreset tire )
|
||||
private void DrawPacejka( Bitmap bitmap )
|
||||
{
|
||||
|
||||
var tire = Tire.Pacejka;
|
||||
var width = bitmap.Width;
|
||||
var height = bitmap.Height;
|
||||
|
||||
@ -108,18 +118,32 @@ class TirePresetPreview : AssetPreview
|
||||
|
||||
for ( float x = 0; x <= 1; x += 0.01f )
|
||||
{
|
||||
float val = tire.Evaluate( x ) * Zoom;
|
||||
float val = tire.PacejkaFy( x ) * Zoom;
|
||||
pointCache.Add( new( width * x, height - height * val ) );
|
||||
}
|
||||
|
||||
bitmap.DrawLines( pointCache.ToArray() );
|
||||
}
|
||||
|
||||
{ // draw longitudinal line
|
||||
pointCache.Clear();
|
||||
|
||||
bitmap.SetPen( Color.Green, 1 );
|
||||
|
||||
for ( float x = 0; x <= 1; x += 0.01f )
|
||||
{
|
||||
float val = tire.PacejkaFx( x ) * Zoom;
|
||||
pointCache.Add( new( width * x, height - height * val ) );
|
||||
}
|
||||
bitmap.DrawLines( pointCache.ToArray() );
|
||||
}
|
||||
|
||||
pointCache.Clear();
|
||||
}
|
||||
private void Draw( Bitmap bitmap, TirePreset tire )
|
||||
private void Draw( Bitmap bitmap )
|
||||
{
|
||||
bitmap.Clear( Color.Black );
|
||||
bitmap.SetAntialias( true );
|
||||
DrawPacejka( bitmap, tire );
|
||||
DrawPacejka( bitmap );
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
using Editor;
|
||||
using Sandbox;
|
||||
|
||||
namespace VeloX;
|
||||
|
||||
[CustomEditor( typeof( TirePreset ) )]
|
||||
public class TirePresetWidget : ControlObjectWidget
|
||||
{
|
||||
public override bool SupportsMultiEdit => false;
|
||||
public override bool IncludeLabel => false;
|
||||
|
||||
public TirePresetWidget( SerializedProperty property ) : base( property, true )
|
||||
{
|
||||
|
||||
var obj = SerializedObject;
|
||||
var tirePreset = obj.ParentProperty.GetValue<TirePreset>();
|
||||
|
||||
Layout = Layout.Column();
|
||||
Layout.Margin = 8f;
|
||||
Layout.Add( new Label.Body( $" {ToolTip}" ) { Color = Color.White } );
|
||||
Layout.Add( Create( obj.GetProperty( nameof( TirePreset.B ) ) ) );
|
||||
Layout.Add( Create( obj.GetProperty( nameof( TirePreset.C ) ) ) );
|
||||
Layout.Add( Create( obj.GetProperty( nameof( TirePreset.D ) ) ) );
|
||||
Layout.Add( Create( obj.GetProperty( nameof( TirePreset.E ) ) ) );
|
||||
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user