A* Pathfinding Algorithm

Posted on

Problem

I have programmed a A* Pathfinding algorithm in the Console. I would now like to know if there are any things that I could do to improve the time it takes, to find a path. Right now it takes around 80ms.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

Those are the necessary Using directories

class Node
{
    public bool wall; //Ist die Node eine Wand
    public bool start; //Ist die Node die Start-Node
    public bool end; //Ist die Node die End-Node

    public Node previous; //Vorherige Node / weg zu dieser Node

    public int x, y; // X und Y position

    public int g, h, f; //G = Distance from starting node / H = Distance from end node / F = G + H

    public Node(int x, int y) //Initialsierung
    {
        this.x = x;
        this.y = y;
    }
}

class Map
{
    public int Width { get; private set; }
    public int Height { get; private set; }
    public Node start;
    public Node end;
    public Node[,] map;

    public readonly List<Node> searching = new List<Node>(); //Auch offene Liste gennant
    public readonly List<Node> searched = new List<Node>(); //Auch geschlossene Liste gennant

    public Map(int wi, int he) //Initialisierung
    {
        Width = wi;
        Height = he;

        map = new Node[wi, he];
        for (int i = 0; i < Width; i++)
        {
            for (int j = 0; j < Height; j++)
            {
                map[i, j] = new Node(i, j); //Generisch Initialisiert
            }
        }
    }
    public void SetStart(int x, int y) //Setzt den Startpunkt
    {
        map[x, y].start = true;
        map[x, y].end = false;
        map[x, y].g = 0;
        start = map[x, y];
    }
    public void SetEnd(int x, int y) //Setzt den Endpunkt
    {
        map[x, y].end = true;
        map[x, y].start = false;
        map[x, y].h = 0;
        end = map[x, y];
    }
    public void SetWalls(List<int[]> walls) //Setzt die Wände an Ihre Positionen
    {
        for (int i = 0; i < walls.Count; i++)
        {
            map[walls[i][0], walls[i][1]].wall = true;
        }
    }
    private void CalculateGCost(int x, int y, Node previous) //Errechnet die G Cost / die Distanz zu dem Startpunkt
    {
        int cost = 10;

        if (!map[x, y].start && !map[x, y].end)
        {

            if (previous.x != x && previous.y != y) //neu
            {
                cost += 4;
            }

            map[x, y].g = previous.g + cost;
            map[x, y].previous = previous;
        }
    }
    public void CalculateAllHCost() //Errechnet für die ganze Karte die H Cost / die Distanz zu dem Endpunkt
    {
        for (int i = 0; i < Width; i++)
        {
            for (int j = 0; j < Height; j++)
            {
                CalculateHCost(i, j);
            }
        }
    }
    private void CalculateHCost(int x, int y) //Errechnet für dieses eine Feld die H Cost / die Distanz zu dem Endpunkt
    {
        if (map[x, y] != end)
        {
            int xstep = Math.Abs(end.x - x); //Wie weit das ende von dem jetzigen Punkt in der X Achse entfernt ist
            int ystep = Math.Abs(end.y - y); //Wie weit das ende von dem jetzigen Punkt in der Y Achse entfernt ist
            map[x, y].h = (xstep + ystep) * 10; //Beides zusammen ist die Insgesamte entfernung
        }
    }
    private void CalculateFCost(int x, int y) => map[x, y].f = map[x, y].g + map[x, y].h; //F Cost ist H Cost + G Cost
    public void StartPathFinding() //Startet den Algorithmus
    {
        searching.Add(start); //Fügt die Start Node der offenen liste Searching hinzu.

        ShowMap();
        Search();
    }
    private void PathFinding(int x, int y) //Findet die benachbarten Nodes und fügt sie in die offene Liste hinzu, wenn sie dort nicht schon sind mit einem besseren vorgänger.
    {
        for (int i = -1; i < 2; i++)
        {
            for (int j = -1; j < 2; j++)
            {
                if ((x + i >= 0 && x + i < Width) & (y + j >= 0 && y + j < Height))
                {
                    if (!map[x + i, y + j].wall)
                    {
                        if (map[x + i, y + j].g == 0 & !map[x + i, y + j].start)
                        {
                            map[x + i, y + j].previous = map[x, y];
                            searching.Add(map[x + i, y + j]);
                        }
                        else if (map[x + i, y + j].g > map[x, y].g + (i == 0 && j == 0 ? 10 : 14))
                        {
                            map[x + i, y + j].previous = map[x, y];
                            Node t = searched.Find(item => item.x == x + i && item.y == y + j);
                            searched.Remove(t);
                            searching.Add(map[x + i, y + j]);
                        }
                    }
                }
            }
        }
    }
    private void Search() //Das Herzstück des Algorithmus, der die Anweisungen gibt, wie der Weg gefunden wird
    {
        Node node;
        do
        {
            foreach (var item in searching) //Für jede Node in der offenen Liste
            {
                CalculateGCost(item.x, item.y, item.previous); //Berechnet die G Cost / die Distanz zu dem Startpunkt
                CalculateFCost(item.x, item.y); //Berechnet die F Cost / die Distanz zu dem Startpunkt + die Distanz zu dem Endpunkt

            }

            int lowestF = searching.Min(c => c.f); //Sucht die niedrigste F Cost
            List<Node> nodes = searching.FindAll(c => c.f == lowestF); //Sucht in der offenen Liste nach den Nodes mit der niedrigsten F Cost
            int lowestH = nodes.Min(c => c.h); //Sucht unter den niedrigsten F Cost Nodes nach der niedrigsten H Cost
            node = nodes.Find(c => c.h == lowestH); //Wählt die Node mit der niedrigsten F und H Cost aus


            if (node == end) //Sollte die Node zufälligerweise die End-Node sein, wird die Suche nach der End-Node abgebrochen (weil sie gefunden wurde)
            {
                break;
            }

            PathFinding(node.x, node.y); //Sucht für die Node mit der niedrigsten F und H Cost die Nachbarn und fügt sie der offenen Liste hinzu
            searching.Remove(node); //Entfernt die Node von der offenen Liste
            searched.Add(node); //und fügt sie der geschlossenen Liste hinzu
        } while (searching.Count != 0 && node != end); //Sollte die End-Node gefunden werden oder die offene Liste leer sein, ist ein fehler aufgetreten oder die End-Node wurde gefunden

    }
    public void ShowMap() //Zeigt die Karte an
    {
        for (int i = 0; i < Width; i++)
        {
            for (int j = 0; j < Height; j++)
            {
                Console.SetCursorPosition(i, j);
                if (map[i, j].start)
                {
                    Console.ForegroundColor = ConsoleColor.Blue; //Makiert den Start mit einer blauen Schriftfarbe
                }
                else if (map[i, j].end)
                {
                    Console.ForegroundColor = ConsoleColor.Yellow; //Makiert das Ende mit einer gelben Schriftfarbe
                }
                else
                {
                    Console.ForegroundColor = ConsoleColor.Gray; //Alles andere wird mit einer grauen Schriftfarbe geschrieben
                }
                Console.WriteLine(map[i, j].wall | map[i, j].end | map[i, j].start ? "X" : " "); //Schreibt ein X wenn es sich bei dem Feld um eine Wand / das Ende / den Start handelt
            }
        }
    }
    public void ShowPath(Node node) //Malt den schnellsten Weg von der Start-Node zu der End-Node an.
    {
        System.Threading.Thread.Sleep(100);
        Console.SetCursorPosition(node.x, node.y);
        Console.BackgroundColor = ConsoleColor.White;
        Console.WriteLine("X");
        Console.BackgroundColor = ConsoleColor.Black;
        if (node != start)
        {
            ShowPath(node.previous); //Zeigt die nächste Node in der Reihe
        }
        else
        {
            Console.SetCursorPosition(0, Height + 1); //Die Reihe ist fertig, es wird an das ende der Map gesetzt um weiter Infos auf der Konsole auszugeben ohne die Karte zu überschreiben
        }
    }
}

