//----------------------------------------------
// Realistic Car Controller
//
// Copyright © 2014 - 2023 BoneCracker Games
// https://www.bonecrackergames.com
// Buğra Özdoğanlar
//
//----------------------------------------------
using UnityEngine;
using UnityEngine.AI;
using System;
using System.Collections;
using System.Collections.Generic;
///
/// AI Controller of RCC. It's not professional, but it does the job. Follows all waypoints, or follows/chases the target gameobject.
///
[RequireComponent(typeof(RCC_CarControllerV3))]
[AddComponentMenu("BoneCracker Games/Realistic Car Controller/AI/RCC AI Car Controller")]
public class RCC_AICarController : MonoBehaviour {
// Car controller.
public RCC_CarControllerV3 CarController {
get {
if (_carController == null)
_carController = GetComponentInParent();
return _carController;
}
}
private RCC_CarControllerV3 _carController;
public RCC_AIWaypointsContainer waypointsContainer; // Waypoints Container.
public int currentWaypointIndex = 0; // Current index in Waypoint Container.
public string targetTag = "Player"; // Search and chase Gameobjects with tags.
// AI Type
public NavigationMode navigationMode = NavigationMode.FollowWaypoints;
public enum NavigationMode { FollowWaypoints, ChaseTarget, FollowTarget }
// Raycast distances used for detecting obstacles at front of the AI vehicle.
[Range(5f, 30f)] public float raycastLength = 3f;
[Range(10f, 90f)] public float raycastAngle = 30f;
public LayerMask obstacleLayers = -1;
public GameObject obstacle;
public bool useRaycasts = true; // Using forward and sideways raycasts to avoid obstacles.
private float rayInput = 0f; // Total ray input affected by raycast distances.
private bool raycasting = false; // Raycasts hits an obstacle now?
private float resetTime = 0f; // This timer was used for deciding go back or not, after crashing.
private bool reversingNow = false; // Reversing now?
// Steer, Motor, And Brake inputs. Will feed RCC_CarController with these inputs.
public float steerInput = 0f;
public float throttleInput = 0f;
public float brakeInput = 0f;
public float handbrakeInput = 0f;
// Limit speed.
public bool limitSpeed = false;
public float maximumSpeed = 100f;
// Smoothed steering.
public bool smoothedSteer = true;
// Counts laps and how many waypoints were passed.
public int lap = 0;
public bool stopAfterLap = false;
public int stopLap = 10;
public int totalWaypointPassed = 0;
public bool ignoreWaypointNow = false;
// Detector radius.
public int detectorRadius = 200;
public int startFollowDistance = 300;
public int stopFollowDistance = 30;
private bool updateTargets = false;
private float lastUpdatedTargets = 0f;
// Unity's Navigator.
private NavMeshAgent navigator;
// Detector with Sphere Collider. Used for finding target Gameobjects in chasing mode.
public List targetsInZone = new List();
public List brakeZones = new List();
public Transform targetChase; // Target Gameobject for chasing.
public RCC_AIBrakeZone targetBrake; // Target brakezone.
// Firing an event when each RCC AI vehicle spawned / enabled.
public delegate void onRCCAISpawned(RCC_AICarController RCCAI);
public static event onRCCAISpawned OnRCCAISpawned;
// Firing an event when each RCC AI vehicle disabled / destroyed.
public delegate void onRCCAIDestroyed(RCC_AICarController RCCAI);
public static event onRCCAIDestroyed OnRCCAIDestroyed;
private void Awake() {
// If Waypoints Container is not selected in Inspector Panel, find it on scene.
if (!waypointsContainer)
waypointsContainer = FindObjectOfType(typeof(RCC_AIWaypointsContainer)) as RCC_AIWaypointsContainer;
// Creating our Navigator and setting properties.
GameObject navigatorObject = new GameObject("Navigator");
navigatorObject.transform.SetParent(transform, false);
navigator = navigatorObject.AddComponent();
navigator.radius = 1;
navigator.speed = 1;
navigator.angularSpeed = 100000f;
navigator.acceleration = 100000f;
navigator.height = 1;
navigator.avoidancePriority = 0;
}
private void OnEnable() {
// Setting external controller on enable.
CarController.externalController = true;
// Calling this event when AI vehicle spawned.
if (OnRCCAISpawned != null)
OnRCCAISpawned(this);
}
private void Update() {
// If not controllable, no need to go further.
if (!CarController.canControl)
return;
// If limit speed is not enabled, maximum speed is same with vehicle's maximum speed.
if (!limitSpeed)
maximumSpeed = CarController.maxspeed;
// Assigning navigator's position to front wheels of the vehicle
navigator.transform.localPosition = Vector3.zero;
navigator.transform.localPosition += Vector3.forward * CarController.FrontLeftWheelCollider.transform.localPosition.z;
CheckTargets(); // Checking targets if navigation mode is set to chase or follow target mode.
CheckBrakeZones(); // Checking existing brake zones in the scene.
if (!updateTargets)
lastUpdatedTargets += Time.deltaTime;
if (lastUpdatedTargets >= 1f)
updateTargets = true;
}
private void FixedUpdate() {
// If not controllable, no need to go further.
if (!CarController.canControl)
return;
// If enabled, raycasts will be used to avoid obstacles at runtime.
if (useRaycasts)
FixedRaycasts(); // Recalculates steerInput if one of raycasts detects an object front of AI vehicle.
Navigation(); // Calculates steerInput based on navigator.
CheckReset(); // Used for deciding go back or not after crashing.
FeedRCC(); // Feeds inputs of the RCC.
}
private void Navigation() {
// Navigator Input is multiplied by 1.5f for fast reactions.
float navigatorInput = Mathf.Clamp(transform.InverseTransformDirection(navigator.desiredVelocity).x * 1f, -1f, 1f);
if (navigatorInput > .4f)
navigatorInput = 1f;
if (navigatorInput < -.4f)
navigatorInput = -1f;
// Navigation has three modes.
switch (navigationMode) {
case NavigationMode.FollowWaypoints:
// If our scene doesn't have a Waypoint Container, stop and return with error.
if (!waypointsContainer) {
Debug.LogError("Waypoints Container Couldn't Found!");
Stop();
return;
}
// If our scene has Waypoints Container and it doesn't have any waypoints, stop and return with error.
if (waypointsContainer && waypointsContainer.waypoints.Count < 1) {
Debug.LogError("Waypoints Container Doesn't Have Any Waypoints!");
Stop();
return;
}
// If stop after lap is enabled, stop at target lap.
if (stopAfterLap && lap >= stopLap) {
Stop();
return;
}
// Next waypoint and its position.
RCC_Waypoint currentWaypoint = waypointsContainer.waypoints[currentWaypointIndex];
// Checks for the distance to next waypoint. If it is less than written value, then pass to next waypoint.
float distanceToNextWaypoint = Vector3.Distance(transform.position, currentWaypoint.transform.position);
// Setting destination of the Navigator.
if (navigator.isOnNavMesh)
navigator.SetDestination(waypointsContainer.waypoints[currentWaypointIndex].transform.position);
// If distance to the next waypoint is not 0, and close enough to the vehicle, increase index of the current waypoint and total waypoint.
if (distanceToNextWaypoint != 0 && distanceToNextWaypoint < waypointsContainer.waypoints[currentWaypointIndex].radius) {
currentWaypointIndex++;
totalWaypointPassed++;
// If all waypoints were passed, sets the current waypoint to first waypoint and increase lap.
if (currentWaypointIndex >= waypointsContainer.waypoints.Count) {
currentWaypointIndex = 0;
lap++;
}
// Setting destination of the Navigator.
if (navigator.isOnNavMesh)
navigator.SetDestination(waypointsContainer.waypoints[currentWaypointIndex].transform.position);
}
// If vehicle goes forward, calculate throttle and brake inputs.
if (!reversingNow) {
throttleInput = (distanceToNextWaypoint < (waypointsContainer.waypoints[currentWaypointIndex].radius * (CarController.speed / 30f))) ? (Mathf.Clamp01(currentWaypoint.targetSpeed - CarController.speed)) : 1f;
throttleInput *= Mathf.Clamp01(Mathf.Lerp(10f, 0f, (CarController.speed) / maximumSpeed));
brakeInput = (distanceToNextWaypoint < (waypointsContainer.waypoints[currentWaypointIndex].radius * (CarController.speed / 30f))) ? (Mathf.Clamp01(CarController.speed - currentWaypoint.targetSpeed)) : 0f;
handbrakeInput = 0f;
// If vehicle speed is high enough, calculate them related to navigator input. This will reduce throttle input, and increase brake input on sharp turns.
if (CarController.speed > 30f) {
throttleInput -= Mathf.Abs(navigatorInput) / 3f;
brakeInput += Mathf.Abs(navigatorInput) / 3f;
}
}
break;
case NavigationMode.ChaseTarget:
// If our scene doesn't have a target to chase, stop and return.
if (!targetChase) {
Stop();
return;
}
// Setting destination of the Navigator.
if (navigator.isOnNavMesh)
navigator.SetDestination(targetChase.position);
// If vehicle goes forward, calculate throttle and brake inputs.
if (!reversingNow) {
throttleInput = 1f;
throttleInput *= Mathf.Clamp01(Mathf.Lerp(10f, 0f, (CarController.speed) / maximumSpeed));
brakeInput = 0f;
handbrakeInput = 0f;
// If vehicle speed is high enough, calculate them related to navigator input. This will reduce throttle input, and increase brake input on sharp turns.
if (CarController.speed > 30f) {
throttleInput -= Mathf.Abs(navigatorInput) / 3f;
brakeInput += Mathf.Abs(navigatorInput) / 3f;
}
}
break;
case NavigationMode.FollowTarget:
// If our scene doesn't have a Waypoints Container, return with error.
if (!targetChase) {
Stop();
return;
}
// Setting destination of the Navigator.
if (navigator.isOnNavMesh)
navigator.SetDestination(targetChase.position);
// Checks for the distance to target.
float distanceToTarget = Vector3.Distance(transform.position, targetChase.position);
// If vehicle goes forward, calculate throttle and brake inputs.
if (!reversingNow) {
throttleInput = distanceToTarget < (stopFollowDistance * Mathf.Lerp(1f, 5f, CarController.speed / 50f)) ? Mathf.Lerp(-5f, 1f, distanceToTarget / (stopFollowDistance / 1f)) : 1f;
throttleInput *= Mathf.Clamp01(Mathf.Lerp(10f, 0f, (CarController.speed) / maximumSpeed));
brakeInput = distanceToTarget < (stopFollowDistance * Mathf.Lerp(1f, 5f, CarController.speed / 50f)) ? Mathf.Lerp(5f, 0f, distanceToTarget / (stopFollowDistance / 1f)) : 0f;
handbrakeInput = 0f;
// If vehicle speed is high enough, calculate them related to navigator input. This will reduce throttle input, and increase brake input on sharp turns.
if (CarController.speed > 30f) {
throttleInput -= Mathf.Abs(navigatorInput) / 3f;
brakeInput += Mathf.Abs(navigatorInput) / 3f;
}
if (throttleInput < .05f)
throttleInput = 0f;
if (brakeInput < .05f)
brakeInput = 0f;
}
break;
}
// If vehicle is in brake zone, apply brake input.
if (targetBrake) {
// If vehicle is in brake zone and speed of the vehicle is higher than the target speed, apply brake input.
if (Vector3.Distance(transform.position, targetBrake.transform.position) < targetBrake.distance && CarController.speed > targetBrake.targetSpeed) {
throttleInput = 0f;
brakeInput = 1f;
}
}
if (brakeInput > .25f)
throttleInput = 0f;
// Steer input.
steerInput = (ignoreWaypointNow ? rayInput : navigatorInput + rayInput);
steerInput = Mathf.Clamp(steerInput, -1f, 1f) * CarController.direction;
// Clamping inputs.
throttleInput = Mathf.Clamp01(throttleInput);
brakeInput = Mathf.Clamp01(brakeInput);
handbrakeInput = Mathf.Clamp01(handbrakeInput);
// If vehicle goes backwards, set brake input to 1 for reversing.
if (reversingNow) {
throttleInput = 0f;
brakeInput = 1f;
handbrakeInput = 0f;
} else {
if (CarController.speed < 5f && brakeInput >= .5f) {
brakeInput = 0f;
handbrakeInput = 1f;
}
}
}
///
/// Vehicle will try to go backwards if crashed or stucked.
///
private void CheckReset() {
// If navigation mode is set to follow, this means vehicle may stop. If vehicle is stopped near the target, no need to go backwards.
if (targetChase && navigationMode == NavigationMode.FollowTarget && Vector3.Distance(transform.position, targetChase.position) < stopFollowDistance) {
reversingNow = false;
resetTime = 0;
return;
}
// If unable to move forward, puts the gear to R.
if (CarController.speed <= 5 && transform.InverseTransformDirection(CarController.Rigid.velocity).z <= 1f)
resetTime += Time.deltaTime;
// If car is stucked for 2 seconds, reverse now.
if (resetTime >= 2)
reversingNow = true;
// If car is stucked for 4 seconds, or speed exceeds 25, go forward.
if (resetTime >= 4 || CarController.speed >= 25) {
reversingNow = false;
resetTime = 0;
}
}
///
/// Using raycasts to avoid obstacles.
///
private void FixedRaycasts() {
// Creating five raycasts with angles.
int[] anglesOfRaycasts = new int[5];
anglesOfRaycasts[0] = 0;
anglesOfRaycasts[1] = Mathf.FloorToInt(raycastAngle / 3f);
anglesOfRaycasts[2] = Mathf.FloorToInt(raycastAngle / 1f);
anglesOfRaycasts[3] = -Mathf.FloorToInt(raycastAngle / 1f);
anglesOfRaycasts[4] = -Mathf.FloorToInt(raycastAngle / 3f);
// Ray pivot position.
Vector3 pivotPos = transform.position;
pivotPos += transform.forward * CarController.FrontLeftWheelCollider.transform.localPosition.z;
// Ray hit.
RaycastHit hit;
rayInput = 0f;
bool casted = false;
// Casting rays.
for (int i = 0; i < anglesOfRaycasts.Length; i++) {
// Drawing normal gizmos.
Debug.DrawRay(pivotPos, Quaternion.AngleAxis(anglesOfRaycasts[i], transform.up) * transform.forward * raycastLength, Color.white);
// Casting the ray. If ray hits another obstacle...
if (Physics.Raycast(pivotPos, Quaternion.AngleAxis(anglesOfRaycasts[i], transform.up) * transform.forward, out hit, raycastLength, obstacleLayers) && !hit.collider.isTrigger && hit.transform.root != transform) {
switch (navigationMode) {
case NavigationMode.FollowWaypoints:
// Drawing hit gizmos.
Debug.DrawRay(pivotPos, Quaternion.AngleAxis(anglesOfRaycasts[i], transform.up) * transform.forward * raycastLength, Color.red);
casted = true;
// Setting ray input related to distance to the obstacle.
if (i != 0)
rayInput -= Mathf.Lerp(Mathf.Sign(anglesOfRaycasts[i]), 0f, (hit.distance / raycastLength));
break;
case NavigationMode.ChaseTarget:
if (targetChase && hit.transform != targetChase && !hit.transform.IsChildOf(targetChase)) {
// Drawing hit gizmos.
Debug.DrawRay(pivotPos, Quaternion.AngleAxis(anglesOfRaycasts[i], transform.up) * transform.forward * raycastLength, Color.red);
casted = true;
// Setting ray input related to distance to the obstacle.
if (i != 0)
rayInput -= Mathf.Lerp(Mathf.Sign(anglesOfRaycasts[i]), 0f, (hit.distance / raycastLength));
}
break;
case NavigationMode.FollowTarget:
// Drawing hit gizmos.
Debug.DrawRay(pivotPos, Quaternion.AngleAxis(anglesOfRaycasts[i], transform.up) * transform.forward * raycastLength, Color.red);
casted = true;
// Setting ray input related to distance to the obstacle.
if (i != 0)
rayInput -= Mathf.Lerp(Mathf.Sign(anglesOfRaycasts[i]), 0f, (hit.distance / raycastLength));
break;
}
// If ray hits an obstacle, set obstacle. Otherwise set it to null.
if (casted)
obstacle = hit.transform.gameObject;
else
obstacle = null;
}
}
// Ray hits an obstacle or not?
raycasting = casted;
// If so, clamp the ray input.
rayInput = Mathf.Clamp(rayInput, -1f, 1f);
// If ray input is high enough, ignore the navigator input and directly use the ray input for steering.
if (raycasting && Mathf.Abs(rayInput) > .5f)
ignoreWaypointNow = true;
else
ignoreWaypointNow = false;
}
///
/// Feeding the RCC with throttle, brake, steer, and handbrake inputs.
///
private void FeedRCC() {
// Feeding throttleInput of the RCC.
if (!CarController.changingGear && !CarController.cutGas)
CarController.throttleInput = (CarController.direction == 1 ? Mathf.Clamp01(throttleInput) : Mathf.Clamp01(brakeInput));
else
CarController.throttleInput = 0f;
if (!CarController.changingGear && !CarController.cutGas)
CarController.brakeInput = (CarController.direction == 1 ? Mathf.Clamp01(brakeInput) : Mathf.Clamp01(throttleInput));
else
CarController.brakeInput = 0f;
// Feeding steerInput of the RCC.
if (smoothedSteer)
CarController.steerInput = Mathf.Lerp(CarController.steerInput, steerInput, Time.deltaTime * 20f);
else
CarController.steerInput = steerInput;
CarController.handbrakeInput = handbrakeInput;
}
///
/// Stops the vehicle immediately.
///
private void Stop() {
throttleInput = 0f;
brakeInput = 0f;
steerInput = 0f;
handbrakeInput = 1f;
}
///
/// Checks the near targets if navigation mode is set to follow or chase mode.
///
private void CheckTargets() {
if (!updateTargets)
return;
updateTargets = false;
lastUpdatedTargets = 0f;
Collider[] colliders = Physics.OverlapSphere(transform.position, detectorRadius);
for (int i = 0; i < colliders.Length; i++) {
// If a target in the zone, add it to the list.
if (colliders[i].transform.root.CompareTag(targetTag)) {
if (!targetsInZone.Contains(colliders[i].transform.root))
targetsInZone.Add(colliders[i].transform.root);
}
// If a brake zone in the zone, add it to the list.
if (colliders[i].GetComponent()) {
if (!brakeZones.Contains(colliders[i].GetComponent()))
brakeZones.Add(colliders[i].GetComponent());
}
}
// Removing unnecessary targets in list first. If target is null or not active, remove it from the list.
for (int i = 0; i < targetsInZone.Count; i++) {
if (targetsInZone[i] == null)
targetsInZone.RemoveAt(i);
if (!targetsInZone[i].gameObject.activeInHierarchy)
targetsInZone.RemoveAt(i);
else {
// If distance to the target is far away, remove it from the list.
if (Vector3.Distance(transform.position, targetsInZone[i].transform.position) > (detectorRadius * 1.1f))
targetsInZone.RemoveAt(i);
}
}
// If there is a target in the zone, get closest enemy.
if (targetsInZone.Count > 0)
targetChase = GetClosestEnemy(targetsInZone.ToArray());
else
targetChase = null;
}
///
/// Checks the brake zones.
///
private void CheckBrakeZones() {
// Removing unnecessary brake zones in list. If brake zone is null or not active, remove it from the list.
for (int i = 0; i < brakeZones.Count; i++) {
if (brakeZones[i] == null)
brakeZones.RemoveAt(i);
if (!brakeZones[i].gameObject.activeInHierarchy)
brakeZones.RemoveAt(i);
else {
// If distance to the brake zone is far away, remove it from the list.
if (Vector3.Distance(transform.position, brakeZones[i].transform.position) > (detectorRadius * 1.1f))
brakeZones.RemoveAt(i);
}
}
// If there is a brake zone, get closest one.
if (brakeZones.Count > 0)
targetBrake = GetClosestBrakeZone(brakeZones.ToArray());
else
targetBrake = null;
}
///
/// Gets the closest enemy.
///
///
///
private Transform GetClosestEnemy(Transform[] enemies) {
Transform bestTarget = null;
float closestDistanceSqr = Mathf.Infinity;
Vector3 currentPosition = transform.position;
foreach (Transform potentialTarget in enemies) {
Vector3 directionToTarget = potentialTarget.position - currentPosition;
float dSqrToTarget = directionToTarget.sqrMagnitude;
if (dSqrToTarget < closestDistanceSqr) {
closestDistanceSqr = dSqrToTarget;
bestTarget = potentialTarget;
}
}
return bestTarget;
}
///
/// Gets the closest brake zone.
///
///
///
private RCC_AIBrakeZone GetClosestBrakeZone(RCC_AIBrakeZone[] enemies) {
RCC_AIBrakeZone bestTarget = null;
float closestDistanceSqr = Mathf.Infinity;
Vector3 currentPosition = transform.position;
foreach (RCC_AIBrakeZone potentialTarget in enemies) {
Vector3 directionToTarget = potentialTarget.transform.position - currentPosition;
float dSqrToTarget = directionToTarget.sqrMagnitude;
if (dSqrToTarget < closestDistanceSqr) {
closestDistanceSqr = dSqrToTarget;
bestTarget = potentialTarget;
}
}
return bestTarget;
}
private void OnDisable() {
// Disabling external controller of the vehicle on disable.
CarController.externalController = false;
// Calling this event when AI vehicle is destroyed.
if (OnRCCAIDestroyed != null)
OnRCCAIDestroyed(this);
}
}