Files
CartoonCopChase/Assets/Scripts/AIController.cs
2026-08-28 16:09:06 +01:00

445 lines
16 KiB
C#

using UnityEngine;
using UnityEngine.AI;
using ArcadeVP;
public class AIController : MonoBehaviour
{
public enum AIState { Patrol, Pursue, Searching, Unstuck, Destroyed }
[Header("Target")]
public ArcadeVehicleController player;
private CarHealth playerHealth;
private ArcadeVehicleController aiVehicle;
[Header("Current State (Debug)")]
public AIState currentState = AIState.Patrol;
[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;
[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("Patrol Settings")]
private Vector3 currentPatrolPoint;
private bool hasPatrolPoint = false;
void Start()
{
aiVehicle = GetComponent<ArcadeVehicleController>();
currentPath = new NavMeshPath();
if (player == null)
{
ArcadeVehicleController[] cars = FindObjectsByType<ArcadeVehicleController>(FindObjectsSortMode.None);
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;
// 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;
}
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 ---
bool hasLoS = CheckLineOfSight(aiTransform, playerTransform);
if (hasLoS)
{
currentState = AIState.Pursue;
sightLostTimer = 0f;
lastKnownPosition = playerPos;
}
else if (currentState == AIState.Pursue)
{
// Grace period before completely losing the player (e.g. driving behind a single tree)
sightLostTimer += Time.fixedDeltaTime;
if (sightLostTimer >= timeToLoseTarget)
{
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);
// If close and have line of sight, bypass NavMesh and attack directly!
bool isCloseAndVisible = distance < 30f && CheckLineOfSight(aiTransform, player.carBody != null ? player.carBody.transform : player.transform);
if (isCloseAndVisible)
{
Vector3 targetPoint = playerPos;
Transform playerTransform = player.carBody != null ? player.carBody.transform : player.transform;
Vector3 localAIPos = playerTransform.InverseTransformPoint(aiPos);
// PIT Maneuver Check
if (localAIPos.z < 0f && localAIPos.z > -6f && Mathf.Abs(localAIPos.x) > 1.2f && distance < 12f)
{
Transform targetWheel = player.rearWheels[0];
if (player.rearWheels.Length > 1 && Vector3.Distance(aiPos, player.rearWheels[1].position) < Vector3.Distance(aiPos, targetWheel.position))
{
targetWheel = player.rearWheels[1];
}
Vector3 pushDirection = (playerPos - targetWheel.position).normalized;
targetPoint = targetWheel.position + (playerTransform.forward * 0.5f) + (pushDirection * 0.5f);
}
else
{
// Ramming Check
float leadTime = Mathf.Clamp(distance / Mathf.Max(currentSpeed, minRamSpeed), 0f, 1.0f);
targetPoint = playerPos + (playerVelocity * leadTime);
// Offset the target so they don't all aim for the exact same pixel (Flocking/V-Formation)
float sideOffset = (GetInstanceID() % 3 == 0) ? 0f : (GetInstanceID() % 2 == 0 ? -2.5f : 2.5f);
targetPoint += playerTransform.right * sideOffset;
}
DriveTowardsPoint(aiTransform, aiPos, targetPoint, currentSpeed, true);
}
else
{
// Distant pursuit: Use NavMesh to navigate city streets without hitting buildings
Vector3 waypoint = GetNavMeshWaypoint(aiPos, playerPos);
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 (Avoid other cops!) ---
Vector3 avoidanceOffset = Vector3.zero;
bool needsToBrakeForAlly = false;
float avoidDist = aggressive ? 12f : 20f;
// 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 ? 1.5f : -1.5f);
}
}
// 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 * 1.5f;
if (hitL.distance < 8f) needsToBrakeForAlly = true;
}
}
// 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 * 1.5f;
if (hitR.distance < 8f) needsToBrakeForAlly = true;
}
}
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 ---
// 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;
float newY = Mathf.LerpAngle(currentEuler.y, targetRotation.eulerAngles.y, forcedTurnSpeed * Time.fixedDeltaTime);
aiVehicle.carBody.MoveRotation(Quaternion.Euler(currentEuler.x, newY, currentEuler.z));
}
}
}