class Program
{
    static void Main(string[] args)
    {

        Console.ReadKey(true);

        int counter = 0; //Wieviele Linien es gibt
        int longestLine = 0; //Die Längste Linie
        string line; //Was die Linie beinhaltet

        List<int[]> walls = new List<int[]>(); //Die Wände, die durch die Input Text Datei gesetzt wurden

        int[] start = new int[2]; //Start-Node Koordinaten
        int[] end = new int[2]; //End-Node Koordinaten



        System.IO.StreamReader file =
            new System.IO.StreamReader(@"PUT THE PATH TO YOUR TXT FILE HERE");
        while ((line = file.ReadLine()) != null)
        {
            if (line.Length > longestLine)
            {
                longestLine = line.Length; //Guckt nach der längsten Linie (Die breite der Karte)
            }

            int counter2 = 0;
            foreach (char x in line)
            {
                if (x == 'X') //Sollte ein "X" angegeben sein, bedeutet das, dass dort eine Wand gestzt werden muss
                {
                    walls.Add(new int[] { counter2, counter });
                }
                else if (x == 'S') //Sollte ein "S" angegeben sein, bedeutet das, dass dort der Startpunkt / die Start-Node gesetzt werden muss
                {
                    start[0] = counter2;
                    start[1] = counter;
                }
                else if (x == 'E') //Sollte ein "E" angegeben sein, bedeutet das, dass dort der Endpunkt / die End-Node gesetzt werden muss
                {
                    end[0] = counter2;
                    end[1] = counter;
                }
                counter2++;
            }
            counter++; //Guckt nach der Anzahl der Linien (Die Höhe der Karte)
        }

        Map map = new Map(longestLine, counter); //Karte wird Initialisiert

        map.SetStart(start[0], start[1]); //Start wird gesetzt
        map.SetEnd(end[0], end[1]); //Ende wird gesetzt
        map.SetWalls(walls); //Wände werden gesetzt

        System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
        sw.Start();
        map.CalculateAllHCost(); //Alle H Costs werden berechnet
        map.StartPathFinding(); //Der Algorithmus startet
        sw.Stop();

        map.ShowMap();
        map.ShowPath(map.end);

        Console.WriteLine("The Path took {0} ms to calculate", sw.ElapsedMilliseconds);

        foreach (Node node in map.searched)
        {
            Console.WriteLine("{0,-6} : G = {1,-4}| H = {2,-4}| F = {3,-4}", node.x + ", " + node.y, node.g, node.h, node.f); //Alle untersuchten Nodes werden einmal aufgeführt
        }

        Console.ReadKey(true);
    }
}

