Building out Golf Level and mission states and improved AI
This commit is contained in:
@@ -5,6 +5,7 @@ using ArcadeVP;
|
||||
public class AIController : MonoBehaviour
|
||||
{
|
||||
public enum AIState { Patrol, Pursue, Searching, Unstuck, Destroyed }
|
||||
public enum AITactic { Chase, Intercept }
|
||||
|
||||
[Header("Target")]
|
||||
public ArcadeVehicleController player;
|
||||
@@ -13,6 +14,8 @@ public class AIController : MonoBehaviour
|
||||
|
||||
[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.")]
|
||||
@@ -32,6 +35,10 @@ public class AIController : MonoBehaviour
|
||||
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;
|
||||
@@ -46,17 +53,41 @@ public class AIController : MonoBehaviour
|
||||
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>(FindObjectsSortMode.None);
|
||||
ArcadeVehicleController[] cars = FindObjectsByType<ArcadeVehicleController>(FindObjectsInactive.Exclude);
|
||||
foreach (ArcadeVehicleController car in cars)
|
||||
{
|
||||
if (car.CompareTag("Player"))
|
||||
@@ -78,6 +109,13 @@ public class AIController : MonoBehaviour
|
||||
{
|
||||
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)
|
||||
{
|
||||
@@ -101,6 +139,35 @@ public class AIController : MonoBehaviour
|
||||
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;
|
||||
@@ -116,7 +183,7 @@ public class AIController : MonoBehaviour
|
||||
stuckTimer = 0f;
|
||||
}
|
||||
|
||||
// --- 2. VISION & STATE MACHINE ---
|
||||
// --- 2. VISION & STATE MACHINE (Radio Integrated) ---
|
||||
bool hasLoS = CheckLineOfSight(aiTransform, playerTransform);
|
||||
|
||||
if (hasLoS)
|
||||
@@ -124,12 +191,31 @@ public class AIController : MonoBehaviour
|
||||
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)
|
||||
{
|
||||
// Grace period before completely losing the player (e.g. driving behind a single tree)
|
||||
// No one has seen the player recently. Start the grace period.
|
||||
sightLostTimer += Time.fixedDeltaTime;
|
||||
if (sightLostTimer >= timeToLoseTarget)
|
||||
|
||||
// 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;
|
||||
@@ -222,8 +308,49 @@ public class AIController : MonoBehaviour
|
||||
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 < 30f && CheckLineOfSight(aiTransform, player.carBody != null ? player.carBody.transform : player.transform);
|
||||
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)
|
||||
{
|
||||
@@ -231,26 +358,41 @@ public class AIController : MonoBehaviour
|
||||
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)
|
||||
// --- 2. AGGRESSIVE FLANKING & PINNING ---
|
||||
if (localAIPos.z > 0f && distance < 15f)
|
||||
{
|
||||
Transform targetWheel = player.rearWheels[0];
|
||||
if (player.rearWheels.Length > 1 && Vector3.Distance(aiPos, player.rearWheels[1].position) < Vector3.Distance(aiPos, targetWheel.position))
|
||||
// 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)
|
||||
{
|
||||
targetWheel = player.rearWheels[1];
|
||||
// AI is on the left, violently ram the player's front-left quarter panel
|
||||
targetPoint = playerPos + playerTransform.forward * 2f + playerTransform.right * 1f;
|
||||
}
|
||||
|
||||
Vector3 pushDirection = (playerPos - targetWheel.position).normalized;
|
||||
targetPoint = targetWheel.position + (playerTransform.forward * 0.5f) + (pushDirection * 0.5f);
|
||||
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
|
||||
{
|
||||
// Ramming Check
|
||||
float leadTime = Mathf.Clamp(distance / Mathf.Max(currentSpeed, minRamSpeed), 0f, 1.0f);
|
||||
// General Ramming / Catching up
|
||||
float leadTime = Mathf.Clamp(distance / Mathf.Max(currentSpeed, minRamSpeed), 0f, 1.5f);
|
||||
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);
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -258,8 +400,11 @@ public class AIController : MonoBehaviour
|
||||
}
|
||||
else
|
||||
{
|
||||
// Distant pursuit: Use NavMesh to navigate city streets without hitting buildings
|
||||
Vector3 waypoint = GetNavMeshWaypoint(aiPos, playerPos);
|
||||
// --- 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
|
||||
@@ -348,18 +493,32 @@ public class AIController : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
// --- 1. ANTI-CRASH AVOIDANCE (Avoid other cops!) ---
|
||||
// --- 1. ANTI-CRASH AVOIDANCE & LANE OFFSET ---
|
||||
Vector3 avoidanceOffset = Vector3.zero;
|
||||
bool needsToBrakeForAlly = false;
|
||||
float avoidDist = aggressive ? 12f : 20f;
|
||||
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 ? 1.5f : -1.5f);
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,8 +527,12 @@ public class AIController : MonoBehaviour
|
||||
{
|
||||
if (hitL.collider.GetComponentInParent<AIController>() != null)
|
||||
{
|
||||
avoidanceOffset += aiTransform.right * 1.5f;
|
||||
if (hitL.distance < 8f) needsToBrakeForAlly = true;
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,8 +541,12 @@ public class AIController : MonoBehaviour
|
||||
{
|
||||
if (hitR.collider.GetComponentInParent<AIController>() != null)
|
||||
{
|
||||
avoidanceOffset -= aiTransform.right * 1.5f;
|
||||
if (hitR.distance < 8f) needsToBrakeForAlly = true;
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,13 +597,16 @@ public class AIController : MonoBehaviour
|
||||
|
||||
aiVehicle.ProvideInputs(steerInput, throttleInput, brakeInput);
|
||||
|
||||
// --- 4. FORCED ROTATION ---
|
||||
// --- 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;
|
||||
float newY = Mathf.LerpAngle(currentEuler.y, targetRotation.eulerAngles.y, forcedTurnSpeed * Time.fixedDeltaTime);
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user