2026-08-12 12:11:13 +03:00

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;
}
}
}