Laats heb ik een implementatie gemaakt van het A* algoritme voor mensen die hier niet bekend mee zijn, het is een pathfinding algoritme gebaseerd op Dykstra's algoritme dat telkens de buur zoekt met de beste (laagste) score en probeert vanaf daar weer de volgende buur te vinden met de beste score om zo uiteindelijk bij de finish te komen. (Om doodlopende paden te voorkomen worden telkens alle buren 'ge-scored' en in een 'nog na te kijken' lijst gezet voordat de beste buur hier uit wordt gehaald).
Ik werk met een systeem van broodkruimeltjes zoals hier mooi te zien is:
Zoals je ziet is het eigenlijk een wrapper om een Vector3 die de vorige breadcrumb vast houd even als een score. Er zijn nooit 2 breadcrumbs op dezelfde positie met een zelfde code daarom gebruiken ik de equals en getHashCode van Vector3. (Ik kon helaas geen struct gebruiken omdat je dan een oneindige loop krijgt als je zichzelf als item erin zet).
Hieronder is de code te zien die probeert dit pad te vinden, eerst had ik de score en checkneighbours delen in apparte methoden maar om ervoor te zorgen dat de code sneller wordt heb ik deze nu in deze methode gezet.
Toen ik deze code geschreven had, getest had, en gepubliceerd op mijn blogje dacht ik klaar, maar al snel wees iemand me op de snelheid van deze implementatie, gek genoeg is de code namelijk vrij traag
Zoals aan de titel te zien is, is de code helaas erg traag een pad vinden in een wereld van 10x10x10 kost in release mode op mijn processor al +-450ms. Nu heb ik wel een vermoeden waarom dit vrij traag is. Volgens een poster op mijn blog is de contains methode van list erg traag en die wordt (26*2+2)*n uitgevoerd. Nu heb ik tijdelijk List<> vervangen door HashSet<> vervangen maar hier werd de code niet erg veel sneller van, terwijl het toch de bedoeling is dat de hashset contains in O(1) uit kan voeren?
Verder heb ik heel vaak het item met de laagste score nodig, ik zat te denken aan een soort van insertion sort elke keer als ik een nieuw item in de openList stop, maar hoe zorg ik er dan voor dat ik niet elke keer de hele array moet kopiëeren die achter deze nieuwe lijst zou zitten, of uberhaupt de items deels 1 item moet verplaatsen? Ik heb helaas van te voren ook geen idee hoeveel iteraties ik nodig ga hebben en hoe groot de lijst gaat worden. Voor de huidige wereld die ik heb van 10x10x10 is 1/3 van de tiles geblocked (333) en wordt de grote while loop 665x uitgevoerd (betrekkelijk weinig als je bedenkt dat er 10^10^10 mogelijkheden zijn). Maar dat betekend dus wel 885780x Contains() en een hele hoop andere code die volgens mij vaker dan nodig wordt uitgevoerd.
Maar nu ben ik een beetje verloren, welke data typen kan ik het beste gebruiken en waar zit er nu echt ruimte voor verbetering, ik denk vooral bij de Contains() methode maar een Hashset lijkt niet te werken, wat kan ik hier dan wel het beste voor gebruiken?
(hieronder de code)
Notes: het gaat hier om 3D pathfinding vanuit 1 node kun je naar 26 andere nodes (orthogonaal, diagonaal en 'dubbel diagonaal').
Edit: misschien handig om mijn streven hier neer te zetten, eigenlijk wil ik dat deze code op een 'moderne pc' maar 50ms nodig heeft voor een mogelijk veel grotere map, maar dan moet ik er sowieso wat meer niveau's in bouwen, ook is het aantal niet geblokkeerde nodes dan veel groter.
Edit2: interessant detail trouwens: hoe meer nodes geblokkeerd zijn (bijvoorbeeld 1 op 2) hoe sneller het algoritme is, waarschijnlijk omdat hij niet veel te kiezen heeft om het optimale pad te vinden.
Ik werk met een systeem van broodkruimeltjes zoals hier mooi te zien is:
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| /// <summary> /// These are the BreadCrumbs (linked list) that we lay so we can find back our path when we reach the finish /// </summary> public class BreadCrumb { public Vector3 position; public BreadCrumb next; public int cost = Int32.MaxValue; //High value so that real options will always be cheaper public BreadCrumb(Vector3 position) : this(position, null) { } public BreadCrumb(Vector3 position, BreadCrumb parent) { this.position = position; this.next = parent; } //This way we can correctly tell if a BreadCrumb is in a list. public override bool Equals(object obj) { if (obj is BreadCrumb) { return ((BreadCrumb)obj).position == this.position; } else { return false; } } public override int GetHashCode() { return position.GetHashCode(); } } |
Zoals je ziet is het eigenlijk een wrapper om een Vector3 die de vorige breadcrumb vast houd even als een score. Er zijn nooit 2 breadcrumbs op dezelfde positie met een zelfde code daarom gebruiken ik de equals en getHashCode van Vector3. (Ik kon helaas geen struct gebruiken omdat je dan een oneindige loop krijgt als je zichzelf als item erin zet).
Hieronder is de code te zien die probeert dit pad te vinden, eerst had ik de score en checkneighbours delen in apparte methoden maar om ervoor te zorgen dat de code sneller wordt heb ik deze nu in deze methode gezet.
Toen ik deze code geschreven had, getest had, en gepubliceerd op mijn blogje dacht ik klaar, maar al snel wees iemand me op de snelheid van deze implementatie, gek genoeg is de code namelijk vrij traag
Zoals aan de titel te zien is, is de code helaas erg traag een pad vinden in een wereld van 10x10x10 kost in release mode op mijn processor al +-450ms. Nu heb ik wel een vermoeden waarom dit vrij traag is. Volgens een poster op mijn blog is de contains methode van list erg traag en die wordt (26*2+2)*n uitgevoerd. Nu heb ik tijdelijk List<> vervangen door HashSet<> vervangen maar hier werd de code niet erg veel sneller van, terwijl het toch de bedoeling is dat de hashset contains in O(1) uit kan voeren?
Verder heb ik heel vaak het item met de laagste score nodig, ik zat te denken aan een soort van insertion sort elke keer als ik een nieuw item in de openList stop, maar hoe zorg ik er dan voor dat ik niet elke keer de hele array moet kopiëeren die achter deze nieuwe lijst zou zitten, of uberhaupt de items deels 1 item moet verplaatsen? Ik heb helaas van te voren ook geen idee hoeveel iteraties ik nodig ga hebben en hoe groot de lijst gaat worden. Voor de huidige wereld die ik heb van 10x10x10 is 1/3 van de tiles geblocked (333) en wordt de grote while loop 665x uitgevoerd (betrekkelijk weinig als je bedenkt dat er 10^10^10 mogelijkheden zijn). Maar dat betekend dus wel 885780x Contains() en een hele hoop andere code die volgens mij vaker dan nodig wordt uitgevoerd.
Maar nu ben ik een beetje verloren, welke data typen kan ik het beste gebruiken en waar zit er nu echt ruimte voor verbetering, ik denk vooral bij de Contains() methode maar een Hashset lijkt niet te werken, wat kan ik hier dan wel het beste voor gebruiken?
(hieronder de code)
C#:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
| /// <summary> /// Method that uses an A* algorithm to find the best path from start to end. /// </summary> /// <returns>The end breadcrump where each parent is a step back (so its a reversed path!)</returns> private static BreadCrumb FindPathReversed(World world, Vector3 start, Vector3 end) { //The openList will contains tiles we are going to examine //The closedList contains tile that we've already examined and made part of a path List<BreadCrumb> openList = new List<BreadCrumb>(); List<BreadCrumb> closedList = new List<BreadCrumb>(); Vector3 tmp; int cost = Int32.MaxValue; int diff = 0; //Add the start node to the openList BreadCrumb current = new BreadCrumb(start); BreadCrumb finish = new BreadCrumb(end); openList.Add(current); //Stop if there are no more options, or if we have found the finish while (openList.Count > 0 && !closedList.Contains(finish)) { current = openList[0]; //Find the lowest cost Tile that we can consider from the openList for (int i = 1; i < openList.Count; i++) { if (openList[i].cost < current.cost) { current = openList[i]; } } //Switch that one over to the closed list openList.Remove(current); closedList.Add(current); //Find neigbours add them to the openlist and check if those who where already //on the openlist can be reached faster this way. Also calculate their best scores //surrounding is een statische Vector3[] waarin de 26 'offsets' staan naar buren staan (-1,0,0) is linker buur for (int i = 0; i < surrounding.Length; i++) { tmp = current.position + surrounding[i]; if (tmp.X >= world.Left && tmp.X < world.Right && tmp.Y >= world.Bottom && tmp.Y < world.Top && tmp.Z >= world.Front && tmp.Z < world.Back && world.PositionIsFree(tmp)) { BreadCrumb node = new BreadCrumb(tmp, current); if (!closedList.Contains(node)) { if (!openList.Contains(node)) { openList.Add(node); } //Do scoring diff = 0; if (current.position.X != node.position.X) { diff++; } if (current.position.Y != node.position.Y) { diff++; } if (current.position.Z != node.position.Z) { diff++; } //double diagonal moves cost 17 (all 3 different) normal diagonal moves cost 14 (2 different). //Orthogonal costs 10 switch (diff) { case 1: cost = current.cost + 10; break; case 2: cost = current.cost + 14; break; case 3: cost = current.cost + 17; break; } if (cost < node.cost) //updates the cost only if the new cost is less //this way new nodes and nodes already on the openlist both get correct scores { node.cost = cost; node.next = current; } } } } } //The last added node is the node containing the path! if(closedList.Contains(finish)){ return closedList[closedList.Count -1]; }else{ return null; //no path found } } |
Notes: het gaat hier om 3D pathfinding vanuit 1 node kun je naar 26 andere nodes (orthogonaal, diagonaal en 'dubbel diagonaal').
Edit: misschien handig om mijn streven hier neer te zetten, eigenlijk wil ik dat deze code op een 'moderne pc' maar 50ms nodig heeft voor een mogelijk veel grotere map, maar dan moet ik er sowieso wat meer niveau's in bouwen, ook is het aantal niet geblokkeerde nodes dan veel groter.
Edit2: interessant detail trouwens: hoe meer nodes geblokkeerd zijn (bijvoorbeeld 1 op 2) hoe sneller het algoritme is, waarschijnlijk omdat hij niet veel te kiezen heeft om het optimale pad te vinden.