615 lines
23 KiB
C#
615 lines
23 KiB
C#
using UnityEngine;
|
|
using UnityEngine.AI;
|
|
using ArcadeVP;
|
|
|
|
public class AIController : MonoBehaviour
|
|
{
|
|
public enum AIState { Patrol, Pursue, Searching, Unstuck, Destroyed }
|
|
public enum AITactic { Chase, Intercept }
|
|
|
|
[Header("Target")]
|
|
public ArcadeVehicleController player;
|
|
private CarHealth playerHealth;
|
|
private ArcadeVehicleController aiVehicle;
|
|
|
|
[Header("Current State (Debug)")]
|
|
public AIState currentState = AIState.Patrol;
|
|
public AITactic currentTactic = AITactic.Chase;
|
|
private float tacticTimer = 0f;
|
|
|
|
[Header("Vision & Senses")]
|
|
[Tooltip("Maximum distance the AI can spot the player.")]
|
|
public float visionRange = 120f;
|
|
[Tooltip("Field of view angle. E.g. 120 means 60 degrees left and right.")]
|
|
[Range(0, 360)] public float visionAngle = 120f;
|
|
[Tooltip("Layers that block Line of Sight (buildings, walls).")]
|
|
public LayerMask obstacleLayers;
|
|
|
|
[Header("Search System")]
|
|
[Tooltip("How long the player must be hidden before AI switches to Search mode.")]
|
|
public float timeToLoseTarget = 1.5f;
|
|
[Tooltip("How long to search the last known area before giving up and patrolling.")]
|
|
public float searchDuration = 10.0f;
|
|
|
|
private float sightLostTimer = 0f;
|
|
private float searchTimer = 0f;
|
|
private Vector3 lastKnownPosition;
|
|
|
|
// --- POLICE RADIO (HIVE MIND) ---
|
|
public static Vector3 globalLastKnownPosition;
|
|
public static float globalLastSpottedTime = -999f;
|
|
|
|
[Header("Navigation (NavMesh)")]
|
|
[Tooltip("How often the AI recalculates its path around buildings.")]
|
|
public float pathRecalculationRate = 0.5f;
|
|
private float pathTimer = 0f;
|
|
private NavMeshPath currentPath;
|
|
|
|
[Header("Aggression Settings (Pursuit)")]
|
|
public float forcedTurnSpeed = 8f;
|
|
public float minRamSpeed = 10f;
|
|
|
|
[Header("Unstuck System")]
|
|
private float stuckTimer = 0f;
|
|
private float unstuckReverseTimer = 0f;
|
|
|
|
[Header("Arrest System")]
|
|
[Tooltip("How close the AI must be to pin the player.")]
|
|
public float arrestDistance = 6.5f;
|
|
[Tooltip("If player is moving slower than this, they are considered pinned.")]
|
|
public float playerPinSpeedThreshold = 2f;
|
|
[Tooltip("How many seconds the player must be pinned before they are busted.")]
|
|
public float timeToArrest = 5f;
|
|
private float currentArrestTime = 0f;
|
|
|
|
[Header("Patrol Settings")]
|
|
private Vector3 currentPatrolPoint;
|
|
private bool hasPatrolPoint = false;
|
|
|
|
[Header("Rubber-Banding (Catch-Up)")]
|
|
private float baseMaxSpeed;
|
|
private float baseAcceleration;
|
|
private int aiID;
|
|
|
|
void Start()
|
|
{
|
|
aiID = Random.Range(1, 100000);
|
|
aiVehicle = GetComponent<ArcadeVehicleController>();
|
|
if (aiVehicle != null)
|
|
{
|
|
baseMaxSpeed = aiVehicle.maxSpeed;
|
|
baseAcceleration = aiVehicle.acceleration;
|
|
}
|
|
|
|
currentTactic = (aiID % 3 == 0) ? AITactic.Intercept : AITactic.Chase;
|
|
tacticTimer = Random.Range(0f, 5f);
|
|
currentPath = new NavMeshPath();
|
|
|
|
if (player == null)
|
|
{
|
|
ArcadeVehicleController[] cars = FindObjectsByType<ArcadeVehicleController>(FindObjectsInactive.Exclude);
|
|
foreach (ArcadeVehicleController car in cars)
|
|
{
|
|
if (car.CompareTag("Player"))
|
|
{
|
|
player = car;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (player != null)
|
|
{
|
|
playerHealth = player.GetComponent<CarHealth>();
|
|
lastKnownPosition = player.transform.position; // "scanner" start
|
|
}
|
|
}
|
|
|
|
void FixedUpdate()
|
|
{
|
|
if (player == null || aiVehicle == null || aiVehicle.carBody == null) return;
|
|
|
|
// Pause AI during intro/outro
|
|
if (GameManager.Instance != null && !GameManager.Instance.isGameActive)
|
|
{
|
|
aiVehicle.ProvideInputs(0f, 0f, 1f); // Slam the brakes
|
|
return;
|
|
}
|
|
|
|
// If player is destroyed, ignore them and just patrol
|
|
if (playerHealth != null && playerHealth.isDestroyed)
|
|
{
|
|
currentState = AIState.Patrol;
|
|
ExecutePatrol(aiVehicle.carBody.transform, aiVehicle.carBody.transform.position, aiVehicle.carBody.linearVelocity.magnitude);
|
|
return;
|
|
}
|
|
|
|
Transform aiTransform = aiVehicle.carBody.transform;
|
|
Transform playerTransform = player.carBody != null ? player.carBody.transform : player.transform;
|
|
|
|
Vector3 aiPos = aiTransform.position;
|
|
Vector3 playerPos = playerTransform.position;
|
|
float currentSpeed = aiVehicle.carBody.linearVelocity.magnitude;
|
|
|
|
// --- 1. UNSTUCK RECOVERY ---
|
|
if (unstuckReverseTimer > 0f)
|
|
{
|
|
unstuckReverseTimer -= Time.fixedDeltaTime;
|
|
aiVehicle.ProvideInputs(0f, -1f, 0f); // Reverse straight back
|
|
return;
|
|
}
|
|
|
|
// --- ARREST / PINNING SYSTEM ---
|
|
if (currentState == AIState.Pursue)
|
|
{
|
|
float playerSpeed = player.carBody != null ? player.carBody.linearVelocity.magnitude : player.carVelocity.magnitude;
|
|
float distToPlayer = Vector3.Distance(aiPos, playerPos);
|
|
|
|
// If the AI is practically touching the player, and the player is barely moving
|
|
if (distToPlayer < arrestDistance && playerSpeed < playerPinSpeedThreshold)
|
|
{
|
|
currentArrestTime += Time.fixedDeltaTime;
|
|
|
|
if (currentArrestTime >= timeToArrest)
|
|
{
|
|
if (GameManager.Instance != null && !GameManager.Instance.isGameOver)
|
|
{
|
|
GameManager.Instance.Busted();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
currentArrestTime = 0f; // Reset if player escapes or speeds up
|
|
}
|
|
}
|
|
else
|
|
{
|
|
currentArrestTime = 0f;
|
|
}
|
|
|
|
if (currentSpeed < 1.0f && currentState != AIState.Patrol)
|
|
{
|
|
stuckTimer += Time.fixedDeltaTime;
|
|
if (stuckTimer > 1.5f)
|
|
{
|
|
unstuckReverseTimer = 1.5f;
|
|
stuckTimer = 0f;
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
stuckTimer = 0f;
|
|
}
|
|
|
|
// --- 2. VISION & STATE MACHINE (Radio Integrated) ---
|
|
bool hasLoS = CheckLineOfSight(aiTransform, playerTransform);
|
|
|
|
if (hasLoS)
|
|
{
|
|
currentState = AIState.Pursue;
|
|
sightLostTimer = 0f;
|
|
lastKnownPosition = playerPos;
|
|
|
|
// Broadcast to all other units over the radio!
|
|
globalLastKnownPosition = playerPos;
|
|
globalLastSpottedTime = Time.time;
|
|
}
|
|
else if (Time.time - globalLastSpottedTime < 2.0f)
|
|
{
|
|
// We don't have direct Line of Sight, but ANOTHER cop does and is calling it in over the radio!
|
|
if (currentState != AIState.Pursue)
|
|
{
|
|
currentState = AIState.Pursue; // Jump back into the chase!
|
|
}
|
|
sightLostTimer = 0f;
|
|
lastKnownPosition = globalLastKnownPosition;
|
|
// The pursuit logic will automatically use NavMesh to drive to this radioed position (or intercept it)
|
|
}
|
|
else if (currentState == AIState.Pursue)
|
|
{
|
|
// No one has seen the player recently. Start the grace period.
|
|
sightLostTimer += Time.fixedDeltaTime;
|
|
|
|
// Interceptors intentionally break line of sight to take side streets, so they get a massive memory buffer.
|
|
float allowedLostTime = (currentTactic == AITactic.Intercept) ? 15f : timeToLoseTarget;
|
|
|
|
if (sightLostTimer >= allowedLostTime)
|
|
{
|
|
currentState = AIState.Searching;
|
|
searchTimer = 0f;
|
|
}
|
|
}
|
|
else if (currentState == AIState.Searching)
|
|
{
|
|
searchTimer += Time.fixedDeltaTime;
|
|
if (searchTimer >= searchDuration)
|
|
{
|
|
currentState = AIState.Patrol; // Give up
|
|
}
|
|
}
|
|
|
|
// --- 3. EXECUTE BEHAVIOR BASED ON STATE ---
|
|
if (currentState == AIState.Pursue)
|
|
{
|
|
ExecutePursuit(aiTransform, aiPos, playerPos, currentSpeed);
|
|
}
|
|
else if (currentState == AIState.Searching)
|
|
{
|
|
ExecuteSearch(aiTransform, aiPos, currentSpeed);
|
|
}
|
|
else if (currentState == AIState.Patrol)
|
|
{
|
|
ExecutePatrol(aiTransform, aiPos, currentSpeed);
|
|
}
|
|
}
|
|
|
|
private bool CheckLineOfSight(Transform aiTransform, Transform playerTransform)
|
|
{
|
|
Vector3 dirToPlayer = playerTransform.position - aiTransform.position;
|
|
float dist = dirToPlayer.magnitude;
|
|
|
|
// Out of absolute range
|
|
if (dist > visionRange) return false;
|
|
|
|
// Check Field of View
|
|
float angle = Vector3.Angle(aiTransform.forward, dirToPlayer);
|
|
if (angle > visionAngle * 0.5f)
|
|
{
|
|
// Proximity sense (hearing/radar scanner): Even if behind them, if very close they detect you
|
|
if (dist > 15f) return false;
|
|
}
|
|
|
|
// Raycast to check for buildings/walls blocking the view
|
|
Vector3 rayStart = aiTransform.position + Vector3.up * 1f;
|
|
Vector3 rayEnd = playerTransform.position + Vector3.up * 1f;
|
|
Vector3 rayDir = (rayEnd - rayStart).normalized;
|
|
|
|
if (Physics.Raycast(rayStart, rayDir, out RaycastHit hit, dist, obstacleLayers))
|
|
{
|
|
if (!hit.collider.CompareTag("Player") && !hit.collider.transform.IsChildOf(playerTransform))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private Vector3 GetNavMeshWaypoint(Vector3 start, Vector3 destination)
|
|
{
|
|
pathTimer += Time.fixedDeltaTime;
|
|
if (pathTimer >= pathRecalculationRate || currentPath == null || currentPath.corners.Length == 0)
|
|
{
|
|
pathTimer = 0f;
|
|
NavMesh.CalculatePath(start, destination, NavMesh.AllAreas, currentPath);
|
|
}
|
|
|
|
if (currentPath != null && currentPath.corners.Length > 1)
|
|
{
|
|
// corners[0] is our position. corners[1] is the next turn/waypoint
|
|
Vector3 nextCorner = currentPath.corners[1];
|
|
|
|
// If we are very close to the next corner, aim for the one after it to smooth steering
|
|
if (Vector3.Distance(start, nextCorner) < 8f && currentPath.corners.Length > 2)
|
|
{
|
|
nextCorner = currentPath.corners[2];
|
|
}
|
|
return nextCorner;
|
|
}
|
|
|
|
// Fallback to straight line if NavMesh fails
|
|
return destination;
|
|
}
|
|
|
|
private void ExecutePursuit(Transform aiTransform, Vector3 aiPos, Vector3 playerPos, float currentSpeed)
|
|
{
|
|
Vector3 playerVelocity = player.carBody != null ? player.carBody.linearVelocity : Vector3.zero;
|
|
float distance = Vector3.Distance(aiPos, playerPos);
|
|
|
|
// --- 0. TACTIC EVALUATION ---
|
|
tacticTimer += Time.fixedDeltaTime;
|
|
if (tacticTimer > 8f)
|
|
{
|
|
tacticTimer = 0f;
|
|
// 30% chance to become an Interceptor if not too close, else revert to Chase
|
|
if (distance > 25f && Random.value < 0.3f)
|
|
{
|
|
currentTactic = AITactic.Intercept;
|
|
}
|
|
else
|
|
{
|
|
currentTactic = AITactic.Chase;
|
|
}
|
|
|
|
// Safety fallback: if an interceptor gets incredibly far away (e.g. took a wrong turn), force chase
|
|
if (currentTactic == AITactic.Intercept && distance > 70f)
|
|
{
|
|
currentTactic = AITactic.Chase;
|
|
}
|
|
}
|
|
|
|
// --- 1. DYNAMIC RUBBER-BANDING (The "Always a Threat" System) ---
|
|
if (distance > 45f)
|
|
{
|
|
float multiplier = Mathf.Clamp(distance / 45f, 1f, 2.2f);
|
|
aiVehicle.maxSpeed = baseMaxSpeed * multiplier;
|
|
aiVehicle.acceleration = baseAcceleration * (multiplier * 1.5f);
|
|
}
|
|
else
|
|
{
|
|
aiVehicle.maxSpeed = baseMaxSpeed;
|
|
aiVehicle.acceleration = baseAcceleration;
|
|
}
|
|
|
|
// If close and have line of sight, bypass NavMesh and attack directly!
|
|
bool isCloseAndVisible = distance < 40f && CheckLineOfSight(aiTransform, player.carBody != null ? player.carBody.transform : player.transform);
|
|
|
|
// Interceptors rely on NavMesh to cut ahead unless they are practically on top of the player
|
|
if (currentTactic == AITactic.Intercept && distance > 20f)
|
|
{
|
|
isCloseAndVisible = false;
|
|
}
|
|
|
|
if (isCloseAndVisible)
|
|
{
|
|
Vector3 targetPoint = playerPos;
|
|
Transform playerTransform = player.carBody != null ? player.carBody.transform : player.transform;
|
|
Vector3 localAIPos = playerTransform.InverseTransformPoint(aiPos);
|
|
|
|
// --- 2. AGGRESSIVE FLANKING & PINNING ---
|
|
if (localAIPos.z > 0f && distance < 15f)
|
|
{
|
|
// We are IN FRONT of the player. BRAKE CHECK!
|
|
// Target is placed directly behind us (at player's position) so angle is 180, causing AI to slam brakes
|
|
targetPoint = playerPos;
|
|
}
|
|
else if (localAIPos.z > -8f && Mathf.Abs(localAIPos.x) > 1.5f && distance < 15f)
|
|
{
|
|
// We are ALONGSIDE the player. T-BONE / SIDESWIPE (The NFS UC Box-In)
|
|
if (localAIPos.x < 0)
|
|
{
|
|
// AI is on the left, violently ram the player's front-left quarter panel
|
|
targetPoint = playerPos + playerTransform.forward * 2f + playerTransform.right * 1f;
|
|
}
|
|
else
|
|
{
|
|
// AI is on the right, violently ram the player's front-right quarter panel
|
|
targetPoint = playerPos + playerTransform.forward * 2f - playerTransform.right * 1f;
|
|
}
|
|
}
|
|
else if (localAIPos.z < -2f && localAIPos.z > -12f && Mathf.Abs(localAIPos.x) <= 3f && distance < 15f)
|
|
{
|
|
// We are DIRECTLY BEHIND the player. PIT MANEUVER.
|
|
float pitOffset = (aiID % 2 == 0) ? -2.0f : 2.0f;
|
|
targetPoint = playerPos - playerTransform.forward * 1.5f + playerTransform.right * pitOffset;
|
|
}
|
|
else
|
|
{
|
|
// General Ramming / Catching up
|
|
float leadTime = Mathf.Clamp(distance / Mathf.Max(currentSpeed, minRamSpeed), 0f, 1.5f);
|
|
targetPoint = playerPos + (playerVelocity * leadTime);
|
|
|
|
// Flocking Offset so they don't drive in a single file line
|
|
float sideOffset = (aiID % 3 == 0) ? 0f : (aiID % 2 == 0 ? -3.0f : 3.0f);
|
|
targetPoint += playerTransform.right * sideOffset;
|
|
}
|
|
|
|
DriveTowardsPoint(aiTransform, aiPos, targetPoint, currentSpeed, true);
|
|
}
|
|
else
|
|
{
|
|
// --- 3. PREDICTIVE INTERCEPT PATHING (No More Conga Lines) ---
|
|
// Interceptors predict 4.5s ahead (taking side streets to cut off). Chasers predict 2.5s ahead.
|
|
float predictionTime = (currentTactic == AITactic.Intercept) ? 4.5f : 2.5f;
|
|
Vector3 predictedPos = playerPos + (playerVelocity * predictionTime);
|
|
Vector3 waypoint = GetNavMeshWaypoint(aiPos, predictedPos);
|
|
DriveTowardsPoint(aiTransform, aiPos, waypoint, currentSpeed, true);
|
|
|
|
// Draw path for debugging
|
|
if (currentPath != null && currentPath.corners.Length > 1)
|
|
{
|
|
for (int i = 0; i < currentPath.corners.Length - 1; i++)
|
|
Debug.DrawLine(currentPath.corners[i], currentPath.corners[i+1], Color.magenta);
|
|
}
|
|
}
|
|
|
|
lastKnownPosition = playerPos;
|
|
}
|
|
|
|
private void ExecuteSearch(Transform aiTransform, Vector3 aiPos, float currentSpeed)
|
|
{
|
|
// Use NavMesh to safely drive to the last known position
|
|
Vector3 waypoint = GetNavMeshWaypoint(aiPos, lastKnownPosition);
|
|
DriveTowardsPoint(aiTransform, aiPos, waypoint, currentSpeed, false);
|
|
}
|
|
|
|
private void ExecutePatrol(Transform aiTransform, Vector3 aiPos, float currentSpeed)
|
|
{
|
|
if (!hasPatrolPoint)
|
|
{
|
|
hasPatrolPoint = FindNewPatrolPoint(aiPos, aiTransform);
|
|
}
|
|
|
|
if (hasPatrolPoint)
|
|
{
|
|
Vector3 waypoint = GetNavMeshWaypoint(aiPos, currentPatrolPoint);
|
|
DriveTowardsPoint(aiTransform, aiPos, waypoint, currentSpeed, false);
|
|
|
|
// Reached point or stuck
|
|
if (Vector3.Distance(aiPos, currentPatrolPoint) < 15f || stuckTimer > 1.0f)
|
|
{
|
|
if (PatrolManager.Instance != null) PatrolManager.Instance.UnregisterTarget(this);
|
|
hasPatrolPoint = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Fallback Cruise
|
|
float maxSpeedMs = 30f / 3.6f;
|
|
float throttle = currentSpeed > maxSpeedMs ? 0f : 0.4f;
|
|
float brake = currentSpeed > maxSpeedMs + 2f ? 0.5f : 0f;
|
|
aiVehicle.ProvideInputs(0.1f, throttle, brake);
|
|
}
|
|
}
|
|
|
|
private bool FindNewPatrolPoint(Vector3 aiPos, Transform aiTransform)
|
|
{
|
|
// Pick a completely random point in front of us using NavMesh
|
|
Vector3 randomDir = Quaternion.Euler(0, Random.Range(-90f, 90f), 0) * aiTransform.forward;
|
|
Vector3 randomPos = aiPos + (randomDir * Random.Range(60f, 150f));
|
|
|
|
// Snap it to the nearest valid NavMesh location (road, drivable, etc)
|
|
if (NavMesh.SamplePosition(randomPos, out NavMeshHit hit, 50f, NavMesh.AllAreas))
|
|
{
|
|
if (PatrolManager.Instance != null)
|
|
{
|
|
if (!PatrolManager.Instance.IsPointCrowded(hit.position, this))
|
|
{
|
|
currentPatrolPoint = hit.position;
|
|
PatrolManager.Instance.RegisterTarget(this, hit.position);
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
currentPatrolPoint = hit.position;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void DriveTowardsPoint(Transform aiTransform, Vector3 aiPos, Vector3 targetPoint, float currentSpeed, bool aggressive)
|
|
{
|
|
Vector3 dirToTarget = (targetPoint - aiPos).normalized;
|
|
dirToTarget.y = 0;
|
|
|
|
if (dirToTarget.sqrMagnitude < 0.01f)
|
|
{
|
|
aiVehicle.ProvideInputs(0f, 0.5f, 0f);
|
|
return;
|
|
}
|
|
|
|
// --- 1. ANTI-CRASH AVOIDANCE & LANE OFFSET ---
|
|
Vector3 avoidanceOffset = Vector3.zero;
|
|
bool needsToBrakeForAlly = false;
|
|
float avoidDist = aggressive ? 15f : 20f;
|
|
|
|
// Apply a persistent lane offset so they naturally spread out across the road instead of forming a conga line
|
|
if (aggressive)
|
|
{
|
|
float laneOffset = (aiID % 3 == 0) ? 0f : (aiID % 2 == 0 ? -1.2f : 1.2f);
|
|
avoidanceOffset += aiTransform.right * laneOffset;
|
|
}
|
|
|
|
// Check if player is assigned before accessing
|
|
Transform playerT = player != null && player.carBody != null ? player.carBody.transform : (player != null ? player.transform : null);
|
|
|
|
// Ray Center
|
|
if (Physics.Raycast(aiPos + Vector3.up, aiTransform.forward, out RaycastHit hitC, avoidDist))
|
|
{
|
|
if (hitC.collider.GetComponentInParent<AIController>() != null)
|
|
{
|
|
needsToBrakeForAlly = true;
|
|
avoidanceOffset += aiTransform.right * (Random.value > 0.5f ? 2.5f : -2.5f);
|
|
}
|
|
else if (playerT != null && !hitC.collider.CompareTag("Player") && !hitC.collider.transform.IsChildOf(playerT))
|
|
{
|
|
avoidanceOffset += hitC.normal * 2.5f; // Steer away from wall ahead
|
|
}
|
|
}
|
|
|
|
// Ray Left
|
|
if (Physics.Raycast(aiPos + Vector3.up - aiTransform.right * 1.5f, aiTransform.forward, out RaycastHit hitL, avoidDist))
|
|
{
|
|
if (hitL.collider.GetComponentInParent<AIController>() != null)
|
|
{
|
|
avoidanceOffset += aiTransform.right * 2.5f;
|
|
if (hitL.distance < 10f) needsToBrakeForAlly = true;
|
|
}
|
|
else if (playerT != null && !hitL.collider.CompareTag("Player") && !hitL.collider.transform.IsChildOf(playerT))
|
|
{
|
|
avoidanceOffset += aiTransform.right * 2.5f; // Wall on left, steer right
|
|
}
|
|
}
|
|
|
|
// Ray Right
|
|
if (Physics.Raycast(aiPos + Vector3.up + aiTransform.right * 1.5f, aiTransform.forward, out RaycastHit hitR, avoidDist))
|
|
{
|
|
if (hitR.collider.GetComponentInParent<AIController>() != null)
|
|
{
|
|
avoidanceOffset -= aiTransform.right * 2.5f;
|
|
if (hitR.distance < 10f) needsToBrakeForAlly = true;
|
|
}
|
|
else if (playerT != null && !hitR.collider.CompareTag("Player") && !hitR.collider.transform.IsChildOf(playerT))
|
|
{
|
|
avoidanceOffset -= aiTransform.right * 2.5f; // Wall on right, steer left
|
|
}
|
|
}
|
|
|
|
if (avoidanceOffset != Vector3.zero)
|
|
{
|
|
dirToTarget = (dirToTarget + avoidanceOffset).normalized;
|
|
}
|
|
|
|
// --- 2. STEERING & THROTTLE ---
|
|
float angleToTarget = Vector3.SignedAngle(aiTransform.forward, dirToTarget, Vector3.up);
|
|
float steerInput = Mathf.Clamp(angleToTarget / 40f, -1f, 1f);
|
|
|
|
float throttleInput = aggressive ? 1f : 0.6f;
|
|
float brakeInput = 0f;
|
|
|
|
if (Mathf.Abs(angleToTarget) > 100f)
|
|
{
|
|
if (currentSpeed > 5f)
|
|
{
|
|
throttleInput = 0f;
|
|
brakeInput = 1f;
|
|
}
|
|
else
|
|
{
|
|
throttleInput = -1f;
|
|
}
|
|
}
|
|
else if (Mathf.Abs(angleToTarget) > 60f)
|
|
{
|
|
throttleInput = aggressive ? 0.5f : 0.3f;
|
|
}
|
|
|
|
// --- 3. SPEED LIMITERS & AVOIDANCE BRAKING ---
|
|
if (needsToBrakeForAlly)
|
|
{
|
|
throttleInput = 0f;
|
|
brakeInput = aggressive ? 0.4f : 0.8f; // Try not to rear-end the guy in front
|
|
}
|
|
else if (!aggressive)
|
|
{
|
|
float maxSpeedMs = 30f / 3.6f;
|
|
if (currentSpeed > maxSpeedMs)
|
|
{
|
|
throttleInput = 0f;
|
|
if (currentSpeed > maxSpeedMs + 2f) brakeInput = 0.5f;
|
|
}
|
|
}
|
|
|
|
aiVehicle.ProvideInputs(steerInput, throttleInput, brakeInput);
|
|
|
|
// --- 4. FORCED ROTATION (Overdrive Steering) ---
|
|
// Don't force rotation if we are actively swerving to avoid a buddy
|
|
if (!needsToBrakeForAlly && (aggressive || currentSpeed < 2f) && Mathf.Abs(angleToTarget) > 2f)
|
|
{
|
|
Quaternion targetRotation = Quaternion.LookRotation(dirToTarget, Vector3.up);
|
|
Vector3 currentEuler = aiTransform.rotation.eulerAngles;
|
|
|
|
// Cheat: If aggressive (ramming), snap the steering much faster than natural physics allows
|
|
float actualTurnSpeed = aggressive ? forcedTurnSpeed * 1.8f : forcedTurnSpeed;
|
|
float newY = Mathf.LerpAngle(currentEuler.y, targetRotation.eulerAngles.y, actualTurnSpeed * Time.fixedDeltaTime);
|
|
|
|
aiVehicle.carBody.MoveRotation(Quaternion.Euler(currentEuler.x, newY, currentEuler.z));
|
|
}
|
|
}
|
|
}
|