parent
94a092011d
commit
abe8d00557
@ -0,0 +1,290 @@ |
||||
/*
|
||||
OneLoneCoder.com - PathFinding A* |
||||
"No more getting lost..." - @Javidx9 |
||||
|
||||
Disclaimer |
||||
~~~~~~~~~~ |
||||
I don't care what you use this for. It's intended to be educational, and perhaps |
||||
to the oddly minded - a little bit of fun. Please hack this, change it and use it |
||||
in any way you see fit. BUT, you acknowledge that I am not responsible for anything |
||||
bad that happens as a result of your actions. However, if good stuff happens, I |
||||
would appreciate a shout out, or at least give the blog some publicity for me. |
||||
Cheers! |
||||
|
||||
Background |
||||
~~~~~~~~~~ |
||||
The A* path finding algorithm is a widely used and powerful shortest path |
||||
finding node traversal algorithm. A heuristic is used to bias the algorithm |
||||
towards success. This code is probably more interesting than the video. :-/ |
||||
|
||||
|
||||
Author |
||||
~~~~~~ |
||||
Twitter: @javidx9 |
||||
Blog: www.onelonecoder.com |
||||
|
||||
Video: |
||||
~~~~~~ |
||||
https://youtu.be/icZj67PTFhc
|
||||
|
||||
Last Updated: 08/10/2017 |
||||
*/ |
||||
|
||||
#include <iostream> |
||||
#include <string> |
||||
#include <algorithm> |
||||
using namespace std; |
||||
|
||||
#include "olcConsoleGameEngine.h" |
||||
|
||||
class OneLoneCoder_PathFinding : public olcConsoleGameEngine |
||||
{ |
||||
public: |
||||
OneLoneCoder_PathFinding() |
||||
{ |
||||
m_sAppName = L"Path Finding"; |
||||
} |
||||
|
||||
private: |
||||
|
||||
struct sNode |
||||
{ |
||||
bool bObstacle = false; // Is the node an obstruction?
|
||||
bool bVisited = false; // Have we searched this node before?
|
||||
float fGlobalGoal; // Distance to goal so far
|
||||
float fLocalGoal; // Distance to goal if we took the alternative route
|
||||
int x; // Nodes position in 2D space
|
||||
int y; |
||||
vector<sNode*> vecNeighbours; // Connections to neighbours
|
||||
sNode* parent; // Node connecting to this node that offers shortest parent
|
||||
}; |
||||
|
||||
sNode *nodes = nullptr; |
||||
int nMapWidth = 16; |
||||
int nMapHeight = 16; |
||||
|
||||
sNode *nodeStart = nullptr; |
||||
sNode *nodeEnd = nullptr; |
||||
|
||||
|
||||
protected: |
||||
virtual bool OnUserCreate() |
||||
{ |
||||
// Create a 2D array of nodes - this is for convenience of rendering and construction
|
||||
// and is not required for the algorithm to work - the nodes could be placed anywhere
|
||||
// in any space, in multiple dimensions...
|
||||
nodes = new sNode[nMapWidth * nMapHeight]; |
||||
for (int x = 0; x < nMapWidth; x++) |
||||
for (int y = 0; y < nMapHeight; y++) |
||||
{ |
||||
nodes[y * nMapWidth + x].x = x; // ...because we give each node its own coordinates
|
||||
nodes[y * nMapWidth + x].y = y; |
||||
nodes[y * nMapWidth + x].bObstacle = false; |
||||
nodes[y * nMapWidth + x].parent = nullptr; |
||||
nodes[y * nMapWidth + x].bVisited = false; |
||||
} |
||||
|
||||
// Create connections - in this case nodes are on a regular grid
|
||||
for (int x = 0; x < nMapWidth; x++) |
||||
for (int y = 0; y < nMapHeight; y++) |
||||
{ |
||||
if(y>0) |
||||
nodes[y*nMapWidth + x].vecNeighbours.push_back(&nodes[(y - 1) * nMapWidth + (x + 0)]); |
||||
if(y<nMapHeight-1) |
||||
nodes[y*nMapWidth + x].vecNeighbours.push_back(&nodes[(y + 1) * nMapWidth + (x + 0)]); |
||||
if (x>0) |
||||
nodes[y*nMapWidth + x].vecNeighbours.push_back(&nodes[(y + 0) * nMapWidth + (x - 1)]); |
||||
if(x<nMapWidth-1) |
||||
nodes[y*nMapWidth + x].vecNeighbours.push_back(&nodes[(y + 0) * nMapWidth + (x + 1)]); |
||||
|
||||
// We can also connect diagonally
|
||||
/*if (y>0 && x>0)
|
||||
nodes[y*nMapWidth + x].vecNeighbours.push_back(&nodes[(y - 1) * nMapWidth + (x - 1)]); |
||||
if (y<nMapHeight-1 && x>0) |
||||
nodes[y*nMapWidth + x].vecNeighbours.push_back(&nodes[(y + 1) * nMapWidth + (x - 1)]); |
||||
if (y>0 && x<nMapWidth-1) |
||||
nodes[y*nMapWidth + x].vecNeighbours.push_back(&nodes[(y - 1) * nMapWidth + (x + 1)]); |
||||
if (y<nMapHeight - 1 && x<nMapWidth-1) |
||||
nodes[y*nMapWidth + x].vecNeighbours.push_back(&nodes[(y + 1) * nMapWidth + (x + 1)]); |
||||
*/ |
||||
} |
||||
|
||||
// Manually positio the start and end markers so they are not nullptr
|
||||
nodeStart = &nodes[(nMapHeight / 2) * nMapWidth + 1]; |
||||
nodeEnd = &nodes[(nMapHeight / 2) * nMapWidth + nMapWidth-2]; |
||||
return true; |
||||
} |
||||
|
||||
bool Solve_AStar() |
||||
{ |
||||
// Reset Navigation Graph - default all node states
|
||||
for (int x = 0; x < nMapWidth; x++) |
||||
for (int y = 0; y < nMapHeight; y++) |
||||
{ |
||||
nodes[y*nMapWidth + x].bVisited = false; |
||||
nodes[y*nMapWidth + x].fGlobalGoal = INFINITY; |
||||
nodes[y*nMapWidth + x].fLocalGoal = INFINITY; |
||||
nodes[y*nMapWidth + x].parent = nullptr; // No parents
|
||||
} |
||||
|
||||
auto distance = [](sNode* a, sNode* b) // For convenience
|
||||
{ |
||||
return sqrtf((a->x - b->x)*(a->x - b->x) + (a->y - b->y)*(a->y - b->y)); |
||||
}; |
||||
|
||||
auto heuristic = [distance](sNode* a, sNode* b) // So we can experiment with heuristic
|
||||
{ |
||||
return distance(a, b); |
||||
}; |
||||
|
||||
// Setup starting conditions
|
||||
sNode *nodeCurrent = nodeStart; |
||||
nodeStart->fLocalGoal = 0.0f; |
||||
nodeStart->fGlobalGoal = heuristic(nodeStart, nodeEnd); |
||||
|
||||
// Add start node to not tested list - this will ensure it gets tested.
|
||||
// As the algorithm progresses, newly discovered nodes get added to this
|
||||
// list, and will themselves be tested later
|
||||
list<sNode*> listNotTestedNodes; |
||||
listNotTestedNodes.push_back(nodeStart); |
||||
|
||||
// if the not tested list contains nodes, there may be better paths
|
||||
// which have not yet been explored. However, we will also stop
|
||||
// searching when we reach the target - there may well be better
|
||||
// paths but this one will do - it wont be the longest.
|
||||
while (!listNotTestedNodes.empty() && nodeCurrent != nodeEnd)// Find absolutely shortest path // && nodeCurrent != nodeEnd)
|
||||
{ |
||||
// Sort Untested nodes by global goal, so lowest is first
|
||||
listNotTestedNodes.sort([](const sNode* lhs, const sNode* rhs){ return lhs->fGlobalGoal < rhs->fGlobalGoal; } ); |
||||
|
||||
// Front of listNotTestedNodes is potentially the lowest distance node. Our
|
||||
// list may also contain nodes that have been visited, so ditch these...
|
||||
while(!listNotTestedNodes.empty() && listNotTestedNodes.front()->bVisited) |
||||
listNotTestedNodes.pop_front(); |
||||
|
||||
// ...or abort because there are no valid nodes left to test
|
||||
if (listNotTestedNodes.empty()) |
||||
break; |
||||
|
||||
nodeCurrent = listNotTestedNodes.front(); |
||||
nodeCurrent->bVisited = true; // We only explore a node once
|
||||
|
||||
|
||||
// Check each of this node's neighbours...
|
||||
for (auto nodeNeighbour : nodeCurrent->vecNeighbours) |
||||
{ |
||||
// ... and only if the neighbour is not visited and is
|
||||
// not an obstacle, add it to NotTested List
|
||||
if (!nodeNeighbour->bVisited && nodeNeighbour->bObstacle == 0) |
||||
listNotTestedNodes.push_back(nodeNeighbour); |
||||
|
||||
// Calculate the neighbours potential lowest parent distance
|
||||
float fPossiblyLowerGoal = nodeCurrent->fLocalGoal + distance(nodeCurrent, nodeNeighbour); |
||||
|
||||
// If choosing to path through this node is a lower distance than what
|
||||
// the neighbour currently has set, update the neighbour to use this node
|
||||
// as the path source, and set its distance scores as necessary
|
||||
if (fPossiblyLowerGoal < nodeNeighbour->fLocalGoal) |
||||
{ |
||||
nodeNeighbour->parent = nodeCurrent; |
||||
nodeNeighbour->fLocalGoal = fPossiblyLowerGoal; |
||||
|
||||
// The best path length to the neighbour being tested has changed, so
|
||||
// update the neighbour's score. The heuristic is used to globally bias
|
||||
// the path algorithm, so it knows if its getting better or worse. At some
|
||||
// point the algo will realise this path is worse and abandon it, and then go
|
||||
// and search along the next best path.
|
||||
nodeNeighbour->fGlobalGoal = nodeNeighbour->fLocalGoal + heuristic(nodeNeighbour, nodeEnd); |
||||
} |
||||
}
|
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
virtual bool OnUserUpdate(float fElapsedTime) |
||||
{ |
||||
int nNodeSize = 9; |
||||
int nNodeBorder = 2; |
||||
|
||||
// Use integer division to nicely get cursor position in node space
|
||||
int nSelectedNodeX = m_mousePosX / nNodeSize; |
||||
int nSelectedNodeY = m_mousePosY / nNodeSize; |
||||
|
||||
if (m_mouse[0].bReleased) // Use mouse to draw maze, shift and ctrl to place start and end
|
||||
{ |
||||
if(nSelectedNodeX >=0 && nSelectedNodeX < nMapWidth) |
||||
if (nSelectedNodeY >= 0 && nSelectedNodeY < nMapHeight) |
||||
{ |
||||
if (m_keys[VK_SHIFT].bHeld) |
||||
nodeStart = &nodes[nSelectedNodeY * nMapWidth + nSelectedNodeX]; |
||||
else if (m_keys[VK_CONTROL].bHeld) |
||||
nodeEnd = &nodes[nSelectedNodeY * nMapWidth + nSelectedNodeX]; |
||||
else |
||||
nodes[nSelectedNodeY * nMapWidth + nSelectedNodeX].bObstacle = !nodes[nSelectedNodeY * nMapWidth + nSelectedNodeX].bObstacle; |
||||
|
||||
Solve_AStar(); // Solve in "real-time" gives a nice effect
|
||||
} |
||||
} |
||||
|
||||
// Draw Connections First - lines from this nodes position to its
|
||||
// connected neighbour node positions
|
||||
Fill(0, 0, ScreenWidth(), ScreenHeight(), L' '); |
||||
for (int x = 0; x < nMapWidth; x++) |
||||
for (int y = 0; y < nMapHeight; y++) |
||||
{ |
||||
for (auto n : nodes[y*nMapWidth + x].vecNeighbours) |
||||
{ |
||||
DrawLine(x*nNodeSize + nNodeSize / 2, y*nNodeSize + nNodeSize / 2, |
||||
n->x*nNodeSize + nNodeSize / 2, n->y*nNodeSize + nNodeSize / 2, PIXEL_SOLID, FG_DARK_BLUE); |
||||
} |
||||
} |
||||
|
||||
// Draw Nodes on top
|
||||
for (int x = 0; x < nMapWidth; x++) |
||||
for (int y = 0; y < nMapHeight; y++) |
||||
{ |
||||
|
||||
Fill(x*nNodeSize + nNodeBorder, y*nNodeSize + nNodeBorder,
|
||||
(x + 1)*nNodeSize - nNodeBorder, (y + 1)*nNodeSize - nNodeBorder,
|
||||
PIXEL_HALF, nodes[y * nMapWidth + x].bObstacle ? FG_WHITE : FG_BLUE); |
||||
|
||||
if (nodes[y * nMapWidth + x].bVisited) |
||||
Fill(x*nNodeSize + nNodeBorder, y*nNodeSize + nNodeBorder, (x + 1)*nNodeSize - nNodeBorder, (y + 1)*nNodeSize - nNodeBorder, PIXEL_SOLID, FG_BLUE); |
||||
|
||||
if(&nodes[y * nMapWidth + x] == nodeStart) |
||||
Fill(x*nNodeSize + nNodeBorder, y*nNodeSize + nNodeBorder, (x + 1)*nNodeSize - nNodeBorder, (y + 1)*nNodeSize - nNodeBorder, PIXEL_SOLID, FG_GREEN); |
||||
|
||||
if(&nodes[y * nMapWidth + x] == nodeEnd) |
||||
Fill(x*nNodeSize + nNodeBorder, y*nNodeSize + nNodeBorder, (x + 1)*nNodeSize - nNodeBorder, (y + 1)*nNodeSize - nNodeBorder, PIXEL_SOLID, FG_RED);
|
||||
|
||||
} |
||||
|
||||
|
||||
// Draw Path by starting ath the end, and following the parent node trail
|
||||
// back to the start - the start node will not have a parent path to follow
|
||||
if (nodeEnd != nullptr) |
||||
{ |
||||
sNode *p = nodeEnd; |
||||
while (p->parent != nullptr) |
||||
{ |
||||
DrawLine(p->x*nNodeSize + nNodeSize / 2, p->y*nNodeSize + nNodeSize / 2, |
||||
p->parent->x*nNodeSize + nNodeSize / 2, p->parent->y*nNodeSize + nNodeSize / 2, PIXEL_SOLID, FG_YELLOW); |
||||
|
||||
// Set next node to this node's parent
|
||||
p = p->parent; |
||||
} |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
}; |
||||
|
||||
int main() |
||||
{ |
||||
OneLoneCoder_PathFinding game; |
||||
game.ConstructConsole(160, 160, 6, 6); |
||||
game.Start(); |
||||
return 0; |
||||
} |
Loading…
Reference in new issue