445 lines
16 KiB
C#
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));
|
|
}
|
|
}
|
|
}
|