Im sorry that all my Comments are in german, but the code should still be readable.
Somewhere in the Main Method you have to Specify a Path to a txt file, that is used as the map.

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
X                   X       XX   X   X    X      X
X                     XXX  X   X   X   X         X
X                   X  X     XXXXXXXXXXXXX XXXXXXX
X                   XX XX    X           X       X
X                 XXX  XX    X           X       X
X                       X    X           XXXX    X
X                 X  E  XXXXXX                   X
X                 X     X    XXXXXXX             X
X                 XX XXXX                        X
X                    X                           X
X                    X                           X
X                    X                           X
X                    X             XXXXXXXXXXXX  X
X                    X XXXXXXXXXXXX              X
X            XXXX XXXX             X   X   XXXXX X
X                 X                X   XXXXXS  X X
X                                  X       X X   X
X                 X                X   X     XX XX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

you can copy this map into the txt file and youll have the same one as I do.

I hope you can help me or give me feedback on the overall quality of the code.

Solution

I cannot comment on the pathfinding algorithm yet because i don’t remember how it should work. But your ellapsed time capture includes ShowMap and that isnt part of the algorithm. ShowMapis the most time consuming part of your time measurement, so i suggest you remove that and re-measure.
If you havent yet, you can use the built in performance profiler in Visual Studio.

This image shows the hot path in your application.
enter image description here

Leave a Reply

Your email address will not be published. Required fields are marked *