I wrote a little A* implementation by my own and would like to have some feedback ;) The code is in OOP way, thus we can use an other pathfinding algorithm easily. The "Base" is just the interface class, thus is should not matter here.
Before we look at the code, let me say one or two senteces about it behaviour and motivations.
I like to write reusable code. Thus I looked for a possibility to abstract the behaviour of the algorithm from its underlying data model. Because of this I decided to implement a connection object for these two tasks. This is the "AreaVisitor". If you want to use this header, you have to subclass the "AreaVisitor" class and adapt it to your own data model. It doesn´t matter if you have a tiled 2d array or just some abstract nodes. You decide the important informations, the algorithm just use this in its usual way. I know this is not as special as it sounds, but I think its useful to explain it first ;)
#pragma once
#include <memory>
#include <assert.h>
#include "Base.hpp"
namespace sfw {
namespace pathfinder {
template <class Identifier>
class AStar : public Base<Identifier>
{
public:
template <class Identifier>
class Node_ : sl::NonCopyable
{
private:
const Node_* m_Parent = nullptr;
Identifier m_ID;
int m_Heuristic = 0;
int m_Cost = 0;
public:
Node_(const Identifier& _id, int _cost, int _heuristic, const Node_* _parent) :
m_ID(_id),
m_Cost(std::move(_cost)),
m_Heuristic(std::move(_heuristic)),
m_Parent(_parent)
{
assert(m_Cost >= 0 && m_Heuristic >= 0);
}
Node_(Node_&& _node)
{
*this = std::move(_node);
}
Node_& operator =(Node_&& _node)
{
if (this != &_node)
{
m_ID = std::move(_node.m_ID);
m_Cost = std::move(_node.m_Cost);
m_Heuristic = std::move(_node.m_Heuristic);
m_Parent = std::move(_node.m_Parent);
}
return *this;
}
const Identifier& getIdentifier() const
{
return m_ID;
}
int getCost() const
{
if (m_Parent)
return m_Parent->getCost() + m_Cost;
return m_Cost;
}
int getHeuristic() const
{
return m_Heuristic;
}
int getFinalCost() const
{
return getCost() + getHeuristic();
}
const Node_* getParent() const
{
return m_Parent;
}
};
using Node = Node_<Identifier>;
using NodePtr = std::unique_ptr<Node>;
template <class Identifier>
class AreaVisitor_ : sl::NonCopyable
{
public:
using Node = Node;
using NodePtr = NodePtr;
virtual ~AreaVisitor_() = default;
virtual std::vector<NodePtr> getNeighborNodes(const Node& _pos) = 0;
virtual int calculateHeuristic(const Identifier& _id) const = 0;
virtual void setFinalChecked(const Node& _node) = 0;
virtual void setInOpenList(const Node& _node) = 0;
virtual bool isInOpenList(const Node& _node) = 0;
virtual void removeFromOpenList(const Node& _node) = 0;
virtual void setup(const Identifier& _destinationID) = 0;
};
using AreaVisitor = AreaVisitor_<Identifier>;
using AreaVisitorPtr = std::unique_ptr<AreaVisitor>;
private:
AreaVisitorPtr m_AreaVisitor;
public:
AStar(AreaVisitorPtr&& _visitor) :
m_AreaVisitor(std::move(_visitor))
{
assert(m_AreaVisitor);
}
Path calculatePath(const Identifier& _startID, const Identifier& _destinationID) const override
{
assert(m_AreaVisitor);
m_AreaVisitor->setup(_destinationID);
std::vector<NodePtr> openList, closedList;
openList.emplace_back(std::make_unique<Node>(_startID, 0, m_AreaVisitor->calculateHeuristic(_startID), nullptr));
while (!openList.empty())
{
auto node = std::move(openList.back());
assert(node);
openList.pop_back();
m_AreaVisitor->removeFromOpenList(*node);
// check if node is destination
if (node->getIdentifier() == _destinationID)
{
Path result;
for (const Node* curNode = node.get(); curNode != nullptr; curNode = curNode->getParent())
result.push_back(curNode->getIdentifier());
std::reverse(std::begin(result), std::end(result));
return std::move(result);
}
// fill open nodes with new neighbors
for (auto& newNode : m_AreaVisitor->getNeighborNodes(*node))
{
// try to override older node
if (m_AreaVisitor->isInOpenList(*newNode))
{
auto itr = std::find_if(std::begin(openList), std::end(openList), [&newNode](const NodePtr& _node){
return newNode->getIdentifier() == _node->getIdentifier();
});
if ((*itr)->getFinalCost() > newNode->getFinalCost())
*(*itr) = std::move(*newNode);
}
// insert new node
else
{
m_AreaVisitor->setInOpenList(*newNode);
openList.insert(std::lower_bound(std::rbegin(openList), std::rend(openList), newNode->getFinalCost(),
[](const NodePtr& _node, int _value){
return _node->getFinalCost() < _value;
}).base(), std::move(newNode));
}
}
// current node to closed list
m_AreaVisitor->setFinalChecked(*node);
closedList.push_back(std::move(node));
}
// return empty path
return Path();
}
};
} // namespace pathfinder
} // namespace sfw
I would be really thankfull for any constructive feedback ;) I am also thankful about a better name than "AreaVisitor" for the AStar internal object.
For the algorithm to find the actual shortest path, the heuristic function must be **admissible**, meaning that it never overestimates the actual cost to get to the nearest goal node. The heuristic function is problem-specific and must be provided by the user of the algorithm.
– Loki Astari Feb 2 '16 at 23:25