Hi,
I implemented A* search algorithm for my enemy to go to any voice or player. But I want to call it if enemy hears a voice or sees the player and each time I call it, I want it to reset the path accordingly. But when target position changes, sometimes program gives this error and but enemy still be able to follow the player, it just stops for a second. Also enemy cannot move to that location exactly, stops before reaching it:
And here’s the error code: (I’ve marked the error line as “this is the error line”, you can find it with ctrl+f)
IndexOutOfRangeException: Index was outside the bounds of the array.
Enemy+d__43.MoveNext () (at Assets/Scripts/Enemy/Enemy.cs:147)
UnityEngine.SetupCoroutine.InvokeMoveNext (System.Collections.IEnumerator enumerator, System.IntPtr returnValueAddress) (at :0)
UnityEngine.MonoBehaviour:StartCoroutine(String)
Enemy:OnPathFound(Vector3[ ], Boolean) (at Assets/Scripts/Enemy/Enemy.cs:139)
PathRequestManager:FinishedProcessingPath(Vector3[ ], Boolean) (at Assets/Scripts/PathRequestManager.cs:41)
d__3:MoveNext() (at Assets/Scripts/Pathfinding.cs:80)
UnityEngine.SetupCoroutine:InvokeMoveNext(IEnumerator, IntPtr)
Enemy Script (not all of it, just related part)
using System.Collections;
using System.Collections.Generic;
using System.Security.Cryptography;
using TMPro;
using UnityEngine;
using static UnityEngine.GraphicsBuffer;
public class Enemy : MonoBehaviour
{
// Components
Animator anim;
[Header("Cursor Indicator")]
[SerializeField] public GameObject cursorIndicatorPrefab;
private GameObject cursorIndicator;
[Header("Turning Around")]
[SerializeField] float timeBetweenTurns = 5f;
float timeSinceLastTurn;
[Header("Flags and States")]
[SerializeField] bool canPatrol = false;
[SerializeField] bool canTurnAround = true;
bool sawPlayer;
bool heardVoice = false;
bool walking = true;
bool passedOut = false;
bool dead = false;
bool patroling = false;
bool lockedOn = false; // Checks if the player locked onto this specific enemy
bool hasNoGun = true;
bool hasMeleeGun = false;
bool hasGun = false;
bool hasRifle = false;
[Header("Movement")]
[SerializeField] private float walkSpeed = 1f;
[SerializeField] private float triggerSpeed = 2f;
[SerializeField] private List<Transform> patrolWaypoints; // List of waypoints for patrolling
private int currentWaypointIndex = 0; // Index of the current waypoint
private Vector3 lastVoicePosition; // Store the last hit position
GameObject[] doors;
Vector3[] path;
int targetIndex;
Transform target;
[Header("Death")]
[SerializeField] public GameObject deathPrefab;
private GameObject death;
[Header("Attack")]
[SerializeField] LayerMask attackableLayer; // Player layer
[Header("Melee Combat")]
[SerializeField] Transform MeleeAttackTransform;
[SerializeField] Vector2 MeleeAttackArea;
[Header("Voice")]
[SerializeField] int voiceRange = 1; // The range that enemy absolutely hear anything
[Header("Devices")]
private GameObject[] devices; // All devices on scene
[Header("View")]
[SerializeField] Transform AngleOfViewTransform;
float sideLength = 15f;
float angleOfView = 90.0f;
float viewDistance = 7f;
// Start is called before the first frame update
void Start()
{
anim = GetComponent<Animator>();
// Initializing enemy flags
sawPlayer = false;
dead = false;
lockedOn = false;
// Initializing and Hiding Cursor Indicator
cursorIndicator = Instantiate(cursorIndicatorPrefab, transform.position, Quaternion.identity);
cursorIndicator.SetActive(lockedOn);
// Initializing Devices
devices = GameObject.FindGameObjectsWithTag("Device");
doors = GameObject.FindGameObjectsWithTag("Door");
//PathRequestManager.RequestPath(transform.position, PlayerController.Instance.transform.position, OnPathFound);
}
// Update is called once per frame
void Update()
{
if (!dead)
{
checkAngleOfView();
checkVoice();
TurnAround();
if (hasNoGun || hasMeleeGun)
{
MeleeAttack();
}
}
}
private void FixedUpdate()
{
if (!dead)
{
checkLockedOn();
if (!sawPlayer && !heardVoice)
{
Patrol();
}
else if (sawPlayer)
{
//MoveToPlayer();
MoveToPosition(PlayerController.Instance.transform.position);
}
else if (!sawPlayer && heardVoice)
{
//MoveToVoice();
MoveToPosition(lastVoicePosition);
}
}
}
private void OnDrawGizmos() // For now, only draws melee attack area
{
Gizmos.color = Color.yellow;
Gizmos.DrawWireCube(MeleeAttackTransform.position, MeleeAttackArea);
}
public void OnPathFound(Vector3[] newPath, bool pathSuccessfull)
{
if (pathSuccessfull)
{
path = newPath;
StopCoroutine("FollowPath");
StartCoroutine("FollowPath");
}
}
public IEnumerator FollowPath()
{
if (path != null && !dead)
{
Vector3 currentWayPoint = path[0]; // This is the error line
while (true)
{
if (transform.position == currentWayPoint)
{
targetIndex++;
if (targetIndex >= path.Length)
{
anim.SetBool("Walking", false);
yield break;
}
currentWayPoint = path[targetIndex];
}
if (sawPlayer)
{
transform.position = Vector3.MoveTowards(transform.position, currentWayPoint, triggerSpeed * Time.deltaTime);
Vector3 movementDirection = PlayerController.Instance.transform.position - transform.position;
movementDirection.z = 0;
movementDirection.Normalize();
float angle = Mathf.Atan2(movementDirection.y, movementDirection.x) * Mathf.Rad2Deg;
transform.rotation = Quaternion.AngleAxis(angle, Vector3.forward);
anim.SetBool("Walking", movementDirection.magnitude > 0);
}
else
{
transform.position = Vector3.MoveTowards(transform.position, currentWayPoint, walkSpeed * Time.deltaTime);
Vector3 movementDirection = lastVoicePosition - transform.position;
movementDirection.z = 0;
movementDirection.Normalize();
float angle = Mathf.Atan2(movementDirection.y, movementDirection.x) * Mathf.Rad2Deg;
transform.rotation = Quaternion.AngleAxis(angle, Vector3.forward);
anim.SetBool("Walking", movementDirection.magnitude > 0);
}
yield return null;
}
}
}
void MoveToPosition(Vector3 position)
{
PathRequestManager.RequestPath(transform.position, position, OnPathFound);
}
Pathfinding Script
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System;
using UnityEngine;
public class Pathfinding : MonoBehaviour
{
PathRequestManager requestManager;
Grid grid;
void Awake()
{
grid = GetComponent<Grid>();
requestManager = GetComponent<PathRequestManager>();
}
IEnumerator FindPath(Vector3 startPos, Vector3 targetPos)
{
Stopwatch sw = new Stopwatch();
sw.Start();
Vector3[] waypoints = new Vector3[0];
bool pathSuccess = false;
Node startNode = grid.NodeFromWorldPoint(startPos);
Node targetNode = grid.NodeFromWorldPoint(targetPos);
if (startNode.walkable && targetNode.walkable)
{
Heap<Node> openSet = new Heap<Node>(grid.MaxSize);
HashSet<Node> closedSet = new HashSet<Node>();
openSet.Add(startNode);
while (openSet.Count > 0)
{
Node currentNode = openSet.RemoveFirst();
closedSet.Add(currentNode);
if (currentNode == targetNode)
{
sw.Stop();
// print("Path found in: " + sw.ElapsedMilliseconds + " ms");
pathSuccess = true;
break;
}
foreach (Node neighbor in grid.GetNeighbors(currentNode))
{
if (!neighbor.walkable || closedSet.Contains(neighbor))
{
continue;
}
int newMovementCostToNeighbor = currentNode.gCost + getDistance(currentNode, neighbor);
if (newMovementCostToNeighbor < neighbor.gCost || !openSet.Contains(neighbor))
{
neighbor.gCost = newMovementCostToNeighbor;
neighbor.hCost = getDistance(neighbor, targetNode);
neighbor.parent = currentNode;
if (!openSet.Contains(neighbor))
{
openSet.Add(neighbor);
}
else
{
openSet.UpdateItem(neighbor);
}
}
}
}
}
yield return null;
if (pathSuccess)
{
waypoints = RetracePath(startNode, targetNode);
}
requestManager.FinishedProcessingPath(waypoints, pathSuccess);
}
public Vector3[] RetracePath(Node startNode, Node endNode)
{
List<Node> path = new List<Node>();
Node currentNode = endNode;
while (currentNode != startNode)
{
path.Add(currentNode);
currentNode = currentNode.parent;
}
Vector3[] waypoints = SimplifyPath(path);
Array.Reverse(waypoints);
return waypoints;
}
public Vector3[] SimplifyPath(List<Node> path)
{
List<Vector3> waypoints = new List<Vector3>();
Vector2 directionOld = Vector2.zero;
for (int i = 1; i < path.Count; i++)
{
Vector2 directionNew = new Vector2(path[i - 1].gridX - path[i].gridX, path[i - 1].gridY - path[i].gridY);
if (directionNew != directionOld)
{
waypoints.Add(path[i].worldPosition);
}
directionOld = directionNew;
}
return waypoints.ToArray();
}
public int getDistance(Node a, Node b)
{
int dstX = Mathf.Abs(a.gridX - b.gridX);
int dstY = Mathf.Abs(a.gridY - b.gridY);
if (dstX > dstY)
{
return 14 * dstY + 10 * (dstX - dstY);
}
else
{
return 14 * dstX + 10 * (dstY - dstX);
}
}
public void StartFindPath(Vector3 startPos, Vector3 targetPos)
{
StartCoroutine(FindPath(startPos, targetPos));
}
}
PathRequestManager Script
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public class PathRequestManager : MonoBehaviour
{
Queue<PathRequest> pathRequestQueue = new Queue<PathRequest>();
PathRequest currentPathRequest;
static PathRequestManager Instance;
Pathfinding pathfinding;
bool isProcessingPath;
void Awake()
{
Instance = this;
pathfinding = GetComponent<Pathfinding>();
}
public static void RequestPath(Vector3 pathStart, Vector3 pathEnd, Action<Vector3[], bool> callback)
{
PathRequest newRequest = new PathRequest(pathStart, pathEnd, callback);
Instance.pathRequestQueue.Enqueue(newRequest);
Instance.TryProcessNext();
}
void TryProcessNext()
{
if (!isProcessingPath && pathRequestQueue.Count > 0)
{
currentPathRequest = pathRequestQueue.Dequeue();
isProcessingPath = true;
pathfinding.StartFindPath(currentPathRequest.pathStart, currentPathRequest.pathEnd);
}
}
public void FinishedProcessingPath(Vector3[] path, bool success)
{
currentPathRequest.callback(path, success);
isProcessingPath = false;
TryProcessNext();
}
struct PathRequest
{
public Vector3 pathStart;
public Vector3 pathEnd;
public Action<Vector3[], bool> callback;
public PathRequest(Vector3 _start, Vector3 _end, Action<Vector3[], bool> _callback)
{
pathStart = _start;
pathEnd = _end;
callback = _callback;
}
}
}