Enemy AI keeps chasing player even after the player has left its line of sight.
Hello and thank you for taking the time to read this question.
I have a prototype of a basic AI which uses finite state machine to switch from patrolling around way-points to chasing the player when in the enemy AI's view. My problem is that the enemy AI keeps chasing the player after the player has left its sight. This shouldn't happen because the enemy AI should be switching back to patrolling when the player has left its line of sight.
Please could someone look at the scripts and let me know what it is that I have done wrong that is breaking the enemy AI's behaviour. Below are the scripts that I am talking about which are used in my prototype.
The LineSight.cs script is responsible for informing the enemy AI if the player is withing its view. This script is attached to the enemy object, but a empty child object called Eye point is is linked to the LineSight.cs script and the object is placed in front of the enemy AI's eyes.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LineSight : MonoBehaviour
{
//How sensitive shoudl we be to sight
public enum SightSensitivity { STRICT, LOOSE }
//Sight sensitivity
public SightSensitivity Sensitity = SightSensitivity.STRICT;
//can we see target
public bool CanSeeTarget = false;
//FOV
public float FieldOfView = 90f;
//reference to target
private Transform Target = null;
//reference to eyes
public Transform EyePoint = null;
//reference to transform component
private Transform ThisTransform = null;
//reference to sphere collider
private SphereCollider ThisCollider = null;
//reference to last known object sighting, if any
public Vector3 LastKnowSighting = Vector3.zero;
private void Awake()
{
ThisTransform = GetComponent<Transform>();
ThisCollider = GetComponent<SphereCollider>();
LastKnowSighting = ThisTransform.position;
Target = GameObject.FindGameObjectWithTag("Player").GetComponent<Transform>();
}
bool InFOV()
{
//get direction to target
Vector3 DirToTarget = Target.position - EyePoint.position;
//get angle between forward and look direction
float Angle = Vector3.Angle(EyePoint.forward, DirToTarget);
//are we within feild of view?
if (Angle <= FieldOfView)
{
//Debug.Log("Within View");
return true;
}
//not within view
return false;
}
bool ClearLineOfSight()
{
RaycastHit Info;
//ThisCollider.radius
if (Physics.Raycast(EyePoint.position, (Target.position - EyePoint.position).normalized, out Info, 4))
{
//if player, then can see player
if (Info.transform.CompareTag("Player"))
{
//Debug.DrawRay(EyePoint.position, (Target.position - EyePoint.position).normalized, Color.yellow, 100000);
Debug.Log("Raycast hit player");
return true;
}
//Debug.Log("Raycast is hitting something");
}
return false;
}
void UpdateSight()
{
switch (Sensitity)
{
case SightSensitivity.STRICT:
CanSeeTarget = InFOV() && ClearLineOfSight();
break;
case SightSensitivity.LOOSE:
CanSeeTarget = InFOV() || ClearLineOfSight();
break;
}
}
void OnTriggerStay(Collider Other)
{
UpdateSight();
//update last known sighting
if (CanSeeTarget)
{
LastKnowSighting = Target.position;
//Debug.Log("I saw you at: "+LastKnowSighting);
}
}
void OnTriggerExit(Collider Other)
{
if (!Other.CompareTag("Player"))
{
return;
}
CanSeeTarget = false;
}
}
The EnemyAI.cs script holds the grunt of the AI code to make the AI move around and switch states. It references the LineSight.cs script to check if the player is within its view, and of that is true, then the EnemyAI.cs script would switch the enemy from patrolling to chasing, and chasing after the player in sight.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class AI_Enemy : MonoBehaviour
{
public enum ENEMY_STATE { PATROL, CHASE, ATTACK };
public ENEMY_STATE CurrentState
{
get { return currentState; }
set
{
// update current state
currentState = value;
//stop all running coroutines
StopAllCoroutines();
switch (CurrentState)
{
case ENEMY_STATE.PATROL:
StartCoroutine(AIPatrol());
break;
case ENEMY_STATE.CHASE:
StartCoroutine(AIChase());
break;
case ENEMY_STATE.ATTACK:
StartCoroutine(AIAttack());
break;
}
}
}
[SerializeField]
private ENEMY_STATE currentState = ENEMY_STATE.PATROL;
//reference to line of sight
private LineSight ThisLineSight = null;
//reference to nav mesh agent
private NavMeshAgent ThisAgent = null;
//reference to player health
//private Health PlayerHealth = null;
//reference to player transform
private Transform PlayerTransform = null;
//reference to patrol destination
//public Transform PatrolDestination;
//damage per second
//public float MaxDamage = 10f;
// For navigation waypoint system
private List<Transform> points = new List<Transform>();
private int destPoints = 0;
public WaypointSystem path;
private void Awake()
{
ThisLineSight = GetComponent<LineSight>();
ThisAgent = GetComponent<NavMeshAgent>();
ThisAgent.autoBraking = false;
//PlayerHealth = GameObject.FindGameObjectWithTag("Player").GetComponent<Health>();
PlayerTransform = GameObject.FindGameObjectWithTag("Player").GetComponent<Transform>();
//PlayerTransform = PlayerHealth.GetComponent<Transform>();
}
void Start()
{
//Get random destination
//GameObject[] Destinations = GameObject.FindGameObjectsWithTag("Dest");
//PatrolDestination = Destinations[Random.Range(0, Destinations.Length)].GetComponent<Transform>();
//configure starting state
CurrentState = ENEMY_STATE.PATROL;
//initalize waypoint system
points = path.waypoints;
}
void GoToNextPoint()
{
//Returns if no points have been set
if (points.Count == 0)
{
return;
}
//Set the agent to go to the currently selected destiniation
ThisAgent.destination = points[destPoints].position;
//Choose the next point in the array as the destination, cycling to the start if necessary
destPoints = (destPoints + 1) % points.Count;
}
public IEnumerator AIPatrol()
{
//loop while patrolling
while (currentState == ENEMY_STATE.PATROL)
{
//set trict search
ThisLineSight.Sensitity = LineSight.SightSensitivity.STRICT;
//chase to patrol position
ThisAgent.isStopped = false;
//ThisAgent.SetDestination(PatrolDestination.position);
if (!ThisAgent.pathPending && ThisAgent.remainingDistance < 0.5f)
{
GoToNextPoint();
}
//wait until path is computed
while (ThisAgent.pathPending)
{
yield return null;
}
//If we can see the target then start chasing
if (ThisLineSight.CanSeeTarget)
{
ThisAgent.isStopped = true;
CurrentState = ENEMY_STATE.CHASE;
//Debug.Log("I see you....");
yield break;
}
Debug.Log("I'm patrolling....");
//wait until next frame
yield return null;
}
yield break;
}
public IEnumerator AIChase()
{
//loop while chasing
while (currentState == ENEMY_STATE.CHASE)
{
//set loose search
ThisLineSight.Sensitity = LineSight.SightSensitivity.LOOSE;
//chase to last known position
ThisAgent.isStopped = false;
ThisAgent.SetDestination(ThisLineSight.LastKnowSighting);
//wait until path is computed
while (ThisAgent.pathPending)
{
yield return null;
}
//have we reached destination
if (ThisAgent.remainingDistance <= ThisAgent.stoppingDistance)
{
//stop agent
ThisAgent.isStopped = true;
//reached destination but cannot see player
if (!ThisLineSight.CanSeeTarget)
{
CurrentState = ENEMY_STATE.PATROL;
Debug.Log("hm, wonder where he went?");
}
//reached destination and can see player. reached attacking distance
else
{
CurrentState = ENEMY_STATE.ATTACK;
//Debug.Log("I'm going to attack you....");
}
yield break;
}
Debug.Log("I'm chasing you....");
//wait until next frame
yield return null;
}
yield break;
}
public IEnumerator AIAttack()
{
//loop while chasing and attacking
while (currentState == ENEMY_STATE.ATTACK)
{
//chase player to position
ThisAgent.isStopped = false;
ThisAgent.SetDestination(PlayerTransform.position);
//wait until path is computed
while (ThisAgent.pathPending)
{
yield return null;
}
//has player ran away?
if (ThisAgent.remainingDistance > ThisAgent.stoppingDistance)
{
//change b ack to chase state
currentState = ENEMY_STATE.CHASE;
yield break;
}
else
{
//attack player
//PlayerHealth.HealthPoints =- MaxDamage * Time.deltaTime;
Debug.Log("I'm attacking....");
}
//wait until next frame
yield return null;
}
yield break;
}
}
I'm providing the WaypointSystem.cs script to provide context and information about how the waypoint system works. This script is really for constructing and holding waypoints (or path objects in the script).
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[ExecuteInEditMode]
public class WaypointSystem : MonoBehaviour {
//Change color of waypoints
public Color PathObjectColor = Color.green;
public Color PathLinesColor = Color.red;
//creates list of path objects
public List<Transform> waypoints = new List<Transform>();
//holds number of path objects
int PathObjectIndex = 0;
//allows path following to be disabled during play mode, useful for telling AI to stop following
public bool disableInGame;
// Update is called once per frame
void Update () {
if (!disableInGame)
{
//auto adds path objects as they are made int he editor
Transform[] PathObjects = GetComponentsInChildren<Transform>();
if (waypoints.Count > 0)
{
waypoints.Clear();
PathObjectIndex = 0; //defaults to 0 while no path objects exist
foreach (Transform t in PathObjects)
{
if (t != transform)
{
t.name = "PathObj_" + PathObjectIndex.ToString();
waypoints.Add(t);
PathObjectIndex++;
}
}
}
}
//
}
private void OnDrawGizmos()
{
if (waypoints.Count > 0)
{
//Draws the path objects
Gizmos.color = PathObjectColor;
foreach (Transform t in waypoints)
{
Gizmos.DrawSphere(t.position, 1f);
}
//Draws the connected lines between the path objects
Gizmos.color = PathLinesColor;
for (int a = 0; a < waypoints.Count - 1; a++)
{
Gizmos.DrawLine(waypoints [a].position, waypoints[a + 1].position);
}
}
}
}
Additional info: You may have notice some commented lines of code dotted around in the scripts, that's because this whole prototype is from two different tutorials on making basic AIs, so I've tried to merge the two together to make use of them. This is because one tutorial taught how to make the finite state machine and the line of site scripts, while the other taught how to make a waypoint system based on using lists and the position values of each path object dotted around the map .
Also you may have also noticed some debug.log messages dotted around, that was my attempt to troubleshoot which areas weren't being accessed or used properly. From what I can gather, the Enemy AI simply does not lose track of the player one it is within its view. At then start of the run time, when the enemy AI does not see the player initially, the enemy AI patrols around the map as normal, but the moment the player is within its sight, its pretty much attached to the player like glue. Its like the raycast that's being used to detect the player follows where ever the player is, and even ignores if the player is behind the enemy AI or on the other side of a wall.
This issue confuses me because in a different project file that I used for learning the finite state machine, the AI worked differently. The EnemyAI.cs script was more or less the same in the previous project file, just uses a slightly different navigating system to waypoints. Here is the code of the EnmeyAI.cs script from the previous project:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
public class AI_Enemy : MonoBehaviour {
public enum ENEMY_STATE { PATROL, CHASE, ATTACK };
public ENEMY_STATE CurrentState
{
get{ return currentState; }
set
{
// update current state
currentState = value;
//stop all running coroutines
StopAllCoroutines();
switch (CurrentState)
{
case ENEMY_STATE.PATROL: StartCoroutine(AIPatrol());
break;
case ENEMY_STATE.CHASE: StartCoroutine(AIChase());
break;
case ENEMY_STATE.ATTACK: StartCoroutine(AIAttack());
break;
}
}
}
[SerializeField]
private ENEMY_STATE currentState = ENEMY_STATE.PATROL;
//reference to line of sight
private LineSight ThisLineSight = null;
//reference to nav mesh agent
private NavMeshAgent ThisAgent = null;
//reference to player health
private Health PlayerHealth = null;
//reference to player transform
private Transform PlayerTransform = null;
//reference to patrol destination
public Transform PatrolDestination;
//damage per second
public float MaxDamage = 10f;
private void Awake()
{
ThisLineSight = GetComponent<LineSight>();
ThisAgent = GetComponent<NavMeshAgent>();
PlayerHealth = GameObject.FindGameObjectWithTag("Player").GetComponent<Health>();
PlayerTransform = GameObject.FindGameObjectWithTag("Player").GetComponent<Transform>();
PlayerTransform = PlayerHealth.GetComponent<Transform>();
}
void Start()
{
//Get random destination
GameObject[] Destinations = GameObject.FindGameObjectsWithTag("Dest");
PatrolDestination = Destinations[Random.Range(0, Destinations.Length)].GetComponent<Transform>();
//configure starting state
CurrentState = ENEMY_STATE.PATROL;
}
public IEnumerator AIPatrol()
{
//loop while patrolling
while (currentState == ENEMY_STATE.PATROL)
{
//set trict search
ThisLineSight.Sensitity = LineSight.SightSensitivity.STRICT;
//chase to patrol position
ThisAgent.isStopped = false;
ThisAgent.SetDestination(PatrolDestination.position);
//wait until path is computed
while (ThisAgent.pathPending)
{
yield return null;
}
//If we can see the target then start chasing
if (ThisLineSight.CanSeeTarget)
{
ThisAgent.isStopped = true;
CurrentState = ENEMY_STATE.CHASE;
//Debug.Log("I see you....");
yield break;
}
//Debug.Log("I'm patrolling....");
//wait until next frame
yield return null;
}
yield break;
}
public IEnumerator AIChase()
{
//loop while chasing
while (currentState == ENEMY_STATE.CHASE)
{
//set loose search
ThisLineSight.Sensitity = LineSight.SightSensitivity.LOOSE;
//chase to last known position
ThisAgent.isStopped = false;
ThisAgent.SetDestination(ThisLineSight.LastKnowSighting);
//wait until path is computed
while (ThisAgent.pathPending)
{
yield return null;
}
//have we reached destination
if (ThisAgent.remainingDistance <= ThisAgent.stoppingDistance)
{
//stop agent
ThisAgent.isStopped = true;
//reached destination but cannot see player
if (!ThisLineSight.CanSeeTarget)
{
CurrentState = ENEMY_STATE.PATROL;
//Debug.Log("hm, wonder where he went?");
}
//reached destination and can see player. reached attacking distance
else
{
CurrentState = ENEMY_STATE.ATTACK;
//Debug.Log("I'm going to attack you....");
}
yield break;
}
//Debug.Log("I'm chasing you....");
//wait until next frame
yield return null;
}
yield break;
}
public IEnumerator AIAttack()
{
//loop while chasing and attacking
while (currentState == ENEMY_STATE.ATTACK)
{
//chase player to position
ThisAgent.isStopped = false;
ThisAgent.SetDestination(PlayerTransform.position);
//wait until path is computed
while (ThisAgent.pathPending)
{
yield return null;
}
//has player ran away?
if (ThisAgent.remainingDistance > ThisAgent.stoppingDistance)
{
//change b ack to chase state
currentState = ENEMY_STATE.CHASE;
yield break;
}
else
{
//attack player
PlayerHealth.HealthPoints =- MaxDamage * Time.deltaTime;
//Debug.Log("I'm attacking....");
}
//wait until next frame
yield return null;
}
yield break;
}
}
Its probably worth mentioning that in the tutorial for finite state machines, the raycast in LineSight.cs used the sphere collider to determine the range of the sight, this did not work in my prototype, so I went for number values which kinda of worked. Below is an extract from the tutorial on what I'm on about:
if( Physics.Raycast( EyePoint.position, (Target.position - EyePoint.position). normalized, out Info, ThisCollider.radius))
ThisCollider.radius references the GetComponent function, in Awake().
A sphere collider is being used on the enemy AI object and is scaled just around the enemy AI, which kinda of works for the time being, so once the player is inside the sphere collider, the enemy AI switches to the chase state,.
So after digesting all that information, what I want to know is a solution to fix the Enemy AI's line of sight, so instead, when the player is out of its line of sight, such as beyond the sphere collider or behind a wall, the enemy AI ceases to chase the player and restores back to patrolling again.
If there are any issues or questions, please let me know.
Thank you.