Added PathFinding #2 Video
parent
72b31e83cf
commit
2ebb88a942
@ -0,0 +1,426 @@ |
||||
/*
|
||||
OneLoneCoder.com - Path Finding #2 - Wave Propagation & Potential Fields |
||||
"...never get lost again, so long as you know where you are" - @Javidx9 |
||||
|
||||
|
||||
Background |
||||
~~~~~~~~~~ |
||||
A nice follow up alternative to the A* Algorithm. Wave propagation is less |
||||
applicable to multiple objects with multiple destinations, but fantatsic |
||||
for multiple objects all reaching the same destination. |
||||
|
||||
WARNING! This code is NOT OPTIMAL!! It is however very robust. There |
||||
are many ways to optimise this further. |
||||
|
||||
License (OLC-3) |
||||
~~~~~~~~~~~~~~~ |
||||
|
||||
Copyright 2018 OneLoneCoder.com |
||||
|
||||
Redistribution and use in source and binary forms, with or without |
||||
modification, are permitted provided that the following conditions |
||||
are met: |
||||
|
||||
1. Redistributions or derivations of source code must retain the above |
||||
copyright notice, this list of conditions and the following disclaimer. |
||||
|
||||
2. Redistributions or derivative works in binary form must reproduce |
||||
the above copyright notice. This list of conditions and the following |
||||
disclaimer must be reproduced in the documentation and/or other |
||||
materials provided with the distribution. |
||||
|
||||
3. Neither the name of the copyright holder nor the names of its |
||||
contributors may be used to endorse or promote products derived |
||||
from this software without specific prior written permission. |
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
||||
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
||||
|
||||
Links |
||||
~~~~~ |
||||
YouTube: https://www.youtube.com/javidx9
|
||||
Discord: https://discord.gg/WhwHUMV
|
||||
Twitter: https://www.twitter.com/javidx9
|
||||
Twitch: https://www.twitch.tv/javidx9
|
||||
GitHub: https://www.github.com/onelonecoder
|
||||
Patreon: https://www.patreon/javidx9
|
||||
Homepage: https://www.onelonecoder.com
|
||||
|
||||
Relevant Videos |
||||
~~~~~~~~~~~~~~~ |
||||
Part #1 https://youtu.be/icZj67PTFhc
|
||||
Part #2 https://youtu.be/0ihciMKlcP8
|
||||
|
||||
Author |
||||
~~~~~~ |
||||
David Barr, aka javidx9, ©OneLoneCoder 2018 |
||||
*/ |
||||
#define OLC_PGE_APPLICATION |
||||
#include "olcPixelGameEngine.h" |
||||
|
||||
#include <vector> |
||||
#include <list> |
||||
#include <algorithm> |
||||
#include <utility> |
||||
|
||||
|
||||
// Override base class with your custom functionality
|
||||
class PathFinding_FlowFields : public olc::PixelGameEngine |
||||
{ |
||||
public: |
||||
PathFinding_FlowFields() |
||||
{ |
||||
sAppName = "PathFinding - Flow Fields"; |
||||
} |
||||
|
||||
private: |
||||
int nMapWidth; |
||||
int nMapHeight; |
||||
int nCellSize; |
||||
int nBorderWidth; |
||||
|
||||
bool *bObstacleMap; |
||||
|
||||
int *nFlowFieldZ; |
||||
float *fFlowFieldY; |
||||
float *fFlowFieldX; |
||||
|
||||
int nStartX; |
||||
int nStartY; |
||||
int nEndX; |
||||
int nEndY; |
||||
|
||||
int nWave = 1; |
||||
|
||||
public: |
||||
bool OnUserCreate() override |
||||
{ |
||||
nBorderWidth = 4; |
||||
nCellSize = 32; |
||||
nMapWidth = ScreenWidth() / nCellSize; |
||||
nMapHeight = ScreenHeight() / nCellSize; |
||||
bObstacleMap = new bool[nMapWidth * nMapHeight]{ false }; |
||||
nFlowFieldZ = new int[nMapWidth * nMapHeight]{ 0 }; |
||||
fFlowFieldX = new float[nMapWidth * nMapHeight]{ 0 }; |
||||
fFlowFieldY = new float[nMapWidth * nMapHeight]{ 0 }; |
||||
|
||||
nStartX = 3; |
||||
nStartY = 7; |
||||
nEndX = 12; |
||||
nEndY = 7; |
||||
return true; |
||||
} |
||||
|
||||
bool OnUserUpdate(float fElapsedTime) override |
||||
{ |
||||
// Little convenience lambda 2D -> 1D
|
||||
auto p = [&](int x, int y) { return y * nMapWidth + x; }; |
||||
|
||||
// User Input
|
||||
int nSelectedCellX = GetMouseX() / nCellSize; |
||||
int nSelectedCellY = GetMouseY() / nCellSize; |
||||
|
||||
if (GetMouse(0).bReleased) |
||||
{ |
||||
// Toggle Obstacle if mouse left clicked
|
||||
bObstacleMap[p(nSelectedCellX, nSelectedCellY)] = |
||||
!bObstacleMap[p(nSelectedCellX, nSelectedCellY)]; |
||||
}
|
||||
|
||||
if (GetMouse(1).bReleased) |
||||
{ |
||||
nStartX = nSelectedCellX; |
||||
nStartY = nSelectedCellY; |
||||
} |
||||
|
||||
if (GetKey(olc::Key::Q).bReleased) |
||||
{ |
||||
nWave++; |
||||
} |
||||
|
||||
if (GetKey(olc::Key::A).bReleased) |
||||
{ |
||||
nWave--; |
||||
if (nWave == 0) |
||||
nWave = 1; |
||||
} |
||||
|
||||
|
||||
|
||||
// 1) Prepare flow field, add a boundary, and add obstacles
|
||||
// by setting the flow Field Height (Z) to -1
|
||||
for (int x = 0; x < nMapWidth; x++) |
||||
{ |
||||
for (int y = 0; y < nMapHeight; y++) |
||||
{ |
||||
// Set border or obstacles
|
||||
if (x == 0 || y == 0 || x == (nMapWidth - 1) || y == (nMapHeight - 1) || bObstacleMap[p(x, y)]) |
||||
{ |
||||
nFlowFieldZ[p(x, y)] = -1; |
||||
} |
||||
else |
||||
{ |
||||
nFlowFieldZ[p(x, y)] = 0; |
||||
} |
||||
} |
||||
} |
||||
|
||||
// 2) Propagate a wave (i.e. flood-fill) from target location. Here I use
|
||||
// a tuple, of {x, y, distance} - though you could use a struct or
|
||||
// similar.
|
||||
std::list<std::tuple<int, int, int>> nodes; |
||||
|
||||
// Add the first discovered node - the target location, with a distance of 1
|
||||
nodes.push_back({ nEndX, nEndY, 1 }); |
||||
|
||||
while (!nodes.empty()) |
||||
{ |
||||
// Each iteration through the discovered nodes may create newly discovered
|
||||
// nodes, so I maintain a second list. It's important not to contaminate
|
||||
// the list being iterated through.
|
||||
std::list<std::tuple<int, int, int>> new_nodes; |
||||
|
||||
// Now iterate through each discovered node. If it has neighbouring nodes
|
||||
// that are empty space and undiscovered, add those locations to the
|
||||
// new nodes list
|
||||
for (auto &n : nodes) |
||||
{ |
||||
int x = std::get<0>(n); // Map X-Coordinate
|
||||
int y = std::get<1>(n); // Map Y-Coordinate
|
||||
int d = std::get<2>(n); // Distance From Target Location
|
||||
|
||||
// Set distance count for this node. NOte that when we add nodes we add 1
|
||||
// to this distance. This emulates propagating a wave across the map, where
|
||||
// the front of that wave increments each iteration. In this way, we can
|
||||
// propagate distance information 'away from target location'
|
||||
nFlowFieldZ[p(x, y)] = d; |
||||
|
||||
// Add neigbour nodes if unmarked, i.e their "height" is 0. Any discovered
|
||||
// node or obstacle will be non-zero
|
||||
|
||||
// Check East
|
||||
if ((x + 1) < nMapWidth && nFlowFieldZ[p(x + 1, y)] == 0) |
||||
new_nodes.push_back({ x + 1, y, d + 1 }); |
||||
|
||||
// Check West
|
||||
if ((x - 1) >= 0 && nFlowFieldZ[p(x - 1, y)] == 0) |
||||
new_nodes.push_back({ x - 1, y, d + 1 }); |
||||
|
||||
// Check South
|
||||
if ((y + 1) < nMapHeight && nFlowFieldZ[p(x, y + 1)] == 0) |
||||
new_nodes.push_back({ x, y + 1, d + 1 }); |
||||
|
||||
// Check North
|
||||
if ((y - 1) >= 0 && nFlowFieldZ[p(x, y - 1)] == 0) |
||||
new_nodes.push_back({ x, y - 1, d + 1 }); |
||||
} |
||||
|
||||
// We will now have potentially multiple nodes for a single location. This means our
|
||||
// algorithm will never complete! So we must remove duplicates form our new node list.
|
||||
// Im doing this with some clever code - but it is not performant(!) - it is merely
|
||||
// convenient. I'd suggest doing away with overhead structures like linked lists and sorts
|
||||
// if you are aiming for fastest path finding.
|
||||
|
||||
// Sort the nodes - This will stack up nodes that are similar: A, B, B, B, B, C, D, D, E, F, F
|
||||
new_nodes.sort([&](const std::tuple<int, int, int> &n1, const std::tuple<int, int, int> &n2) |
||||
{ |
||||
// In this instance I dont care how the values are sorted, so long as nodes that
|
||||
// represent the same location are adjacent in the list. I can use the p() lambda
|
||||
// to generate a unique 1D value for a 2D coordinate, so I'll sort by that.
|
||||
return p(std::get<0>(n1), std::get<1>(n1)) < p(std::get<0>(n2), std::get<1>(n2)); |
||||
}); |
||||
|
||||
// Use "unique" function to remove adjacent duplicates : A, B, -, -, -, C, D, -, E, F -
|
||||
// and also erase them : A, B, C, D, E, F
|
||||
new_nodes.unique([&](const std::tuple<int, int, int> &n1, const std::tuple<int, int, int> &n2) |
||||
{ |
||||
return p(std::get<0>(n1), std::get<1>(n1)) == p(std::get<0>(n2), std::get<1>(n2)); |
||||
}); |
||||
|
||||
// We've now processed all the discoverd nodes, so clear the list, and add the newly
|
||||
// discovered nodes for processing on the next iteration
|
||||
nodes.clear(); |
||||
nodes.insert(nodes.begin(), new_nodes.begin(), new_nodes.end()); |
||||
|
||||
// When there are no more newly discovered nodes, we have "flood filled" the entire
|
||||
// map. The propagation phase of the algorithm is complete
|
||||
} |
||||
|
||||
|
||||
// 3) Create Path. Starting a start location, create a path of nodes until you reach target
|
||||
// location. At each node find the neighbour with the lowest "distance" score.
|
||||
std::list<std::pair<int, int>> path; |
||||
path.push_back({ nStartX, nStartY }); |
||||
int nLocX = nStartX; |
||||
int nLocY = nStartY; |
||||
bool bNoPath = false; |
||||
|
||||
while (!(nLocX == nEndX && nLocY == nEndY) && !bNoPath) |
||||
{ |
||||
std::list<std::tuple<int, int, int>> listNeighbours; |
||||
|
||||
// 4-Way Connectivity
|
||||
if ((nLocY - 1) >= 0 && nFlowFieldZ[p(nLocX, nLocY - 1)] > 0) |
||||
listNeighbours.push_back({ nLocX, nLocY - 1, nFlowFieldZ[p(nLocX, nLocY - 1)] }); |
||||
|
||||
if ((nLocX + 1) < nMapWidth && nFlowFieldZ[p(nLocX + 1, nLocY)] > 0) |
||||
listNeighbours.push_back({ nLocX + 1, nLocY, nFlowFieldZ[p(nLocX + 1, nLocY)] }); |
||||
|
||||
if ((nLocY + 1) < nMapHeight && nFlowFieldZ[p(nLocX, nLocY + 1)] > 0) |
||||
listNeighbours.push_back({ nLocX, nLocY + 1, nFlowFieldZ[p(nLocX, nLocY + 1)] }); |
||||
|
||||
if ((nLocX - 1) >= 0 && nFlowFieldZ[p(nLocX - 1, nLocY)] > 0) |
||||
listNeighbours.push_back({ nLocX - 1, nLocY, nFlowFieldZ[p(nLocX - 1, nLocY)] }); |
||||
|
||||
// 8-Way Connectivity
|
||||
if ((nLocY - 1) >= 0 && (nLocX - 1) >= 0 && nFlowFieldZ[p(nLocX - 1, nLocY - 1)] > 0) |
||||
listNeighbours.push_back({ nLocX - 1, nLocY - 1, nFlowFieldZ[p(nLocX - 1, nLocY - 1)] }); |
||||
|
||||
if ((nLocY - 1) >= 0 && (nLocX + 1) < nMapWidth && nFlowFieldZ[p(nLocX + 1, nLocY - 1)] > 0) |
||||
listNeighbours.push_back({ nLocX + 1, nLocY - 1, nFlowFieldZ[p(nLocX + 1, nLocY - 1)] }); |
||||
|
||||
if ((nLocY + 1) < nMapHeight && (nLocX - 1) >= 0 && nFlowFieldZ[p(nLocX - 1, nLocY + 1)] > 0) |
||||
listNeighbours.push_back({ nLocX - 1, nLocY + 1, nFlowFieldZ[p(nLocX - 1, nLocY + 1)] }); |
||||
|
||||
if ((nLocY + 1) < nMapHeight && (nLocX + 1) < nMapWidth && nFlowFieldZ[p(nLocX + 1, nLocY + 1)] > 0) |
||||
listNeighbours.push_back({ nLocX + 1, nLocY + 1, nFlowFieldZ[p(nLocX + 1, nLocY + 1)] }); |
||||
|
||||
// Sprt neigbours based on height, so lowest neighbour is at front
|
||||
// of list
|
||||
listNeighbours.sort([&](const std::tuple<int, int, int> &n1, const std::tuple<int, int, int> &n2) |
||||
{ |
||||
return std::get<2>(n1) < std::get<2>(n2); // Compare distances
|
||||
}); |
||||
|
||||
if (listNeighbours.empty()) // Neighbour is invalid or no possible path
|
||||
bNoPath = true; |
||||
else |
||||
{ |
||||
nLocX = std::get<0>(listNeighbours.front()); |
||||
nLocY = std::get<1>(listNeighbours.front()); |
||||
path.push_back({ nLocX, nLocY }); |
||||
} |
||||
} |
||||
|
||||
|
||||
// 4) Create Flow "Field"
|
||||
for (int x = 1; x < nMapWidth - 1; x++) |
||||
{ |
||||
for (int y = 1; y < nMapHeight - 1; y++) |
||||
{ |
||||
float vx = 0.0f; |
||||
float vy = 0.0f; |
||||
|
||||
vy -= (float)((nFlowFieldZ[p(x, y + 1)] <= 0 ? nFlowFieldZ[p(x, y)] : nFlowFieldZ[p(x, y + 1)]) - nFlowFieldZ[p(x, y)]); |
||||
vx -= (float)((nFlowFieldZ[p(x + 1, y)] <= 0 ? nFlowFieldZ[p(x, y)] : nFlowFieldZ[p(x + 1, y)]) - nFlowFieldZ[p(x, y)]); |
||||
vy += (float)((nFlowFieldZ[p(x, y - 1)] <= 0 ? nFlowFieldZ[p(x, y)] : nFlowFieldZ[p(x, y - 1)]) - nFlowFieldZ[p(x, y)]); |
||||
vx += (float)((nFlowFieldZ[p(x - 1, y)] <= 0 ? nFlowFieldZ[p(x, y)] : nFlowFieldZ[p(x - 1, y)]) - nFlowFieldZ[p(x, y)]); |
||||
|
||||
float r = 1.0f / sqrtf(vx*vx + vy * vy); |
||||
fFlowFieldX[p(x, y)] = vx * r; |
||||
fFlowFieldY[p(x, y)] = vy * r; |
||||
} |
||||
} |
||||
|
||||
|
||||
|
||||
|
||||
// Draw Map
|
||||
Clear(olc::BLACK); |
||||
|
||||
for (int x = 0; x < nMapWidth; x++) |
||||
{ |
||||
for (int y = 0; y < nMapHeight; y++) |
||||
{ |
||||
olc::Pixel colour = olc::BLUE; |
||||
|
||||
if (bObstacleMap[p(x, y)]) |
||||
colour = olc::GREY; |
||||
|
||||
if (nWave == nFlowFieldZ[p(x, y)]) |
||||
colour = olc::DARK_CYAN; |
||||
|
||||
if (x == nStartX && y == nStartY) |
||||
colour = olc::GREEN; |
||||
|
||||
if (x == nEndX && y == nEndY) |
||||
colour = olc::RED; |
||||
|
||||
// Draw Base
|
||||
FillRect(x * nCellSize, y * nCellSize, nCellSize - nBorderWidth, nCellSize - nBorderWidth, colour); |
||||
|
||||
// Draw "potential" or "distance" or "height" :D
|
||||
//DrawString(x * nCellSize, y * nCellSize, std::to_string(nFlowFieldZ[p(x, y)]), olc::WHITE);
|
||||
|
||||
if (nFlowFieldZ[p(x, y)] > 0) |
||||
{ |
||||
float ax[4], ay[4]; |
||||
float fAngle = atan2f(fFlowFieldY[p(x, y)], fFlowFieldX[p(x, y)]); |
||||
float fRadius = (float)(nCellSize - nBorderWidth) / 2.0f; |
||||
int fOffsetX = x * nCellSize + ((nCellSize - nBorderWidth) / 2); |
||||
int fOffsetY = y * nCellSize + ((nCellSize - nBorderWidth) / 2); |
||||
ax[0] = cosf(fAngle) * fRadius + fOffsetX; |
||||
ay[0] = sinf(fAngle) * fRadius + fOffsetY; |
||||
ax[1] = cosf(fAngle) * -fRadius + fOffsetX; |
||||
ay[1] = sinf(fAngle) * -fRadius + fOffsetY; |
||||
ax[2] = cosf(fAngle + 0.1f) * fRadius * 0.7f + fOffsetX; |
||||
ay[2] = sinf(fAngle + 0.1f) * fRadius * 0.7f + fOffsetY; |
||||
ax[3] = cosf(fAngle - 0.1f) * fRadius * 0.7f + fOffsetX; |
||||
ay[3] = sinf(fAngle - 0.1f) * fRadius * 0.7f + fOffsetY; |
||||
|
||||
DrawLine(ax[0], ay[0], ax[1], ay[1], olc::CYAN); |
||||
DrawLine(ax[0], ay[0], ax[2], ay[2], olc::CYAN); |
||||
DrawLine(ax[0], ay[0], ax[3], ay[3], olc::CYAN); |
||||
|
||||
} |
||||
} |
||||
} |
||||
|
||||
|
||||
bool bFirstPoint = true; |
||||
int ox, oy; |
||||
for (auto &a : path) |
||||
{ |
||||
if (bFirstPoint) |
||||
{ |
||||
ox = a.first; |
||||
oy = a.second; |
||||
bFirstPoint = false; |
||||
} |
||||
else |
||||
{ |
||||
DrawLine( |
||||
ox * nCellSize + ((nCellSize - nBorderWidth) / 2), |
||||
oy * nCellSize + ((nCellSize - nBorderWidth) / 2), |
||||
a.first * nCellSize + ((nCellSize - nBorderWidth) / 2), |
||||
a.second * nCellSize + ((nCellSize - nBorderWidth) / 2), olc::YELLOW); |
||||
|
||||
ox = a.first; |
||||
oy = a.second; |
||||
|
||||
FillCircle(ox * nCellSize + ((nCellSize - nBorderWidth) / 2), oy * nCellSize + ((nCellSize - nBorderWidth) / 2), 10, olc::YELLOW); |
||||
} |
||||
} |
||||
|
||||
|
||||
return true; |
||||
} |
||||
}; |
||||
|
||||
|
||||
int main() |
||||
{ |
||||
PathFinding_FlowFields demo; |
||||
if (demo.Construct(512, 480, 2, 2)) |
||||
demo.Start(); |
||||
return 0; |
||||
} |
Loading…
Reference in new issue