Files
HillLike/SimpleCar.cs

269 lines
7.9 KiB
C#

using osu.Framework.Input.Events;
using osuTK;
using osuTK.Input;
namespace HillLike;
public class SimpleCar
{
public const float WheelRadius = 0.45f;
private const float Gravity = -10f;
private const float EngineForce = 34f;
private const float BrakeForce = 34f;
private const float RollingFriction = 2f;
private const float AirDrag = 0.1f;
private const float SuspensionRest = 0f;
private const float SuspensionK = 140f;
private const float SuspensionD = 18f;
private const float AngularDamp = 2f;
private const float ChassisInertia = 2.4f;
private const float SuspensionTorqueInfluence = 0.35f;
private const float GroundAlignStrength = 8f;
private const float GroundAlignDamping = 2f;
private const float WheelAngularAccel = 60f;
private const float WheelAngularDamping = 1.2f;
private const float WheelGroundGrip = 14f;
private const float AirControlFromWheelSpin = 0.12f;
private readonly Vector2 _wheelBackLocal = new(-0.85f, -0.35f);
private readonly Vector2 _wheelFrontLocal = new(0.85f, -0.35f);
private bool _brake;
private bool _throttle;
private float _wheelSpin;
public float AngularVelocity;
public Vector2 Position;
public float Rotation;
public Vector2 Velocity;
public Vector2 WheelBackPos { get; private set; }
public Vector2 WheelFrontPos { get; private set; }
public float RotationDegrees => Rotation * 180f / (float)Math.PI;
public void Reset(Vector2 startPos)
{
Position = startPos;
Velocity = Vector2.Zero;
Rotation = 0;
AngularVelocity = 0;
_wheelSpin = 0;
_throttle = _brake = false;
UpdateWheelWorldPositions();
}
public void HandleKeyDown(KeyDownEvent e)
{
switch (e.Key)
{
case Key.D or Key.Right:
_throttle = true;
break;
case Key.A or Key.Left:
_brake = true;
break;
}
}
public void HandleKeyUp(KeyUpEvent e)
{
switch (e.Key)
{
case Key.D or Key.Right:
_throttle = false;
break;
case Key.A or Key.Left:
_brake = false;
break;
}
}
public void Step(float dt)
{
var accel = new Vector2(0, Gravity);
var torque = 0f;
accel += -AirDrag * Velocity;
UpdateWheelWorldPositions();
var backGrounded = SolveWheelSuspension(dt, WheelBackPos, out var backForce, out var backTangent,
out _);
var frontGrounded = SolveWheelSuspension(dt, WheelFrontPos, out var frontForce, out var frontTangent,
out _);
accel += backForce + frontForce;
if (backGrounded)
torque += TorqueFromForce(WheelBackPos, backForce) * SuspensionTorqueInfluence;
if (frontGrounded)
torque += TorqueFromForce(WheelFrontPos, frontForce) * SuspensionTorqueInfluence;
var grounded = backGrounded || frontGrounded;
var driveInput = 0f;
if (_throttle) driveInput += 1f;
if (_brake) driveInput -= 1f;
_wheelSpin += driveInput * WheelAngularAccel * dt;
//if (grounded)
//{
// var driveForce = backTangent * EngineForce;
// accel += driveForce;
// torque += TorqueFromForce(WheelBackPos, driveForce);
//}
if (grounded)
accel += new Vector2(-RollingFriction * Velocity.X, 0);
if (grounded)
{
var targetNormal = Vector2.Zero;
var groundedCount = 0;
if (backGrounded)
{
targetNormal += new Vector2(-backTangent.Y, backTangent.X);
groundedCount++;
}
if (frontGrounded)
{
targetNormal += new Vector2(-frontTangent.Y, frontTangent.X);
groundedCount++;
}
if (groundedCount > 0)
{
targetNormal /= groundedCount;
targetNormal = targetNormal.LengthSquared > 0 ? targetNormal.Normalized() : Vector2.UnitY;
var bodyUpDot = Vector2.Dot(BodyUp(), targetNormal);
if (bodyUpDot > 0f)
{
var angleError = SignedAngle(BodyUp(), targetNormal);
torque += angleError * GroundAlignStrength - AngularVelocity * GroundAlignDamping;
}
}
var groundedTangentSpeed = 0f;
if (backGrounded) groundedTangentSpeed += Vector2.Dot(Velocity, backTangent);
if (frontGrounded) groundedTangentSpeed += Vector2.Dot(Velocity, frontTangent);
groundedTangentSpeed /= groundedCount;
var targetSpin = groundedTangentSpeed / WheelRadius;
var gripLerp = 1f - MathF.Exp(-WheelGroundGrip * dt);
_wheelSpin = _wheelSpin + (targetSpin - _wheelSpin) * gripLerp;
}
else if (driveInput != 0f)
{
torque += driveInput * (2.0f + MathF.Abs(_wheelSpin) * AirControlFromWheelSpin);
}
_wheelSpin *= (float)Math.Exp(-WheelAngularDamping * dt);
AngularVelocity += (torque / ChassisInertia) * dt;
AngularVelocity *= (float)Math.Exp(-AngularDamp * dt);
Velocity += accel * dt;
Position += Velocity * dt;
Rotation += AngularVelocity * dt;
if (Rotation > MathF.PI) Rotation -= 2 * MathF.PI;
if (Rotation < -MathF.PI) Rotation += 2 * MathF.PI;
UpdateWheelWorldPositions();
if (!grounded)
return;
}
private void ApplyBrake(bool grounded, Vector2 wheelPos, Vector2 tangent, ref Vector2 accel, ref float torque)
{
if (!grounded)
return;
var tangentSpeed = Vector2.Dot(Velocity, tangent);
if (MathF.Abs(tangentSpeed) < 0.02f)
return;
var brakeForce = -Math.Sign(tangentSpeed) * BrakeForce * tangent;
accel += brakeForce;
torque += TorqueFromForce(wheelPos, brakeForce);
}
private float TorqueFromForce(Vector2 applicationPoint, Vector2 force)
{
var r = applicationPoint - Position;
return r.X * force.Y - r.Y * force.X;
}
private static float SignedAngle(Vector2 from, Vector2 to)
{
var cross = from.X * to.Y - from.Y * to.X;
var dot = Vector2.Dot(from, to);
return MathF.Atan2(cross, dot);
}
private void UpdateWheelWorldPositions()
{
WheelBackPos = Position + Rotate(_wheelBackLocal, Rotation);
WheelFrontPos = Position + Rotate(_wheelFrontLocal, Rotation);
}
private bool SolveWheelSuspension(
float dt,
Vector2 wheelCenter,
out Vector2 suspensionForce,
out Vector2 tangent,
out float compression01)
{
var groundY = Terrain.HeightAt(wheelCenter.X);
var normal = Terrain.NormalAt(wheelCenter.X);
tangent = new Vector2(normal.Y, -normal.X);
var wheelBottomY = wheelCenter.Y - WheelRadius;
var penetration = groundY - wheelBottomY;
var compression = penetration - SuspensionRest;
if (penetration <= 0)
{
suspensionForce = Vector2.Zero;
compression01 = 0;
return false;
}
var spring = SuspensionK * compression;
var vAlongN = Vector2.Dot(Velocity, normal);
var damper = -SuspensionD * vAlongN;
var forceMag = spring + damper;
forceMag = Math.Clamp(forceMag, 0, 250);
suspensionForce = normal * forceMag;
compression01 = Math.Clamp(penetration / (WheelRadius * 2f), 0, 1);
return true;
}
private Vector2 BodyUp()
{
return Rotate(Vector2.UnitY, Rotation);
}
private static Vector2 Rotate(Vector2 v, float radians)
{
var c = MathF.Cos(radians);
var s = MathF.Sin(radians);
return new Vector2(v.X * c - v.Y * s, v.X * s + v.Y * c);
}
}