474 lines
18 KiB
C#
474 lines
18 KiB
C#
using UnityEngine;
|
|
#if ENABLE_INPUT_SYSTEM
|
|
using UnityEngine.InputSystem;
|
|
#endif
|
|
|
|
namespace RaceFields.Vehicles
|
|
{
|
|
[RequireComponent(typeof(Rigidbody))]
|
|
public sealed class ArcadeBuggyController : MonoBehaviour
|
|
{
|
|
[Header("Wheel Colliders")]
|
|
[SerializeField] private WheelCollider frontLeftCollider;
|
|
[SerializeField] private WheelCollider frontRightCollider;
|
|
[SerializeField] private WheelCollider rearLeftCollider;
|
|
[SerializeField] private WheelCollider rearRightCollider;
|
|
|
|
[Header("Wheel Visuals")]
|
|
[SerializeField] private Transform frontLeftVisual;
|
|
[SerializeField] private Transform frontRightVisual;
|
|
[SerializeField] private Transform rearLeftVisual;
|
|
[SerializeField] private Transform rearRightVisual;
|
|
|
|
[Header("Driving")]
|
|
[SerializeField, Range(20f, 190f)] private float maxSpeedKmh = 120f;
|
|
[SerializeField, Range(10f, 120f)] private float maxReverseSpeedKmh = 45f;
|
|
[SerializeField, Range(1f, 10f)] private float accelerationMultiplier = 6f;
|
|
[SerializeField, Range(10f, 45f)] private float maxSteerAngle = 35f;
|
|
[SerializeField, Range(0.1f, 1f)] private float steeringSpeed = 0.5f;
|
|
[SerializeField, Range(100f, 600f)] private float brakeForce = 450f;
|
|
[SerializeField, Range(1f, 10f)] private float decelerationMultiplier = 2f;
|
|
[SerializeField, Range(1f, 10f)] private float handbrakeDriftMultiplier = 5f;
|
|
[SerializeField] private float downforce = 35f;
|
|
|
|
[Header("Stability")]
|
|
[SerializeField] private Vector3 centerOfMass = new Vector3(0f, -0.45f, 0.15f);
|
|
|
|
[Header("Effects")]
|
|
[SerializeField] private bool useEffects = true;
|
|
[SerializeField] private ParticleSystem rearLeftSmoke;
|
|
[SerializeField] private ParticleSystem rearRightSmoke;
|
|
[SerializeField] private TrailRenderer rearLeftSkid;
|
|
[SerializeField] private TrailRenderer rearRightSkid;
|
|
|
|
[Header("Sounds")]
|
|
[SerializeField] private bool useSounds = true;
|
|
[SerializeField] private AudioSource engineSound;
|
|
[SerializeField] private AudioSource tireScreechSound;
|
|
|
|
public float SpeedKmh { get; private set; }
|
|
public bool IsDrifting { get; private set; }
|
|
public bool IsTractionLocked { get; private set; }
|
|
|
|
public WheelCollider GetWheelCollider(WheelSlot slot)
|
|
{
|
|
return slot switch
|
|
{
|
|
WheelSlot.FrontLeft => frontLeftCollider,
|
|
WheelSlot.FrontRight => frontRightCollider,
|
|
WheelSlot.RearLeft => rearLeftCollider,
|
|
WheelSlot.RearRight => rearRightCollider,
|
|
_ => null
|
|
};
|
|
}
|
|
|
|
public Transform GetWheelVisual(WheelSlot slot)
|
|
{
|
|
return slot switch
|
|
{
|
|
WheelSlot.FrontLeft => frontLeftVisual,
|
|
WheelSlot.FrontRight => frontRightVisual,
|
|
WheelSlot.RearLeft => rearLeftVisual,
|
|
WheelSlot.RearRight => rearRightVisual,
|
|
_ => null
|
|
};
|
|
}
|
|
|
|
public void SetWheelEnabled(WheelSlot slot, bool enabled)
|
|
{
|
|
WheelCollider wheel = GetWheelCollider(slot);
|
|
if (wheel != null)
|
|
{
|
|
wheel.motorTorque = 0f;
|
|
wheel.brakeTorque = enabled ? 0f : brakeForce;
|
|
wheel.steerAngle = 0f;
|
|
wheel.enabled = enabled;
|
|
}
|
|
}
|
|
|
|
private const float TorquePerAccelerationStep = 50f;
|
|
private const float DriftVelocityThreshold = 2.5f;
|
|
private const float SkidVelocityThreshold = 5f;
|
|
private const float EffectsMinSpeedKmh = 12f;
|
|
|
|
private Rigidbody body;
|
|
private float throttleInput;
|
|
private float steeringInput;
|
|
private bool handbrakeInput;
|
|
private float steeringAxis;
|
|
private float throttleAxis;
|
|
private float driftingAxis;
|
|
private float localVelocityX;
|
|
private float localVelocityZ;
|
|
private float initialEnginePitch;
|
|
|
|
private WheelFrictionCurve frontLeftSidewaysFriction;
|
|
private WheelFrictionCurve frontRightSidewaysFriction;
|
|
private WheelFrictionCurve rearLeftSidewaysFriction;
|
|
private WheelFrictionCurve rearRightSidewaysFriction;
|
|
private float frontLeftExtremumSlip;
|
|
private float frontRightExtremumSlip;
|
|
private float rearLeftExtremumSlip;
|
|
private float rearRightExtremumSlip;
|
|
|
|
private Quaternion frontLeftVisualRotationOffset = Quaternion.identity;
|
|
private Quaternion frontRightVisualRotationOffset = Quaternion.identity;
|
|
private Quaternion rearLeftVisualRotationOffset = Quaternion.identity;
|
|
private Quaternion rearRightVisualRotationOffset = Quaternion.identity;
|
|
|
|
private void Awake()
|
|
{
|
|
body = GetComponent<Rigidbody>();
|
|
ValidateRequiredReferences();
|
|
CacheVisualRotationOffsets();
|
|
CacheWheelFriction();
|
|
|
|
body.centerOfMass = centerOfMass;
|
|
initialEnginePitch = engineSound != null ? engineSound.pitch : 1f;
|
|
SetEffectsActive(false);
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
ReadInput();
|
|
UpdateTelemetry();
|
|
UpdateWheelVisuals();
|
|
UpdateSounds();
|
|
}
|
|
|
|
private void FixedUpdate()
|
|
{
|
|
ApplySteering();
|
|
ApplyDrive();
|
|
ApplyHandbrake();
|
|
ApplyDownforce();
|
|
UpdateEffects();
|
|
}
|
|
|
|
private void ReadInput()
|
|
{
|
|
#if ENABLE_INPUT_SYSTEM
|
|
if (Keyboard.current != null)
|
|
{
|
|
float forward = Keyboard.current.wKey.isPressed || Keyboard.current.upArrowKey.isPressed ? 1f : 0f;
|
|
float reverse = Keyboard.current.sKey.isPressed || Keyboard.current.downArrowKey.isPressed ? 1f : 0f;
|
|
float left = Keyboard.current.aKey.isPressed || Keyboard.current.leftArrowKey.isPressed ? 1f : 0f;
|
|
float right = Keyboard.current.dKey.isPressed || Keyboard.current.rightArrowKey.isPressed ? 1f : 0f;
|
|
|
|
throttleInput = forward - reverse;
|
|
steeringInput = right - left;
|
|
handbrakeInput = Keyboard.current.spaceKey.isPressed;
|
|
return;
|
|
}
|
|
#endif
|
|
#if ENABLE_LEGACY_INPUT_MANAGER
|
|
throttleInput = Input.GetAxisRaw("Vertical");
|
|
steeringInput = Input.GetAxisRaw("Horizontal");
|
|
handbrakeInput = Input.GetKey(KeyCode.Space);
|
|
#else
|
|
throttleInput = 0f;
|
|
steeringInput = 0f;
|
|
handbrakeInput = false;
|
|
#endif
|
|
}
|
|
|
|
private void UpdateTelemetry()
|
|
{
|
|
SpeedKmh = body.linearVelocity.magnitude * 3.6f;
|
|
Vector3 localVelocity = transform.InverseTransformDirection(body.linearVelocity);
|
|
localVelocityX = localVelocity.x;
|
|
localVelocityZ = localVelocity.z;
|
|
IsDrifting = Mathf.Abs(localVelocityX) > DriftVelocityThreshold;
|
|
}
|
|
|
|
private void ApplySteering()
|
|
{
|
|
if (steeringInput < -0.01f)
|
|
steeringAxis -= Time.fixedDeltaTime * 10f * steeringSpeed;
|
|
else if (steeringInput > 0.01f)
|
|
steeringAxis += Time.fixedDeltaTime * 10f * steeringSpeed;
|
|
else
|
|
steeringAxis = Mathf.MoveTowards(steeringAxis, 0f, Time.fixedDeltaTime * 10f * steeringSpeed);
|
|
|
|
steeringAxis = Mathf.Clamp(steeringAxis, -1f, 1f);
|
|
float steeringAngle = steeringAxis * maxSteerAngle;
|
|
SetSteerAngle(frontLeftCollider, steeringAngle, steeringSpeed);
|
|
SetSteerAngle(frontRightCollider, steeringAngle, steeringSpeed);
|
|
}
|
|
|
|
private void ApplyDrive()
|
|
{
|
|
if (throttleInput > 0.01f)
|
|
{
|
|
throttleAxis = Mathf.MoveTowards(throttleAxis, 1f, Time.fixedDeltaTime * 3f);
|
|
|
|
if (localVelocityZ < -1f)
|
|
{
|
|
ApplyBrakeTorque(brakeForce);
|
|
ApplyMotorTorque(0f);
|
|
return;
|
|
}
|
|
|
|
ApplyBrakeTorque(0f);
|
|
ApplyMotorTorque(SpeedKmh < maxSpeedKmh ? accelerationMultiplier * TorquePerAccelerationStep * throttleAxis : 0f);
|
|
return;
|
|
}
|
|
|
|
if (throttleInput < -0.01f)
|
|
{
|
|
throttleAxis = Mathf.MoveTowards(throttleAxis, -1f, Time.fixedDeltaTime * 3f);
|
|
|
|
if (localVelocityZ > 1f)
|
|
{
|
|
ApplyBrakeTorque(brakeForce);
|
|
ApplyMotorTorque(0f);
|
|
return;
|
|
}
|
|
|
|
ApplyBrakeTorque(0f);
|
|
ApplyMotorTorque(SpeedKmh < maxReverseSpeedKmh ? accelerationMultiplier * TorquePerAccelerationStep * throttleAxis : 0f);
|
|
return;
|
|
}
|
|
|
|
throttleAxis = Mathf.MoveTowards(throttleAxis, 0f, Time.fixedDeltaTime * 10f);
|
|
ApplyMotorTorque(0f);
|
|
|
|
if (!handbrakeInput && IsAnyWheelGrounded())
|
|
{
|
|
ApplyBrakeTorque(0f);
|
|
body.linearVelocity *= 1f / (1f + 0.025f * decelerationMultiplier);
|
|
|
|
if (body.linearVelocity.magnitude < 0.25f)
|
|
body.linearVelocity = Vector3.zero;
|
|
}
|
|
}
|
|
|
|
private void ApplyHandbrake()
|
|
{
|
|
if (handbrakeInput)
|
|
{
|
|
driftingAxis = Mathf.Clamp01(driftingAxis + Time.fixedDeltaTime);
|
|
float startingPoint = driftingAxis * frontLeftExtremumSlip * handbrakeDriftMultiplier;
|
|
if (startingPoint < frontLeftExtremumSlip)
|
|
driftingAxis = frontLeftExtremumSlip / (frontLeftExtremumSlip * handbrakeDriftMultiplier);
|
|
|
|
SetDriftFriction(driftingAxis);
|
|
IsTractionLocked = true;
|
|
ApplyBrakeTorque(brakeForce);
|
|
return;
|
|
}
|
|
|
|
IsTractionLocked = false;
|
|
driftingAxis = Mathf.MoveTowards(driftingAxis, 0f, Time.fixedDeltaTime / 1.5f);
|
|
SetDriftFriction(driftingAxis);
|
|
}
|
|
|
|
private void ApplyDownforce()
|
|
{
|
|
if (!IsAnyWheelGrounded())
|
|
return;
|
|
|
|
body.AddForce(-transform.up * downforce * body.linearVelocity.magnitude, ForceMode.Force);
|
|
}
|
|
|
|
private void UpdateSounds()
|
|
{
|
|
if (!useSounds)
|
|
{
|
|
StopAudio(engineSound);
|
|
StopAudio(tireScreechSound);
|
|
return;
|
|
}
|
|
|
|
if (engineSound != null)
|
|
{
|
|
if (engineSound.clip != null && !engineSound.isPlaying)
|
|
engineSound.Play();
|
|
|
|
engineSound.pitch = initialEnginePitch + body.linearVelocity.magnitude / 25f;
|
|
}
|
|
|
|
bool rearWheelsGrounded = IsWheelGrounded(rearLeftCollider) || IsWheelGrounded(rearRightCollider);
|
|
bool shouldScreech = rearWheelsGrounded && (IsDrifting || (IsTractionLocked && SpeedKmh > EffectsMinSpeedKmh));
|
|
if (tireScreechSound != null)
|
|
{
|
|
if (shouldScreech && tireScreechSound.clip != null && !tireScreechSound.isPlaying)
|
|
tireScreechSound.Play();
|
|
else if (!shouldScreech && tireScreechSound.isPlaying)
|
|
tireScreechSound.Stop();
|
|
}
|
|
}
|
|
|
|
private void UpdateEffects()
|
|
{
|
|
if (!useEffects)
|
|
{
|
|
SetEffectsActive(false);
|
|
return;
|
|
}
|
|
|
|
bool rearLeftGrounded = IsWheelGrounded(rearLeftCollider);
|
|
bool rearRightGrounded = IsWheelGrounded(rearRightCollider);
|
|
bool emitSmoke = IsDrifting && SpeedKmh > EffectsMinSpeedKmh;
|
|
bool emitSkids = (IsTractionLocked || Mathf.Abs(localVelocityX) > SkidVelocityThreshold) && SpeedKmh > EffectsMinSpeedKmh;
|
|
|
|
SetParticlePlaying(rearLeftSmoke, emitSmoke && rearLeftGrounded);
|
|
SetParticlePlaying(rearRightSmoke, emitSmoke && rearRightGrounded);
|
|
SetTrailEmitting(rearLeftSkid, emitSkids && rearLeftGrounded);
|
|
SetTrailEmitting(rearRightSkid, emitSkids && rearRightGrounded);
|
|
}
|
|
|
|
private void UpdateWheelVisuals()
|
|
{
|
|
SyncWheelVisual(frontLeftCollider, frontLeftVisual, frontLeftVisualRotationOffset);
|
|
SyncWheelVisual(frontRightCollider, frontRightVisual, frontRightVisualRotationOffset);
|
|
SyncWheelVisual(rearLeftCollider, rearLeftVisual, rearLeftVisualRotationOffset);
|
|
SyncWheelVisual(rearRightCollider, rearRightVisual, rearRightVisualRotationOffset);
|
|
}
|
|
|
|
private void CacheWheelFriction()
|
|
{
|
|
frontLeftSidewaysFriction = frontLeftCollider != null ? frontLeftCollider.sidewaysFriction : default;
|
|
frontRightSidewaysFriction = frontRightCollider != null ? frontRightCollider.sidewaysFriction : default;
|
|
rearLeftSidewaysFriction = rearLeftCollider != null ? rearLeftCollider.sidewaysFriction : default;
|
|
rearRightSidewaysFriction = rearRightCollider != null ? rearRightCollider.sidewaysFriction : default;
|
|
|
|
frontLeftExtremumSlip = frontLeftSidewaysFriction.extremumSlip;
|
|
frontRightExtremumSlip = frontRightSidewaysFriction.extremumSlip;
|
|
rearLeftExtremumSlip = rearLeftSidewaysFriction.extremumSlip;
|
|
rearRightExtremumSlip = rearRightSidewaysFriction.extremumSlip;
|
|
}
|
|
|
|
private void SetDriftFriction(float drift)
|
|
{
|
|
ApplySidewaysFriction(frontLeftCollider, ref frontLeftSidewaysFriction, frontLeftExtremumSlip, drift);
|
|
ApplySidewaysFriction(frontRightCollider, ref frontRightSidewaysFriction, frontRightExtremumSlip, drift);
|
|
ApplySidewaysFriction(rearLeftCollider, ref rearLeftSidewaysFriction, rearLeftExtremumSlip, drift);
|
|
ApplySidewaysFriction(rearRightCollider, ref rearRightSidewaysFriction, rearRightExtremumSlip, drift);
|
|
}
|
|
|
|
private void ApplySidewaysFriction(WheelCollider wheel, ref WheelFrictionCurve friction, float defaultExtremumSlip, float drift)
|
|
{
|
|
if (wheel == null || !wheel.enabled)
|
|
return;
|
|
|
|
float driftExtremumSlip = defaultExtremumSlip * handbrakeDriftMultiplier;
|
|
friction.extremumSlip = Mathf.Lerp(defaultExtremumSlip, driftExtremumSlip, drift);
|
|
wheel.sidewaysFriction = friction;
|
|
}
|
|
|
|
private void ApplyMotorTorque(float torque)
|
|
{
|
|
SetDriveTorque(frontLeftCollider, torque);
|
|
SetDriveTorque(frontRightCollider, torque);
|
|
SetDriveTorque(rearLeftCollider, torque);
|
|
SetDriveTorque(rearRightCollider, torque);
|
|
}
|
|
|
|
private void ApplyBrakeTorque(float torque)
|
|
{
|
|
SetBrakeTorque(frontLeftCollider, torque);
|
|
SetBrakeTorque(frontRightCollider, torque);
|
|
SetBrakeTorque(rearLeftCollider, torque);
|
|
SetBrakeTorque(rearRightCollider, torque);
|
|
}
|
|
|
|
private static void SetSteerAngle(WheelCollider wheel, float angle, float lerp)
|
|
{
|
|
if (wheel != null && wheel.enabled)
|
|
wheel.steerAngle = Mathf.Lerp(wheel.steerAngle, angle, lerp);
|
|
}
|
|
|
|
private static void SetDriveTorque(WheelCollider wheel, float torque)
|
|
{
|
|
if (wheel != null && wheel.enabled)
|
|
wheel.motorTorque = torque;
|
|
}
|
|
|
|
private static void SetBrakeTorque(WheelCollider wheel, float torque)
|
|
{
|
|
if (wheel != null && wheel.enabled)
|
|
wheel.brakeTorque = torque;
|
|
}
|
|
|
|
private static void SyncWheelVisual(WheelCollider collider, Transform visual, Quaternion visualRotationOffset)
|
|
{
|
|
if (collider == null || !collider.enabled || visual == null)
|
|
return;
|
|
|
|
collider.GetWorldPose(out Vector3 position, out Quaternion rotation);
|
|
visual.SetPositionAndRotation(position, rotation * visualRotationOffset);
|
|
}
|
|
|
|
private void SetEffectsActive(bool active)
|
|
{
|
|
SetParticlePlaying(rearLeftSmoke, active);
|
|
SetParticlePlaying(rearRightSmoke, active);
|
|
SetTrailEmitting(rearLeftSkid, active);
|
|
SetTrailEmitting(rearRightSkid, active);
|
|
}
|
|
|
|
private static void SetParticlePlaying(ParticleSystem particles, bool playing)
|
|
{
|
|
if (particles == null)
|
|
return;
|
|
|
|
if (playing && !particles.isPlaying)
|
|
particles.Play();
|
|
else if (!playing && particles.isPlaying)
|
|
particles.Stop();
|
|
}
|
|
|
|
private static void SetTrailEmitting(TrailRenderer trail, bool emitting)
|
|
{
|
|
if (trail != null)
|
|
trail.emitting = emitting;
|
|
}
|
|
|
|
private static bool IsWheelGrounded(WheelCollider wheel)
|
|
{
|
|
return wheel != null && wheel.enabled && wheel.isGrounded;
|
|
}
|
|
|
|
private bool IsAnyWheelGrounded()
|
|
{
|
|
return IsWheelGrounded(frontLeftCollider) ||
|
|
IsWheelGrounded(frontRightCollider) ||
|
|
IsWheelGrounded(rearLeftCollider) ||
|
|
IsWheelGrounded(rearRightCollider);
|
|
}
|
|
|
|
private static void StopAudio(AudioSource source)
|
|
{
|
|
if (source != null && source.isPlaying)
|
|
source.Stop();
|
|
}
|
|
|
|
private void ValidateRequiredReferences()
|
|
{
|
|
if (frontLeftCollider != null && frontRightCollider != null && rearLeftCollider != null && rearRightCollider != null)
|
|
return;
|
|
|
|
Debug.LogError($"{name} is missing one or more wheel collider references.", this);
|
|
enabled = false;
|
|
}
|
|
|
|
private void CacheVisualRotationOffsets()
|
|
{
|
|
frontLeftVisualRotationOffset = GetVisualRotationOffset(frontLeftCollider, frontLeftVisual);
|
|
frontRightVisualRotationOffset = GetVisualRotationOffset(frontRightCollider, frontRightVisual);
|
|
rearLeftVisualRotationOffset = GetVisualRotationOffset(rearLeftCollider, rearLeftVisual);
|
|
rearRightVisualRotationOffset = GetVisualRotationOffset(rearRightCollider, rearRightVisual);
|
|
}
|
|
|
|
private static Quaternion GetVisualRotationOffset(WheelCollider collider, Transform visual)
|
|
{
|
|
if (collider == null || visual == null)
|
|
return Quaternion.identity;
|
|
|
|
return Quaternion.Inverse(collider.transform.rotation) * visual.rotation;
|
|
}
|
|
|
|
}
|
|
}
|