Het probleem
Ik ben bezig een spelletje met 'RTS besturing' te maken. Nu heb ik een werkend pathfinding algoritme alleen in moeilijke situaties doet dit algoritme er 'enkele seconden' over om een oplossing te vinden. Dit zorgt helaas op zo'n moment voor een complete lock-up van het spelletje.De belangrijkste situaties waarin dit optreed zijn:
1. Een grote afstand op een 'grote map' die vooral leeg is
2. Een niet bestaande route op een 'grote map'
Een 'grote map' bestaat hier uit 40000 tegels.
Welke oplossingen heb ik geprobeerd?
Two way flood fill
Hiermee is probleem twee al voor een groot deel de wereld uit geholpen als de obstructie tenminste een beetje in de buurt van de actor of het doel ligt. Als tijdelijke pleister draait dit nu tegelijk met A* en stop A* als hij een niet oplosbare situatie tegenkomt.Aparte thread voor updates
Dit werkt goed en zorgt er in ieder geval voor de view van de game door blijft updaten en dat dit probleem geen complete lock-up meer veroorzaakt. Echter staan alle actors stil bij een zware route want op dit moment gebeurd het berekenen van de route bij een actor update.Aparte thread voor actor updates welke ik ook nog asynchroom laat verlopen
Zelfde als hierboven met het voordeel dat er meerdere zware routes tegelijk in plaats van achter elkaar uitgerekend kunnen worden. Waardoor als je een groep ergens heen stuurt het in totaal korter duurt.Het A* algoritme optimaliseren
Dit is best een uitdaging gezien ik als amateur die zichzelf wat C++ geleerd heeft eigenlijk maar wat aanmodder.- Ik heb zoveel mogelijk berekeningen die iedere iteratie hetzelfde zijn vervangen voor een statische int die aan het begin 1x uitgerekend wordt.
- In plaats van dat ik alle buurcellen (waarbij ik dynamisch rekening moet houden met het passeerbaar zijn van een cell) aan een cel toevoeg bij het bouwen van van de cell vector voeg ik deze nu pas toe als het algoritme de cell voor het eerst tegen komt. Bij een makkelijk vindbaar doel scheelt dit toch wel tijd gezien er dan weinig cellen geëvalueerd moeten worden.
Optimalisatie A* welke niet lukt
- Het liefst zou ik de array met standaard 8 buurcellen die of -1 zijn of een id hebben willen vervangen voor een lijst. Waar gewoon de gevonden buurcellen instaan. Alleen vind de vector waar een struct inzit waarin die lijst weer zou moeten zitten het volgende commando niet fijn:cellsList[i].neighbours.push_back() == stackoverflow
cellsList[i].neighbours.front() == stackoverflow
enzovoorts...
Met andere woorden geen idee hoe ik die lijst moet benaderen om er iets in te zetten en uit te halen. Een array werkte wel, dan maar dat.

