459 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;
[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; }
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>();
AutoFindReferences();
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
throttleInput = Input.GetAxisRaw("Vertical");
steeringInput = Input.GetAxisRaw("Horizontal");
handbrakeInput = Input.GetKey(KeyCode.Space);
}
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)
{
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()
{
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.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.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)
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.steerAngle = Mathf.Lerp(wheel.steerAngle, angle, lerp);
}
private static void SetDriveTorque(WheelCollider wheel, float torque)
{
if (wheel != null)
wheel.motorTorque = torque;
}
private static void SetBrakeTorque(WheelCollider wheel, float torque)
{
if (wheel != null)
wheel.brakeTorque = torque;
}
private static void SyncWheelVisual(WheelCollider collider, Transform visual, Quaternion visualRotationOffset)
{
if (collider == null || 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.isGrounded;
}
private static void StopAudio(AudioSource source)
{
if (source != null && source.isPlaying)
source.Stop();
}
private void AutoFindReferences()
{
frontLeftCollider ??= FindChildComponent<WheelCollider>("WheelCollider_FL");
frontRightCollider ??= FindChildComponent<WheelCollider>("WheelCollider_FR");
rearLeftCollider ??= FindChildComponent<WheelCollider>("WheelCollider_RL");
rearRightCollider ??= FindChildComponent<WheelCollider>("WheelCollider_RR");
frontLeftVisual ??= FindFirstChildTransform("F_L", "WheelVisual_FL");
frontRightVisual ??= FindFirstChildTransform("F_R", "WheelVisual_FR");
rearLeftVisual ??= FindFirstChildTransform("B_L", "WheelVisual_RL");
rearRightVisual ??= FindFirstChildTransform("B_R", "WheelVisual_RR");
rearLeftSmoke ??= FindChildComponent<ParticleSystem>("LeftTireSmoke_PS");
rearRightSmoke ??= FindChildComponent<ParticleSystem>("RightTireSmoke_PS");
rearLeftSkid ??= FindChildComponent<TrailRenderer>("LeftTireSkid");
rearRightSkid ??= FindChildComponent<TrailRenderer>("RightTireSkid");
}
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;
}
private T FindChildComponent<T>(string childName) where T : Component
{
Transform child = FindChildTransform(childName);
return child != null ? child.GetComponent<T>() : null;
}
private Transform FindChildTransform(string childName)
{
Transform[] children = GetComponentsInChildren<Transform>(true);
for (int i = 0; i < children.Length; i++)
{
if (children[i].name == childName)
return children[i];
}
return null;
}
private Transform FindFirstChildTransform(params string[] childNames)
{
for (int i = 0; i < childNames.Length; i++)
{
Transform child = FindChildTransform(childNames[i]);
if (child != null)
return child;
}
return null;
}
}
}