using RaceFields.Vehicles; using UnityEngine; namespace RaceFields.Harvesters.AI { [RequireComponent(typeof(Rigidbody))] public sealed class HarvesterChaseController : MonoBehaviour { [Header("Target")] [SerializeField] private Transform target; [SerializeField] private bool autoFindPlayer = true; [SerializeField] private float targetSearchInterval = 0.35f; [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("Movement")] [SerializeField] private float maxSpeedKmh = 55f; [SerializeField] private float motorTorque = 30000f; [SerializeField] private float brakeTorque = 6500f; [SerializeField] private float maxSteerAngle = 28f; [SerializeField] private float steeringResponse = 4f; [SerializeField] private float stoppingDistance = 1.4f; [SerializeField] private float reverseDistance = 0.7f; [SerializeField] private float downforce = 45f; [Header("Stability")] [SerializeField] private Vector3 centerOfMass = new Vector3(0f, -0.65f, 0.2f); private Rigidbody body; private float nextTargetSearchTime; private float currentSteerAngle; private void Awake() { body = GetComponent(); AutoFindReferences(); body.centerOfMass = centerOfMass; body.interpolation = RigidbodyInterpolation.Interpolate; body.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic; } private void FixedUpdate() { ResolveTarget(); UpdateDriving(); ApplyDownforce(); } private void Update() { UpdateWheelVisuals(); } public void SetTarget(Transform newTarget) { target = newTarget; nextTargetSearchTime = Time.time + targetSearchInterval; } private void UpdateDriving() { if (target == null) { ApplyBrake(brakeTorque); ApplyMotor(0f); SetSteering(0f); return; } Vector3 localTarget = transform.InverseTransformPoint(target.position); localTarget.y = 0f; float distance = localTarget.magnitude; if (distance < 0.1f) { ApplyBrake(brakeTorque); ApplyMotor(0f); return; } float speedKmh = body.linearVelocity.magnitude * 3.6f; float targetAngle = Mathf.Atan2(localTarget.x, localTarget.z) * Mathf.Rad2Deg; float desiredSteer = Mathf.Clamp(targetAngle, -maxSteerAngle, maxSteerAngle); if (localTarget.z < 0f && Mathf.Abs(localTarget.x) < 0.5f) desiredSteer = currentSteerAngle >= 0f ? maxSteerAngle : -maxSteerAngle; SetSteering(desiredSteer); if (distance <= reverseDistance) { ApplyBrake(0f); ApplyMotor(-motorTorque * 0.45f); return; } if (distance <= stoppingDistance) { ApplyBrake(brakeTorque); ApplyMotor(0f); return; } float angleFactor = Mathf.Clamp(1f - Mathf.Abs(targetAngle) / 130f * 0.5f, 0.65f, 1f); float speedFactor = speedKmh < maxSpeedKmh ? 1f : 0f; float torque = motorTorque * angleFactor * speedFactor; ApplyBrake(0f); ApplyMotor(torque); } private void SetSteering(float desiredAngle) { currentSteerAngle = Mathf.Lerp(currentSteerAngle, desiredAngle, steeringResponse * Time.fixedDeltaTime); if (frontLeftCollider != null) frontLeftCollider.steerAngle = currentSteerAngle; if (frontRightCollider != null) frontRightCollider.steerAngle = currentSteerAngle; } private void ApplyMotor(float torque) { SetMotorTorque(frontLeftCollider, torque * 0.5f); SetMotorTorque(frontRightCollider, torque * 0.5f); SetMotorTorque(rearLeftCollider, torque); SetMotorTorque(rearRightCollider, torque); } private void ApplyBrake(float torque) { SetBrakeTorque(frontLeftCollider, torque); SetBrakeTorque(frontRightCollider, torque); SetBrakeTorque(rearLeftCollider, torque); SetBrakeTorque(rearRightCollider, torque); } private void ApplyDownforce() { body.AddForce(-transform.up * downforce * body.linearVelocity.magnitude, ForceMode.Force); } private void UpdateWheelVisuals() { SyncWheelVisual(frontLeftCollider, frontLeftVisual); SyncWheelVisual(frontRightCollider, frontRightVisual); SyncWheelVisual(rearLeftCollider, rearLeftVisual); SyncWheelVisual(rearRightCollider, rearRightVisual); } private void ResolveTarget() { if (target != null || !autoFindPlayer) return; if (Time.time < nextTargetSearchTime) return; nextTargetSearchTime = Time.time + targetSearchInterval; ArcadeBuggyController player = Object.FindFirstObjectByType(); if (player != null) target = player.transform; } private void AutoFindReferences() { frontLeftCollider ??= FindChildComponent("WheelCollider_FL"); frontRightCollider ??= FindChildComponent("WheelCollider_FR"); rearLeftCollider ??= FindChildComponent("WheelCollider_RL"); rearRightCollider ??= FindChildComponent("WheelCollider_RR"); frontLeftVisual ??= FindChildTransform("H_Wheel_FL"); frontRightVisual ??= FindChildTransform("H_Wheel_FR"); rearLeftVisual ??= FindChildTransform("H_Wheel_RL"); rearRightVisual ??= FindChildTransform("H_Wheel_RR"); } private T FindChildComponent(string childName) where T : Component { Transform child = FindChildTransform(childName); return child != null ? child.GetComponent() : null; } private Transform FindChildTransform(string childName) { Transform[] children = GetComponentsInChildren(true); for (int i = 0; i < children.Length; i++) { if (children[i].name == childName) return children[i]; } return null; } private static void SetMotorTorque(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) { if (collider == null || visual == null) return; collider.GetWorldPose(out Vector3 position, out Quaternion rotation); visual.SetPositionAndRotation(position, rotation); } } }