Nu kan het dus zo zijn dat voor een cell met maar 1 buurman waar ik heenkan ik 8 keer een adres in een array moet checken.
Multithreaded bi-derectional A*
Dit geeft misschien niet het meest optimale pad maar is in theorie wel twee keer zo snel! Het is in theorie ook simpel te maken. Je wisselt het begin en eindpunt om in het al bestaande algortime voor de tweede thread en je laat aan het einde als er een botsing was de eerste thread de route vanaf het startpunt tot de botsing in de lijst zetten en hierna mag de tweede thread de cellen van de botsing naar het doel toevoegen.Los van elkaar heb ik het zo werkend gekregen. Het pijnpunt is echter omdat ik het vanwege de snelheid in twee threads wil hebben dat ik van beide threads een lijst met gecheckte cellen moet bijhouden welke de andere thread in moet kunnen zien om een botsing te kunnen detecteren. Hiervoor had ik twee arrays gemaakt waar de ene thread in schrijft en de andere in leest en vicaversa. Echter als ik met deze aanpassing de twee pathfinding threads draai geeft hij een segmentation fault terug. Het gekke is dat in de debugger hij deze regel aangeeft als pijnpunt: regel 124 in bijgevoegde source
code:
1
| if(!cellsList[newCellId].visited) |
Beide threads hebben echter hun eigen 'cellsList' dus dat zou geen probleem moeten geven. Wat wel is dat de gewraakte lees en schrijfacties naar de twee arrays wel plaats gaan vinden als deze if statement true is. Dus mogelijk dat de debugger daarom hier blijft hangen?
Het werkt namelijk prima als ik de twee threads naast elkaar heb draaien en die lees en schrijfacties naar die array heb weggecomment.
De array vervangen voor een vector geeft hetzelfde gedrag. Met of zonder mutex lock.
Wat wil ik nog gaan proberen?
De pathfinding uit de actor update halen
Op dit moment roept een de update functie van de actor de pathfinding aan als die erachter komt dat er nog een pad berekend moet worden. Ik zou dit los kunnen koppelen en een aparte pathupdater thread kunnen maken die de hele tijd alle actors langsgaat en paden uitrekend waar nodig. Op die manier blijft de rest van het spel doorgaan.[h2]Single threaded bidirectional A*[h2]
Als ik geen oplossing vindt voor het eerder beschreven probleem
Waypoints op de map
Liever niet gezien de map met gebouwen en actors die niet tegelijk op een hokje mogen de map nogal dynamisch maken.Tot slot
Zoals jullie zien is het performance probleem van de pathfinding een aardige bezighouder voor mij geweest. Ik heb hierdoor wel voor het eerst ooit verdiept in multithreading! Dus leerzaam is het zeker. Ik heb alleen wel het idee dat het een beetje een roadblock begint te worden om dit spel verder af te maken. Als jullie verdere suggesties hebben of een idee waarom hij segmentation faults uit blijft spugen sta ik open voor suggesties!Sauce
code:
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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
| struct Cells{ int positionX, positionY, parentCellId, cummulativeCost, cellId; double costToGoal, totalCostGuess; bool visited = false; bool obstacle = false; int neighbours[8]; }; struct routeCell { int positionX; int positionY; int visited; int parentCellId; }; void actors::calculateRoute() { this->noPathPossible = false; this->forwardIsDone = false; this->collisionCell = -1; this->mapArray.reserve(MAP_HEIGHT*MAP_WIDTH); this->mapArrayBack.reserve(MAP_HEIGHT*MAP_WIDTH); for(int i = 0; i < MAP_HEIGHT*MAP_WIDTH; i++) { this->mapArray[i] = 0; this->mapArrayBack[i] = 0; } std::thread pathfinding(&actors::pathAStar, &listOfActors[this->actorId], false); std::thread pathfindingBackwards(&actors::pathAStar, &listOfActors[this->actorId], true); if(canTargetBeReached()) { pathfinding.join(); pathfindingBackwards.join(); } else { this->noPathPossible = true; pathfinding.join(); pathfindingBackwards.join(); } } std::mutex mtx; void actors::pathAStar(bool backward) { std::vector<Cells> cellsList; cellsList.reserve(MAP_HEIGHT*MAP_WIDTH); updateCells(this->actorGoal[0], this->actorGoal[1], cellsList); std::list<Cells> listToCheck; std::list<Cells> checkedList; int startCell; int endCell; if(!backward) { startCell = (actorCords[0]*MAP_HEIGHT)+actorCords[1]; //eigen positie endCell = (actorGoal[0]*MAP_HEIGHT)+actorGoal[1]; //doel positie } else { endCell = (actorCords[0]*MAP_HEIGHT)+actorCords[1]; //eigen positie startCell = (actorGoal[0]*MAP_HEIGHT)+actorGoal[1]; //doel positie } bool endReached = false; //check of de doelcel niet 1 hokje weg is if(((actorCords[0]-actorGoal[0] == 0) ||(actorCords[0]-actorGoal[0] == -1) ||(actorCords[0]-actorGoal[0] == 1)) && ((actorCords[1]-actorGoal[1] == 0) ||(actorCords[1]-actorGoal[1] == -1) ||(actorCords[1]-actorGoal[1] == 1))) { if(!cellsList[endCell].obstacle) { this->pathFound = true; endReached = true; } else { this->pathFound = false; } } else { this->pathFound = false; addNeighbours(startCell, cellsList); cellsList[startCell].visited = true; listToCheck.push_back(cellsList[startCell]); } while(!listToCheck.empty() && startCell != endCell) { if(this->collisionCell != -1) { listToCheck.clear(); this->pathFound = true; } //sorteer de lijst en zet de cell met de laagste cost to goal bovenaan om het eerst te testen listToCheck.sort([](const Cells &f, const Cells &s) { return f.totalCostGuess < s.totalCostGuess; }); //Check of de te checken cell het doel is. Als dat zo is zijn we klaar if(listToCheck.front().cellId == endCell) { listToCheck.clear(); this->pathFound = true; } else if(this->noPathPossible) { listToCheck.clear(); } else { for(int q = 0; q < 8; q++) { if(listToCheck.front().neighbours[q] != -1) { int newCellId = listToCheck.front().neighbours[q]; //We have found neighbours! //check if neighbours was found before if(!cellsList[newCellId].visited) { //Deze cell heeft geen parent is is dus nooit eerder gevonden! De buren moeten dus toegevoegd worden! addNeighbours(newCellId, cellsList); //De cell waarvan we de neighbours onderzoeken is dus automagisch tot nu toe de kortste route hiernaartoe cellsList[newCellId].parentCellId = listToCheck.front().cellId; //Nu moeten de kosten voor de route hiernatoe uitgerekend worden (Dit zijn de kosten van naar de buurman gaan +1 cellsList[newCellId].cummulativeCost = listToCheck.front().cummulativeCost+1; //Als laatste zetten we de cell in de lijst met cellen die gecheckt moet worden listToCheck.push_back(cellsList[newCellId]); //Bereken de afstand naar het doel cellsList[newCellId].costToGoal = dist(cellsList[newCellId].positionX,cellsList[newCellId].positionY,cellsList[endCell].positionX,cellsList[endCell].positionY); cellsList[newCellId].totalCostGuess = cellsList[newCellId].costToGoal + cellsList[newCellId].cummulativeCost; cellsList[newCellId].visited = true; while(!mtx.try_lock()){} if(backward) { if(this->mapArray[newCellId] == 1) { //Collisiion! this->collisionCell = newCellId; } else { this->mapArrayBack[newCellId] = 1; } } else { if(this->mapArrayBack[newCellId] == 1) { //Collisiion! this->collisionCell = newCellId; } else { this->mapArray[newCellId] = 1; } } mtx.unlock(); } else { //Deze cell is al eerder gevonden, staat dus al in de te checken cell lijst if(listToCheck.front().cummulativeCost+1 < cellsList[newCellId].cummulativeCost) { //Er is een kortere route naar deze cell! Pas de parent cell dus aan en geef een nieuwe cummulative Cost; cellsList[newCellId].parentCellId = listToCheck.front().cellId; cellsList[newCellId].cummulativeCost = listToCheck.front().cummulativeCost+1; cellsList[newCellId].totalCostGuess = cellsList[newCellId].costToGoal + cellsList[newCellId].cummulativeCost; } } } } checkedList.push_back(listToCheck.front()); listToCheck.pop_front(); } } //Zet de te lopen route in een lijst if(!backward) { this->route.clear(); if(this->collisionCell == -1) { this->route.push_back({cellsList[endCell].positionX, cellsList[endCell].positionY, cellsList[endCell].visited, cellsList[endCell].parentCellId}); } else { this->route.push_back({cellsList[collisionCell].positionX, cellsList[collisionCell].positionY, cellsList[collisionCell].visited, cellsList[collisionCell].parentCellId}); this->forwardIsDone = true; } if(this->pathFound) { while(!endReached) { if(route.back().visited == true) { this->route.push_back({cellsList[route.back().parentCellId].positionX, cellsList[route.back().parentCellId].positionY, cellsList[route.back().parentCellId].visited, cellsList[route.back().parentCellId].parentCellId}); if(this->route.back().parentCellId == startCell) { endReached = true; } } else { endReached = true; } } } } else if(this->pathFound && this->collisionCell != -1) { while(!this->forwardIsDone) { //wait } while(!endReached) { if(route.front().visited == true) { this->route.push_front({cellsList[route.front().parentCellId].positionX, cellsList[route.front().parentCellId].positionY, cellsList[route.front().parentCellId].visited, cellsList[route.front().parentCellId].parentCellId}); if(this->route.front().parentCellId == startCell) { this->route.push_front({cellsList[route.front().parentCellId].positionX, cellsList[route.front().parentCellId].positionY, cellsList[route.front().parentCellId].visited, cellsList[route.front().parentCellId].parentCellId}); endReached = true; } } else { endReached = true; } } } } |
complete bron: https://github.com/switchboy/isometric
My Steam Profile (Name Switch) Worth: 889€ (225€ with sales)Games owned: 83