Move pathfinders to dedicated files

This commit is contained in:
Jan Mrna 2025-09-28 09:39:40 +02:00
parent 7eee6a5c54
commit c42a6b647e
20 changed files with 430 additions and 377 deletions

View File

@ -2,7 +2,7 @@
# Generated by Kimi K2 # Generated by Kimi K2
#---------- configurable ---------- #---------- configurable ----------
CXX := g++ CXX := g++
CXXFLAGS := -std=c++23 -Wall -Wextra -Wpedantic -ggdb3 CXXFLAGS := -Isrc -std=c++23 -Wall -Wextra -Wpedantic -ggdb3
LDFLAGS := LDFLAGS :=
LDLIBS := -lSDL3 -lSDL3_image -lGLEW -lGL LDLIBS := -lSDL3 -lSDL3_image -lGLEW -lGL
@ -11,7 +11,7 @@ BUILD_DIR:= build
TARGET := pathfinding TARGET := pathfinding
#---------------------------------- #----------------------------------
SOURCES := $(wildcard $(SRC_DIR)/*.cpp) SOURCES := $(shell find $(SRC_DIR) -name '*.cpp')
OBJECTS := $(SOURCES:$(SRC_DIR)/%.cpp=$(BUILD_DIR)/%.o) OBJECTS := $(SOURCES:$(SRC_DIR)/%.cpp=$(BUILD_DIR)/%.o)
#---------------------------------- #----------------------------------
@ -25,6 +25,7 @@ $(TARGET): $(OBJECTS)
# compile step # compile step
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.cpp | $(BUILD_DIR) $(BUILD_DIR)/%.o: $(SRC_DIR)/%.cpp | $(BUILD_DIR)
@mkdir -p $(dir $@)
$(CXX) $(CXXFLAGS) -c -o $@ $< $(CXX) $(CXXFLAGS) -c -o $@ $<
$(BUILD_DIR): $(BUILD_DIR):

View File

@ -7,7 +7,7 @@
#include "window.hpp" #include "window.hpp"
#include "user_input.hpp" #include "user_input.hpp"
#include "log.hpp" #include "log.hpp"
#include "pathfinder.hpp" #include "pathfinder/base.hpp"
#include "math.hpp" #include "math.hpp"
void GameLoop::Run() { void GameLoop::Run() {

View File

@ -1,273 +0,0 @@
#include <memory>
#include <cassert>
#include <queue>
#include "pathfinder.hpp"
#include "log.hpp"
namespace pathfinder {
PathFinderBase::PathFinderBase(const Map* map) : m_Map(map) {}
Path LinearPathFinder::CalculatePath(WorldPos start, WorldPos end)
{
auto path = Path{end};
return path;
}
Path BFS::CalculatePath(WorldPos start_world, WorldPos end_world) {
if (m_Map == nullptr) return {};
const TilePos start = m_Map->WorldToTile(start_world);
const TilePos end = m_Map->WorldToTile(end_world);
if (!m_Map->IsTilePosValid(start) || !m_Map->IsTilePosValid(end))
return {};
if (start == end) {
return {};
}
// clear previous run
m_CameFrom.clear();
m_Distance.clear();
std::queue<TilePos> frontier;
frontier.push(start);
m_CameFrom[start] = start;
m_Distance[start] = 0.0f;
// ---------------- build flow-field ----------------
bool early_exit = false;
while (!frontier.empty() && !early_exit) {
TilePos current = frontier.front();
frontier.pop();
for (TilePos next : m_Map->GetNeighbors(current)) {
if (m_CameFrom.find(next) == m_CameFrom.end()) { // not visited
frontier.push(next);
m_Distance[next] = m_Distance[current] + 1.0f;
m_CameFrom[next] = current;
if (next == end) { // early exit
early_exit = true;
break;
}
}
}
}
// --------------- reconstruct path -----------------
if (m_CameFrom.find(end) == m_CameFrom.end())
return {}; // end not reached
Path path;
TilePos cur = end;
path.push_back(m_Map->TileToWorld(cur));
while (cur != start) {
cur = m_CameFrom[cur];
path.push_back(m_Map->TileToWorld(cur));
}
std::reverse(path.begin(), path.end());
return path;
}
//
//Path Dijkstra::CalculatePath(WorldPos start_world, WorldPos end_world) {
// if (m_Map == nullptr) return {};
//
// const TilePos start = m_Map->WorldToTile(start_world);
// const TilePos end = m_Map->WorldToTile(end_world);
//
// if (!m_Map->IsTilePosValid(start) || !m_Map->IsTilePosValid(end))
// return {};
// if (start == end) {
// return {};
// }
// // clear previous run
// m_CameFrom.clear();
// m_Distance.clear();
//
// std::queue<TilePos> frontier;
// frontier.push(start);
// m_CameFrom[start] = start;
// m_Distance[start] = 0.0f;
//
// // ---------------- build flow-field ----------------
// bool early_exit = false;
// while (!frontier.empty() && !early_exit) {
// TilePos current = frontier.front();
// frontier.pop();
//
// for (TilePos next : m_Map->GetNeighbors(current)) {
// if (m_CameFrom.find(next) == m_CameFrom.end()) { // not visited
// frontier.push(next);
// m_Distance[next] = m_Distance[current] + 1.0f;
// m_CameFrom[next] = current;
//
// if (next == end) { // early exit
// early_exit = true;
// break;
// }
// }
// }
// }
//
// // --------------- reconstruct path -----------------
// if (m_CameFrom.find(end) == m_CameFrom.end())
// return {}; // end not reached
//
// Path path;
// TilePos cur = end;
// path.push_back(m_Map->TileToWorld(cur));
// while (cur != start) {
// cur = m_CameFrom[cur];
// path.push_back(m_Map->TileToWorld(cur));
// }
// std::reverse(path.begin(), path.end());
// return path;
//}
struct QueueEntry
{
float cost;
TilePos tile;
// min-heap -> smallest cost on top
bool operator>(const QueueEntry& o) const noexcept { return cost > o.cost; }
};
Path Dijkstra::CalculatePath(WorldPos start_world, WorldPos end_world)
{
if (!m_Map) return {};
const TilePos start = m_Map->WorldToTile(start_world);
const TilePos end = m_Map->WorldToTile(end_world);
if (!m_Map->IsTilePosValid(start) || !m_Map->IsTilePosValid(end))
return {};
if (start == end) return {};
// clear previous run
m_CameFrom.clear();
m_Cost.clear();
std::priority_queue<QueueEntry, std::vector<QueueEntry>, std::greater<>> frontier;
frontier.push({0.0f, start});
m_CameFrom[start] = start; // sentinel
m_Cost[start] = 0.0f;
while (!frontier.empty())
{
const QueueEntry current = frontier.top();
frontier.pop();
if (current.tile == end) // early exit
break;
for (TilePos next : m_Map->GetNeighbors(current.tile))
{
// cost of moving to neighbour (uniform 1.0 matches original BFS)
const float newCost = m_Cost[current.tile] + m_Map->GetCost(next);
if (!m_Cost.count(next) || newCost < m_Cost[next])
{
m_Cost[next] = newCost;
m_CameFrom[next] = current.tile;
frontier.push({newCost, next});
}
}
}
// reconstruct path
if (!m_CameFrom.count(end))
return {}; // goal never reached
Path path;
TilePos cur = end;
path.push_back(m_Map->TileToWorld(cur));
while (cur != start)
{
cur = m_CameFrom[cur];
path.push_back(m_Map->TileToWorld(cur));
}
std::reverse(path.begin(), path.end());
return path;
}
float GBFS::Heuristic(const TilePos& a, const TilePos& b)
{
return static_cast<float>(std::abs(a.x- b.x) + std::abs(a.y - b.y));
}
Path GBFS::CalculatePath(WorldPos start_world, WorldPos end_world)
{
if (!m_Map) return {};
const TilePos start = m_Map->WorldToTile(start_world);
const TilePos end = m_Map->WorldToTile(end_world);
if (!m_Map->IsTilePosValid(start) || !m_Map->IsTilePosValid(end))
return {};
if (start == end) return {};
m_CameFrom.clear();
std::priority_queue<QueueEntry, std::vector<QueueEntry>, std::greater<>> frontier;
frontier.push({Heuristic(start, end), start});
m_CameFrom[start] = start; // sentinel
while (!frontier.empty())
{
const QueueEntry current = frontier.top();
frontier.pop();
if (current.tile == end) // early exit
break;
for (TilePos next : m_Map->GetNeighbors(current.tile))
{
if (!m_CameFrom.count(next)) // not visited
{
m_CameFrom[next] = current.tile;
frontier.push({Heuristic(end, next), next});
}
}
}
// reconstruct path
if (!m_CameFrom.count(end))
return {}; // goal never reached
Path path;
TilePos cur = end;
path.push_back(m_Map->TileToWorld(cur));
while (cur != start)
{
cur = m_CameFrom[cur];
path.push_back(m_Map->TileToWorld(cur));
}
std::reverse(path.begin(), path.end());
return path;
}
std::unique_ptr<PathFinderBase> create(PathFinderType type, const Map* map) {
switch (type) {
case PathFinderType::LINEAR:
return std::move(std::make_unique<LinearPathFinder>(map));
case PathFinderType::BFS:
return std::move(std::make_unique<BFS>(map));
case PathFinderType::DIJKSTRA:
return std::move(std::make_unique<Dijkstra>(map));
case PathFinderType::GBFS:
return std::move(std::make_unique<GBFS>(map));
case PathFinderType::COUNT:
LOG_WARNING("Incorrect pathfinder type");
return nullptr;
};
return nullptr;
}
} // pathfinder namespace

View File

@ -1,97 +0,0 @@
#pragma once
#include <vector>
#include <memory>
#include <unordered_map>
#include "math.hpp"
#include "map.hpp"
namespace pathfinder {
using Path = std::vector<WorldPos>;
enum class PathFinderType {
LINEAR = 1,
BFS,
DIJKSTRA,
GBFS,
COUNT,
};
class PathFinderBase {
public:
PathFinderBase(const Map* m);
~PathFinderBase() = default;
PathFinderBase(const PathFinderBase&) = delete;
PathFinderBase(PathFinderBase&&) = delete;
PathFinderBase& operator=(const PathFinderBase&) = delete;
PathFinderBase& operator=(PathFinderBase&&) = delete;
virtual const std::string_view& GetName() const = 0;
virtual Path CalculatePath(WorldPos start, WorldPos end) = 0;
protected:
const Map* m_Map;
};
class LinearPathFinder : public PathFinderBase {
public:
LinearPathFinder(const Map* m): PathFinderBase(m) {}
Path CalculatePath(WorldPos start, WorldPos end) override;
const std::string_view& GetName() const override { return m_Name; }
private:
const std::string_view m_Name = "Linear Path";
};
class BFS: public PathFinderBase {
public:
BFS(const Map* m): PathFinderBase(m) {}
Path CalculatePath(WorldPos start, WorldPos end) override;
const std::string_view& GetName() const override { return m_Name; }
private:
const std::string_view m_Name = "Breadth First Search";
std::unordered_map<TilePos, double, TilePosHash> m_Distance;
std::unordered_map<TilePos, TilePos, TilePosHash> m_CameFrom;
};
class Dijkstra: public PathFinderBase {
public:
Dijkstra(const Map* m): PathFinderBase(m) {}
Path CalculatePath(WorldPos start, WorldPos end) override;
const std::string_view& GetName() const override { return m_Name; }
private:
const std::string_view m_Name = "Dijkstra's Algorithm";
std::unordered_map<TilePos, double, TilePosHash> m_Cost;
std::unordered_map<TilePos, TilePos, TilePosHash> m_CameFrom;
};
class GBFS: public PathFinderBase {
public:
GBFS(const Map* m): PathFinderBase(m) {}
Path CalculatePath(WorldPos start, WorldPos end) override;
const std::string_view& GetName() const override { return m_Name; }
private:
static float Heuristic(const TilePos& a, const TilePos& b);
const std::string_view m_Name = "Greedy Best First Search";
std::unordered_map<TilePos, TilePos, TilePosHash> m_CameFrom;
};
std::unique_ptr<PathFinderBase> create(PathFinderType type, const Map* map);
} // pathfinder namespace

View File

View File

View File

@ -0,0 +1,22 @@
#include <memory>
#include <cassert>
#include <queue>
#include "pathfinder/base.hpp"
#include "log.hpp"
#include "math.hpp"
namespace pathfinder {
PathFinderBase::PathFinderBase(const Map* map) : m_Map(map) {}
// LinearPathFinder also lives here, since it is too small to get it's
// own implementation file
Path LinearPathFinder::CalculatePath(WorldPos start, WorldPos end)
{
auto path = Path{end};
return path;
}
} // pathfinder namespace

View File

@ -0,0 +1,52 @@
#pragma once
#include <vector>
#include <memory>
#include <unordered_map>
#include "math.hpp"
#include "map.hpp"
namespace pathfinder {
using Path = std::vector<WorldPos>;
enum class PathFinderType {
LINEAR = 1,
BFS,
DIJKSTRA,
GBFS,
COUNT,
};
class PathFinderBase {
public:
PathFinderBase(const Map* m);
~PathFinderBase() = default;
PathFinderBase(const PathFinderBase&) = delete;
PathFinderBase(PathFinderBase&&) = delete;
PathFinderBase& operator=(const PathFinderBase&) = delete;
PathFinderBase& operator=(PathFinderBase&&) = delete;
virtual const std::string_view& GetName() const = 0;
virtual Path CalculatePath(WorldPos start, WorldPos end) = 0;
protected:
const Map* m_Map;
};
class LinearPathFinder : public PathFinderBase {
public:
LinearPathFinder(const Map* m): PathFinderBase(m) {}
Path CalculatePath(WorldPos start, WorldPos end) override;
const std::string_view& GetName() const override { return m_Name; }
private:
const std::string_view m_Name = "Linear Path";
};
} // pathfinder namespace

View File

@ -0,0 +1,66 @@
#include <queue>
#include "bfs.hpp"
#include "base.hpp"
#include "map.hpp"
#include "math.hpp"
namespace pathfinder {
Path BFS::CalculatePath(WorldPos start_world, WorldPos end_world) {
if (m_Map == nullptr) return {};
const TilePos start = m_Map->WorldToTile(start_world);
const TilePos end = m_Map->WorldToTile(end_world);
if (!m_Map->IsTilePosValid(start) || !m_Map->IsTilePosValid(end))
return {};
if (start == end) {
return {};
}
// clear previous run
m_CameFrom.clear();
m_Distance.clear();
std::queue<TilePos> frontier;
frontier.push(start);
m_CameFrom[start] = start;
m_Distance[start] = 0.0f;
// ---------------- build flow-field ----------------
bool early_exit = false;
while (!frontier.empty() && !early_exit) {
TilePos current = frontier.front();
frontier.pop();
for (TilePos next : m_Map->GetNeighbors(current)) {
if (m_CameFrom.find(next) == m_CameFrom.end()) { // not visited
frontier.push(next);
m_Distance[next] = m_Distance[current] + 1.0f;
m_CameFrom[next] = current;
if (next == end) { // early exit
early_exit = true;
break;
}
}
}
}
// --------------- reconstruct path -----------------
if (m_CameFrom.find(end) == m_CameFrom.end())
return {}; // end not reached
Path path;
TilePos cur = end;
path.push_back(m_Map->TileToWorld(cur));
while (cur != start) {
cur = m_CameFrom[cur];
path.push_back(m_Map->TileToWorld(cur));
}
std::reverse(path.begin(), path.end());
return path;
}
} // pathfinder namespace

View File

@ -0,0 +1,25 @@
#pragma once
#include <string_view>
#include <unordered_map>
#include "base.hpp"
#include "math.hpp"
namespace pathfinder {
class BFS: public PathFinderBase {
public:
BFS(const Map* m): PathFinderBase(m) {}
Path CalculatePath(WorldPos start, WorldPos end) override;
const std::string_view& GetName() const override { return m_Name; }
private:
const std::string_view m_Name = "Breadth First Search";
std::unordered_map<TilePos, double, TilePosHash> m_Distance;
std::unordered_map<TilePos, TilePos, TilePosHash> m_CameFrom;
};
}

View File

@ -0,0 +1,73 @@
#include <queue>
#include "dijkstra.hpp"
#include "base.hpp"
#include "utils.hpp"
#include "math.hpp"
#include "map.hpp"
namespace pathfinder {
Path Dijkstra::CalculatePath(WorldPos start_world, WorldPos end_world)
{
using QueueEntry = utils::QueueEntry;
if (!m_Map) return {};
const TilePos start = m_Map->WorldToTile(start_world);
const TilePos end = m_Map->WorldToTile(end_world);
if (!m_Map->IsTilePosValid(start) || !m_Map->IsTilePosValid(end))
return {};
if (start == end) return {};
// clear previous run
m_CameFrom.clear();
m_Cost.clear();
std::priority_queue<QueueEntry, std::vector<QueueEntry>, std::greater<>> frontier;
frontier.push({0.0f, start});
m_CameFrom[start] = start; // sentinel
m_Cost[start] = 0.0f;
while (!frontier.empty())
{
const QueueEntry current = frontier.top();
frontier.pop();
if (current.tile == end) // early exit
break;
for (TilePos next : m_Map->GetNeighbors(current.tile))
{
// cost of moving to neighbour (uniform 1.0 matches original BFS)
const float newCost = m_Cost[current.tile] + m_Map->GetCost(next);
if (!m_Cost.count(next) || newCost < m_Cost[next])
{
m_Cost[next] = newCost;
m_CameFrom[next] = current.tile;
frontier.push({newCost, next});
}
}
}
// reconstruct path
if (!m_CameFrom.count(end))
return {}; // goal never reached
Path path;
TilePos cur = end;
path.push_back(m_Map->TileToWorld(cur));
while (cur != start)
{
cur = m_CameFrom[cur];
path.push_back(m_Map->TileToWorld(cur));
}
std::reverse(path.begin(), path.end());
return path;
}
} // pathfinder namespace

View File

@ -0,0 +1,26 @@
#pragma once
#include <string_view>
#include <unordered_map>
#include "base.hpp"
#include "map.hpp"
#include "math.hpp"
namespace pathfinder {
class Dijkstra: public PathFinderBase {
public:
Dijkstra(const Map* m): PathFinderBase(m) {}
Path CalculatePath(WorldPos start, WorldPos end) override;
const std::string_view& GetName() const override { return m_Name; }
private:
const std::string_view m_Name = "Dijkstra's Algorithm";
std::unordered_map<TilePos, double, TilePosHash> m_Cost;
std::unordered_map<TilePos, TilePos, TilePosHash> m_CameFrom;
};
} // pathfinder namespace

View File

@ -0,0 +1,71 @@
#include <queue>
#include "gbfs.hpp"
#include "base.hpp"
#include "math.hpp"
#include "map.hpp"
#include "pathfinder/utils.hpp"
namespace pathfinder {
float GBFS::Heuristic(const TilePos& a, const TilePos& b)
{
return static_cast<float>(std::abs(a.x- b.x) + std::abs(a.y - b.y));
}
Path GBFS::CalculatePath(WorldPos start_world, WorldPos end_world)
{
using QueueEntry = pathfinder::utils::QueueEntry;
if (!m_Map) return {};
const TilePos start = m_Map->WorldToTile(start_world);
const TilePos end = m_Map->WorldToTile(end_world);
if (!m_Map->IsTilePosValid(start) || !m_Map->IsTilePosValid(end))
return {};
if (start == end) return {};
m_CameFrom.clear();
std::priority_queue<QueueEntry, std::vector<QueueEntry>, std::greater<>> frontier;
frontier.push({Heuristic(start, end), start});
m_CameFrom[start] = start; // sentinel
while (!frontier.empty())
{
const QueueEntry current = frontier.top();
frontier.pop();
if (current.tile == end) // early exit
break;
for (TilePos next : m_Map->GetNeighbors(current.tile))
{
if (!m_CameFrom.count(next)) // not visited
{
m_CameFrom[next] = current.tile;
frontier.push({Heuristic(end, next), next});
}
}
}
// reconstruct path
if (!m_CameFrom.count(end))
return {}; // goal never reached
Path path;
TilePos cur = end;
path.push_back(m_Map->TileToWorld(cur));
while (cur != start)
{
cur = m_CameFrom[cur];
path.push_back(m_Map->TileToWorld(cur));
}
std::reverse(path.begin(), path.end());
return path;
}
} // pathfinder namespace

View File

@ -0,0 +1,26 @@
#pragma once
#include <string_view>
#include <unordered_map>
#include "base.hpp"
#include "map.hpp"
#include "math.hpp"
namespace pathfinder {
class GBFS: public PathFinderBase {
public:
GBFS(const Map* m): PathFinderBase(m) {}
Path CalculatePath(WorldPos start, WorldPos end) override;
const std::string_view& GetName() const override { return m_Name; }
private:
static float Heuristic(const TilePos& a, const TilePos& b);
const std::string_view m_Name = "Greedy Best First Search";
std::unordered_map<TilePos, TilePos, TilePosHash> m_CameFrom;
};
} // pathfinder namespace

View File

View File

View File

@ -0,0 +1,35 @@
#include <memory>
#include "utils.hpp"
#include "base.hpp"
#include "map.hpp"
#include "math.hpp"
#include "log.hpp"
#include "pathfinder/bfs.hpp"
#include "pathfinder/dijkstra.hpp"
#include "pathfinder/gbfs.hpp"
namespace pathfinder {
namespace utils {
std::unique_ptr<PathFinderBase> create(PathFinderType type, const Map* map) {
using namespace pathfinder;
switch (type) {
case PathFinderType::LINEAR:
return std::move(std::make_unique<LinearPathFinder>(map));
case PathFinderType::BFS:
return std::move(std::make_unique<BFS>(map));
case PathFinderType::DIJKSTRA:
return std::move(std::make_unique<Dijkstra>(map));
case PathFinderType::GBFS:
return std::move(std::make_unique<GBFS>(map));
case PathFinderType::COUNT:
LOG_WARNING("Incorrect pathfinder type");
return nullptr;
};
return nullptr;
}
} // utils namespace
} // pathfinding namespace

View File

@ -0,0 +1,25 @@
#pragma once
#include <memory>
#include "pathfinder/base.hpp"
#include "map.hpp"
#include "math.hpp"
namespace pathfinder {
namespace utils {
struct QueueEntry
{
float cost;
TilePos tile;
// min-heap -> smallest cost on top
bool operator>(const QueueEntry& o) const noexcept { return cost > o.cost; }
};
std::unique_ptr<pathfinder::PathFinderBase> create(pathfinder::PathFinderType type, const Map* map);
} // utils namespace
} // pathfinding namespace

View File

@ -9,14 +9,15 @@
#include "log.hpp" #include "log.hpp"
#include "map.hpp" #include "map.hpp"
#include "user_input.hpp" #include "user_input.hpp"
#include "pathfinder.hpp" #include "pathfinder/base.hpp"
#include "pathfinder/utils.hpp"
PathFindingDemo::PathFindingDemo(int width, int height) : PathFindingDemo::PathFindingDemo(int width, int height) :
m_Map(width, height) m_Map(width, height)
{ {
LOG_DEBUG("."); LOG_DEBUG(".");
// set default pathfinder method // set default pathfinder method
m_PathFinder = pathfinder::create(pathfinder::PathFinderType::LINEAR, (const Map*)&m_Map); m_PathFinder = pathfinder::utils::create(pathfinder::PathFinderType::LINEAR, (const Map*)&m_Map);
} }
PathFindingDemo::~PathFindingDemo() { LOG_DEBUG("."); } PathFindingDemo::~PathFindingDemo() { LOG_DEBUG("."); }
@ -88,7 +89,7 @@ void PathFindingDemo::HandleActions(const std::vector<UserAction> &actions) {
} else if (action.type == UserAction::Type::SELECT_PATHFINDER) { } else if (action.type == UserAction::Type::SELECT_PATHFINDER) {
using namespace pathfinder; using namespace pathfinder;
PathFinderType type = static_cast<PathFinderType>(action.Argument.number); PathFinderType type = static_cast<PathFinderType>(action.Argument.number);
m_PathFinder = pathfinder::create(type, (const Map*)&m_Map); m_PathFinder = pathfinder::utils::create(type, (const Map*)&m_Map);
LOG_INFO("Switched to path finding method: ", m_PathFinder->GetName()); LOG_INFO("Switched to path finding method: ", m_PathFinder->GetName());
} }
}; };

View File

@ -9,7 +9,7 @@
#include "log.hpp" #include "log.hpp"
#include "map.hpp" #include "map.hpp"
#include "user_input.hpp" #include "user_input.hpp"
#include "pathfinder.hpp" #include "pathfinder/base.hpp"
class PathFindingDemo { class PathFindingDemo {
public: public: