I am currently using an implementation of the A* algorithm described here.
Currently, when I run my algorithm, my IDE lags horribly and the .exe generated also lags terribly. Using the CPU analyzer, I narrowed down the culprit using around 95% of the available CPU to my PathFind
method. I'm not entirely sure where to start with optimizing the algorithm or making it less taxing on the system.
public struct Grid
{
public Rectangle Size;
public byte[,] Weight;
/// <summary>
/// Creates a grid for pathfinding, if weight is 0 is inaccessible, non-zero indicates accessible
/// </summary>
/// <param name="posX"></param>
/// <param name="posY"></param>
/// <param name="width"></param>
/// <param name="height"></param>
/// <param name="defaultValue"></param>
public Grid(int posX, int posY, int width, int height, byte defaultValue = 0)
{
Size = new Rectangle(posX, posY, width, height);
Weight = new byte[width, height];
for(int i = 0; i < width; i++)
{
for(int j = 0; j < height; j++)
{
Weight[i, j] = defaultValue;
}
}
}
public void SetWeight(int x, int y, byte weight)
{
Weight[x, y] = weight;
}
public List<Point> PathFind(Point start, Point end)
{
//Nodes that have already been analyzed and have a path from start to them
List<Point> closedSet = new List<Point>();
//Nodes that have been identified as a neighbor of an analyzed node, but not fully analyzed
List<Point> openSet = new List<Point> { start };
//A dictionary identifying the optimal origin point to each node. This is used to back track from the end to find the optimal path
Dictionary<Point, Point> cameFrom = new Dictionary<Point, Point>();
//A dictionary indicating how far ceach analyzed node is from the start
Dictionary<Point, int> currentDistance = new Dictionary<Point, int>();
//A dictionary indicating how far it is expected to reach the end, if the path travels through the specified node
Dictionary<Point, float> predictedDistance = new Dictionary<Point, float>();
//Initialize the start node w/ dist of 0, and an est. distance of ydist + xdist, optimal path in a square grid that doesn't allow for diagonal movement
currentDistance.Add(start, 0);
predictedDistance.Add(start, 0 + Math.Abs(start.X - end.X) + Math.Abs(start.Y - end.Y));
//If there are unanalyzed nodes, process them
while(openSet.Count > 0)
{
//Get current node with the lowest est. cost to finish
Point current = (from p in openSet orderby predictedDistance[p] ascending select p).First();
//If it is finish, return path
if(current.X == end.X && current.Y == end.Y)
{
return ReconstructPath(cameFrom, end);
}
//Move current node from open to closed
openSet.Remove(current);
closedSet.Add(current);
IEnumerable<Point> e = GetNeighborNodes(current);
//Process each valid node around the current node
foreach(Point neighbor in e)
{
var tempCurrentDistance = currentDistance[current] + 1;
//If we already know theres a faster way to this neighbor, use that route and ignore this one
if(closedSet.Contains(neighbor) && tempCurrentDistance >= currentDistance[neighbor])
{ continue; }
//If we don't know the route to this neighbor, or if this is faster, store this route
if(!closedSet.Contains(neighbor) || tempCurrentDistance < currentDistance[neighbor])
{
if(cameFrom.Keys.Contains(neighbor))
{
cameFrom[neighbor] = current;
}
else
{
cameFrom.Add(neighbor, current);
}
currentDistance[neighbor] = tempCurrentDistance;
predictedDistance[neighbor] = currentDistance[neighbor] + Math.Abs(neighbor.X - end.X) + Math.Abs(neighbor.Y - end.Y);
//If this is a new node, add it to processing
if(!openSet.Contains(neighbor))
{ openSet.Add(neighbor); }
}
}
}
throw new Exception(string.Format("unable to find a path between {0}, {1}, and {2}, {3}", start.X, start.Y, end.X, end.Y));
}
private IEnumerable<Point> GetNeighborNodes(Point node)
{
List<Point> nodes = new List<Point>();
//Up
if(node.Y - 1 >= 0)
{
if (Weight[node.X, node.Y - 1] > 0)
{
nodes.Add(new Point(node.X, node.Y - 1));
}
}
//Right
if(node.X + 1 < Size.Width)
{
if (Weight[node.X + 1, node.Y] > 0)
{
nodes.Add(new Point(node.X + 1, node.Y));
}
}
//Down
if(node.Y + 1 < Size.Height)
{
if (Weight[node.X, node.Y + 1] > 0)
{
nodes.Add(new Point(node.X, node.Y + 1));
}
}
//Left
if(node.X - 1 > 0)
{
if (Weight[node.X - 1, node.Y] > 0)
{
nodes.Add(new Point(node.X - 1, node.Y));
}
}
return nodes;
}
private List<Point> ReconstructPath(Dictionary<Point, Point> cameFrom, Point current)
{
if(!cameFrom.Keys.Contains(current))
{
return new List<Point> { current };
}
List<Point> path = ReconstructPath(cameFrom, cameFrom[current]);
path.Add(current);
return path;
}
}
This is my implementation of the algorithm in practice:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Xna.Framework;
using Wired.Code.GameObject;
namespace Wired.Code.AI
{
class AIChase : AIBase
{
private Entity _target;
private bool _requestPathUpdate;
private List<Point> _path;
private List<Point> _oldPath;
private int _pCount;
public AIChase(Entity parent, Entity target) : base(parent)
{
_target = target;
_requestPathUpdate = true;
_path = new List<Point>();
_oldPath = new List<Point>();
_pCount = 0;
}
public bool RequestPathUpdate
{
get { return _requestPathUpdate; }
}
public List<Point> Path
{
set { _path = value; }
}
public Vector2 TargetPos
{
get { return _target.GetOrigin; }
}
public override void Update(GameTime gt)
{
try
{
if (_oldPath.Count == _path.Count || _oldPath[_pCount] == _path[_pCount])
{
if (_pCount < _path.Count)
{
float dx = Math.Sign(_path[_pCount].X * 32 - _parent.Pos.X) * _parent.MoveSpeed;
float dy = Math.Sign(_path[_pCount].Y * 32 - _parent.Pos.Y) * _parent.MoveSpeed;
_parent.Move(new Vector2(dx, dy));
if (_parent.Pos == new Vector2(_path[_pCount].X * 32, _path[_pCount].Y * 32))
{
_pCount++;
}
}
}
else { _pCount = 0; }
}
catch (ArgumentOutOfRangeException e)
{
Console.WriteLine(e.Message);
}
_oldPath = _path;
}
}
}
Every update loop this line is called to set the AI path:
foreach (AIBase a in e.AIComponents)
{
if (a is AIChase)
{
if ((a as AIChase).RequestPathUpdate)
{
(a as AIChase).Path = GetPath(new Point((int)e.Pos.X / 32, (int)e.Pos.Y / 32), new Point((int)(a as AIChase).TargetPos.X / 32, (int)(a as AIChase).TargetPos.Y / 32));
}
}
}
(a as AIChase)
it would probably be beneficial to just place that into a variable and use that value. Would look cleaner too. – Shelby115 yesterday