269 lines
7.9 KiB
C#
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);
|
|
}
|
|
} |