Hierarchial Pathfinding Research

Repository for Hierarchial Pathfinding Research Made by sophomore student of CITM.


Project maintained by AlexMelenchon This project is under supervision of lecturer Marc Garrigó.

I am Àlex Melenchón, student of the Bachelor’s Degree in Video Games by UPC at CITM. This content is generated for the second year’s subject Project 2, under supervision of lecturer Marc Garrigó.

You can take a look at the website with detailed information about the research HERE & you can check the project HERE

Definition & Introduction to the Problem - What is Hierarchial Pathfinding?

The problem that Hierchial Pathfinding is trying to solve is that, in Videogames, most calculated paths are never fully walked; since most of the times something happens in the middle of the path that forces to change plans.

In order to achieve this, what is used by other reference videogames is the map abstraction into a hierarchy with different levels representing groups of tiles from the lower levels (this is achieved by grouping tiles into Nodes, that can be regular or not).

Then is where Hierchial Pathfinding comes into play: since it plans the path in much the same way as people would. You plan an overview route first and then refine it as needed. For example: let’s say you are in your house in Barcelona & you want to go visit Rome, you woudn’t just raw calculate each foot to get there, you will go something as: leave the house -> grab a Taxi -> go to the Airport - etc. That is basically what Hierchial Pathfinding does, it makes a quick & abstract research of the key abstracted-steps we calculated before to get from A to B &, when necessary it refines a simple path between the key steps.

The abstract plan is simple, and we split the pathfinding problem over a long period of time, only doing the next step when the current step is complete.

As I was saying, this is very useful for Videogames, since most of the times knowing the first few moves of a valid path is enough, allowing the requester to start moving in the right direction. Subsequent events may result in the agent having to change its plan, obviating the need for the rest of the paths. This has a huge advantage in performance, since we don’t have to compute all the path that will not be used.

Right off the bat. this sounds really good, but there is one key disadvantage: this does not create optimal paths, but, for real-time pathfinding applications such as Videogames, this is not a huge problem most of the times, providing the path looks reasonable.

This image of the Indie Developer K Lodeman shows how the hierarchy principal works; the huge map is divided into shorter steps in order to then refine the path.

Ways of implementation

First of all, there aren’t static religious ways to implement the Hierarchial Pathfinding (let’s call it HP for now) because it’s not an algorithm. We could say it’s a “technique” to divide the map so the desired Pathfinding Algorithm has an easier time construction the abstract & refined paths.

Regular Implementation

With this said, let’s see the principles that the “hierarchy graph must follow” in order to have an expandable & solid grid configuration (you can find the Near Optimal Hierarchical Path-Finding paper this explanation is based on HERE):

Agents:

Here you can see how the agents come into play making the graph. In the image on the left, you can see the blue divisions represent the clusters & the red lines the created nodes in the entrances. Then, in the right, the Edges between the nodes are visible, with it’s movement cost being shown.

General Methods:

This explains the general structure an standard Hierarchial Pathfinding should follow; further on the document you will find the specific method I selected to implement all of this & also other approaches to the problem, but if you want to know more detailed information about this yo ucan find it HERE).


Results:

As concluded in the paper, the addition of the Hierarchial pathfinding vs your regular algorithm pathfinding, produces these benefits:

Different Approaches

Different Algorithms:

Theta* is basically A* with a Jump Point Search implemented; if you want to know more about JPS*, there is a very nice research from my colleague, Lucho Suaya HERE; which I took as reference & inspiration to make the document you are reading right now.

Dynamic Approach:

Utility beyond Pathfinding:

Irregular Graphs (or navigation meshes):

An example of multiple leveled navigation grid, with irregular nodes & clusters.

An example of multiple leveled navigation grid, with irregular nodes but regular clusters.

Important Note: Navigation Meshes is a very cool & interesting topic to explore, but in this research I want to focus more in the basics of the Hierarchial Pathfinding, since, I am a big believer that, if you understand the basics (ergo, what I am about to explain) who will know how the Navigation Maps & Meshes work way faster &, also, since you will know what is happening in the lowest level, you will also understand the optimizations or came up with your own!

My Approach - HPA*

The approach I selected is to do a Regular Multi-Level Hierarchy A*. The reason is simple and I have already mentioned before: I want this to be an explanation so you will understand how & why a Hiearchial Pathfinding works, so let’s start from the basic, shall we?

Code Structures

The Abstract Graph structure:

struct HPAGraph
{
	//Graph Storage
	std::vector <std::vector<Cluster>> lvlClusters; //We will store all the Clusters of a level in this vector. Each level, will be an instance of the vector that will conain all the clusters
	std::vector <Entrance> entrances;
	std::vector <HierNode*> staticNodes;

	//PreProcessing
	void PrepareGraph();
	void CreateGraphLvl(int lvl);

	//Building Inner Structures: 
		//Build is for logic determination & creation of all the elements of the structure
		//Create is for the creation of a single object
	void BuildClusters(int lvl);

	void BuildEntrances();
	void CreateEntrance(Cluster* c1, Cluster* c2, ADJACENT_DIR adjDir, int lvl);

	void BuildInterNode(iPoint n1, Cluster* c1, iPoint n2, Cluster* c2, int lvl);
	void CreateInterNodes(int lvl);

	void CreateIntraNodes(int lvl);

	void CreateEdges(HierNode* from, HierNode* to, int lvl, EDGE_TYPE type);


	//Utility
	ADJACENT_DIR ClustersAreAdjacent(Cluster* c1, Cluster* c2, int lvl); //Checks if two clusters are adjacent
	HierNode* NodeExists(iPoint pos, Cluster* lvl = nullptr); //Checks if a Node exists in the whole graph or just in a concrete Cluster
	bool EdgeExists(HierNode* from, HierNode* to, int lvl, EDGE_TYPE type); // Check if an Edge between two Nodes Exist
	Cluster* DetermineCluster(iPoint nodePos, int lvl, Cluster* firstCheck = nullptr); //Determines the Cluster in whick a Node is in.

	//Node Insertion
	HierNode* insertNode(iPoint pos, int lvl, bool* toDelete = nullptr); //Insert a Node into the Graph
	void DeleteNode(HierNode* toDelete, int lvl); //Deletes a Node from the Graph
	void ConnectNodeToBorder(HierNode* node, Cluster* c, int lvl); //Connects a Node to all the Nodes in the same Cluster

};

    struct Cluster
{
	Cluster();
	Cluster(int width, int height, iPoint& pos);
	Cluster(const Cluster& clust);

	iPoint pos;
	int width, height;

	std::vector <HierNode*> clustNodes;
};

  enum class ADJACENT_DIR
{
	DIR_NONE = -1,

	VERTICAL,
	LATERAL
};

struct Entrance
{
	Entrance(iPoint pos, int width, int height, ADJACENT_DIR dir);
	Entrance();

	iPoint pos;
	int width, height;

	ADJACENT_DIR dir;

};

class PathNode
{
public:
	PathNode();
	PathNode(float g, float h, const iPoint& pos, PathNode* parent, int parentdir, int myDir, bool isdiagonal = false);
	PathNode(const PathNode& node);

	virtual uint FindWalkableAdjacents(std::vector<PathNode>& list_to_fill);
	virtual float CalculateF(const iPoint& destination);
	float Score() const;

	float g;
	float h;
	iPoint pos;

	PathNode* parent;

	int parentDir;
	int myDirection;
	bool is_Diagonal;

};

//HPA* Nodes
class HierNode : public PathNode
{
public:
	HierNode(iPoint pos);
	HierNode(iPoint pos, bool tmp);
	HierNode(float g, const iPoint& pos, PathNode* parent, int myDir, int parentdir, std::vector<Edge*> edges);

	float CalculateF(const iPoint& destination);
	uint FindWalkableAdjacents(std::vector<HierNode>& list_to_fill, int lvl);

	std::vector <Edge*> edges;
};
  struct Edge
{
	Edge(HierNode* dest, float distanceTo, int lvl, EDGE_TYPE type);

	void UpdateLvl(int lvl)
	{
		this->lvl = lvl;
	}

	HierNode* dest;
	float moveCost;

	int lvl;
	EDGE_TYPE type;

};
  MapLoad()
  {
    HPAGraph absGraph;

	HPAPreProcessing(MAX_LEVELS);
  }


  void HPAPreProcessing(int maxLevel)
  {
    absGraph.PrepareGraph();

    for (int l = 2; l <= maxLevel; l++)
    {
      absGraph.CreateGraphLvl(l);
    }

  }

  void PrepareGraph()
  {

    BuildClusters(1);
    BuildEntrances();

    CreateInterNodes(1);
    CreateIntraNodes(1);
  }

  void CreateGraphLvl(int lvl)
  {
    BuildClusters(lvl);

    CreateInterNodes(lvl);
    CreateIntraNodes(lvl);
  }
  

Being the white tiles walkable & the black ones non-walkable.

  void BuildClusters(int lvl)
  {
    int clustSize = CLUSTER_SIZE_LVL * lvl;

    //Buffer vector
    vector <Cluster> clusterVector;

    Cluster c;

    //We will create the cluster in a row-column order
    for (int i = 0; i < mapWidth , i += clustSize)
    {

      if (i + clustSize > width)
        c.width = width - (i);
      else
        c.width = clustSize;

      for (int k = 0; k < mapHeight, k += clustSize)
      {

        if (k + clustSize > height)
          c.height = height - (k);
        else
          c.height = clustSize;

        c.pos = { i,k };
        clusterVector.push_back(Cluster(c));
      }
    }

    lvlClusters.push_back(clusterVector);
  }
  

As you can see, now the Map has divisions of Clusters that group tiles of the non-abstract level. Note that if we added another Graph Level, it would create another Clusters double the size.

  void BuildEntrances()
  {
      for (each clust1, clust2  lvlClusters[0]) 
      {
        if ClustersAreAdjacent(clust1, clust2, 1)
         CreateEntrance(clust1, clust2, adjacentDir, 1);
      }
  }

Great! The yellow rectangles represent the entrances between the Clusters, but something weird happened. Some Nodes are grey & have weird lines between them; let’s figure out what this is!

  void CreateInterNodes(int lvl)
{
	for (each entrance)
	{
		switch (currEntrance->dir)
		{
		case ADJACENT_DIR::LATERAL:
		{
			c1 = DetermineCluster(currEntrance->pos, lvl);
			c2 = DetermineCluster({ currEntrance->pos.x + 1, currEntrance->pos.y }, lvl);

			if (ClustersAreAdjacent(c1, c2, lvl) != ADJACENT_DIR::LATERAL)
				continue;


			int maxSize = (currEntrance->pos.y + currEntrance->height);

			for (int i = currEntrance->pos.y; i < maxSize; i += NODE_MIN_DISTANCE)
			{
				BuildInterNode({ currEntrance->pos.x,i }, c1, { currEntrance->pos.x + 1, i }, c2, lvl);
			}

		}
		break;
		case ADJACENT_DIR::VERTICAL:
		{
			// Same as adove, different axis....
		}
		break;
		}

	}
}

void BuildInterNode(iPoint p1, Cluster* c1, iPoint p2, Cluster* c2, int lvl)
{

	n1 = NodeExists(p1, allTheGraph);

	if (!n1)
	{
		n1 = new HierNode(p1);
		staticNodes.push_back(n1);
	}

	if (NodeExists(p1, c1) == NULL)
		c1->clustNodes.push_back(n1);


	n2 = NodeExists(p2, allTheGraph);
	...


	CreateEdges(n1, n2, lvl, EDGE_TYPE::INTER);
}

void CreateIntraNodes(int lvl)
{
	for (each cluster in currlvl)
	{
		for (each pair of nodes in cluster)
		{
		CreateEdges(n1, cn2, lvl, EDGE_TYPE::INTRA);
		}
	}
}
void CreateEdges(HierNode* n1, HierNode* n2, int lvl, EDGE_TYPE type)
{
	float distanceTo = 1.f;
	if (type == EDGE_TYPE::INTRA || !EdgeExists(n1, n2, lvl, type))
	{
		//If the connection if between same cluster nodes, we calculate the moving cost trough A*; otherwise  is 1
		if (type == EDGE_TYPE::INTRA)
		{

			distanceTo = SimpleAPathfinding(n1->pos, n2->pos).GetCost();
		}

		n1->edges.push_back(new Edge(n2, distanceTo, lvl, type));
	}

	//Repeat the process for the second node----
	if (type == EDGE_TYPE::INTRA || !EdgeExists(n2, n1, lvl, type))
	{
		if (type == EDGE_TYPE::INTRA)
		{
			if (distanceTo == 1)
				distanceTo = SimpleAPathfinding(n1->pos, n2->pos).GetCost();
		}

		n2->edges.push_back(new Edge(n1, distanceTo, lvl, type));
	}

}
	

Back at our map, now this makes sense: the RED represents the Nodes, the GREN the Inter Edges & the BLUE the Intra Edges (they are calculated just in one Cluster)

Search Process

PATH_TYPE CreatePath(const iPoint& origin, const iPoint& destination, int maxLvl)
{
	n1 = absGraph.insertNode(origin, maxLvl, &toDeleteN1);
	n2 = absGraph.insertNode(destination, maxLvl, &toDeleteN2);

	if (!n1 || !n2)
		return ret;

	n1->h = n1->pos.OctileDistance(n2->pos);


	//HPA* algorithm
	HPAPathfinding(*n1, n2->pos, maxLvl);


	//Delete the nodes from the graph
	if (toDeleteN1)
		absGraph.DeleteNode((HierNode*)n1, maxLvl);
	if (toDeleteN2)
		absGraph.DeleteNode((HierNode*)n2, maxLvl);
}
HierNode* HPAGraph::insertNode(iPoint pos, int Lvl, bool* toDelete)
{

	HierNode* newNode = nullptr;
	Cluster* c;

	if (!App->pathfinding->IsWalkable(pos))
		return nullptr;

	//Determine the cluster
	c = DetermineCluster(pos, Lvl);
	if (!c)
		return nullptr;

	//Check if already exists
	newNode = NodeExists(pos, c);

	//If can be placed inside a cluster & there is no node already there, we create one
	if (!newNode)
	{
		newNode = new HierNode(pos);
		c->clustNodes.push_back(newNode);
		
		//Connects a recently introduced node to it's peers in the same cluster
		ConnectNodeToBorder(newNode, c, Lvl);
		
		*toDelete = true;
	}

	return newNode;
}
void HPAGraph::ConnectNodeToBorder(HierNode* node, Cluster* c, int lvl)
{
	float distanceTo = 0;
	for (int i = 0; i < c->clustNodes.size(); i++)
	{
		//To calculate the cost to the node peers, we should do A* and get the cost from there
		// But this is very slow, so we  will do a RayCast Check first; this is a little less accurate but far more faster
		if (App->pathfinding->LineRayCast(node->pos, c->clustNodes[i]->pos))
		{
			distanceTo = App->pathfinding->GetLastLine()->size();
		}
		else
			distanceTo = App->pathfinding->SimpleAPathfinding(node->pos, c->clustNodes[i]->pos);


		//Create the edges between the two nodes
		node->edges.push_back(new Edge(c->clustNodes[i], distanceTo, lvl, EDGE_TYPE::INTRA));
		c->clustNodes[i]->edges.push_back(new Edge(node, distanceTo, lvl, EDGE_TYPE::INTRA));
	}

}

Following the Map example it would look something like this (being the O & D the Origin and Destination respectively & the Yellow lines the Edges we just create using the ConnectToBorder() method):

Remember that the images only has the Intra Edges for one Cluster! It should have that for every Cluster of the Graph.

int ModulePathfinding::HPAPathfinding(const HierNode& origin, const iPoint& destination, int lvl)
{
	BROFILER_CATEGORY("HPA Algorithm", Profiler::Color::AliceBlue);

	last_path.clear();

	//Variable declaration-----
	//Open & Closed
	std::multimap<float, HierNode> open;
	std::vector<HierNode> closed;

	//Analize the current
	HierNode* curr = nullptr;
	std::multimap<float, HierNode>::iterator lowest;

	// Get New Nodes
	uint limit = 0;
	std::vector<HierNode> adjList;
	std::multimap<float, HierNode>::iterator it2;


	open.insert(std::pair<float, HierNode>(0.f, origin));


	while (open.empty() == false)
	{
		lowest = open.begin();
		closed.push_back(lowest->second);
		curr = &closed.back();

		curr->myDirection = (closed.size() - 1);
		open.erase(lowest);


		if (curr->pos == destination)
		{
			int dir;
			for (curr; curr->pos != origin.pos; curr = &closed[dir])
			{
				last_path.push_back(curr->pos);
				dir = curr->parentDir;
			}
			last_path.push_back(origin.pos);

			std::reverse(last_path.begin(), last_path.end());

			return 0;
		}

		limit = curr->FindWalkableAdjacents(adjList, lvl);

		for (uint i = 0; i < limit; i++)
		{

			if (FindV(adjList[i].pos, &closed) == closed.size())
			{
				it2 = Find(adjList[i].pos, &open);
				adjList[i].CalculateF(destination);
				if (it2 == open.end())
				{
					open.insert(std::pair<float, HierNode>(adjList[i].Score(), adjList[i]));
				}
				else
				{
					if (adjList[i].Score() < it2->second.Score())
					{
						open.erase(Find(adjList[i].pos, &open));
						open.insert(std::pair<float, HierNode>(adjList[i].Score(), adjList[i]));
					}
				}
			}
		}
		adjList.clear();
	}
}

uint HierNode::FindWalkableAdjacents(std::vector<HierNode>& list_to_fill, int lvl)
{
	int edgeNum = edges.size();
	HierNode* currNode = nullptr;
	Edge* currEdge = nullptr;


	//Iterate though all the node edges
	for (int i = 0; i < edgeNum; i++)
	{
		currEdge = edges[i];

		//Checks if the edge has the correct level to put it in the list
			//INTRA: have to be from the same level
			//INTER: can be from the same level or superior
		if (currEdge->type == EDGE_TYPE::INTRA && currEdge->lvl == lvl || currEdge->type == EDGE_TYPE::INTER && currEdge->lvl >= lvl)
		{
			currNode = currEdge->dest;
			list_to_fill.push_back(HierNode(currEdge->moveCost + this->g, currNode->pos, this, myDirection, 0, currNode->edges));
		}

	}

	//---



	return list_to_fill.size();
}

float HierNode::CalculateF(const iPoint& destination)
{
	h = pos.OctileDistance(destination);

	return g + h;
}

Refinemt & Smooth Process

Finishing the Hierarchial Path, we would have a path that would look something like this:

bool ModulePathfinding::RefineAndSmoothPath(std::vector<iPoint>* abstractPath, int lvl, std::vector<iPoint>* pathToFill)
{
	iPoint currPos = { -1, -1 };
	iPoint startPos = { -1, -1 };

	int from = -1;
	Cluster* startCluster = nullptr;

	int pathSize = abstractPath->size();

	for (int i = 0; i < pathSize; i)
	{
		currPos = abstractPath->at(i);

		//Grab the first node
		if (startPos.x == -1)
		{
			startPos = currPos;
			from = i;
			startCluster = absGraph.DetermineCluster(startPos, lvl);
			i++;
			continue;
		}

		//Two Conditions to make path:
			//Check that Distance is not greater than Cluster Size
			//Not be the last node

		if (startCluster != absGraph.DetermineCluster(currPos, lvl) || (i == pathSize - 1 && pathSize > 0))
		{

			std::vector <iPoint>* generatedPath = nullptr;

			//First Quick Check w/Ray Cast
			if (LineRayCast(startPos, currPos) && !last_line.empty())
			{
				generatedPath = &last_line;
			}

			//If the Ray Cast fails, we have to do A* to connect the nodes
			else if (SimpleAPathfinding(startPos, currPos) && !last_path.empty())
			{
				generatedPath = &last_path;
			}

			//If the refinement was succesfull, we added to the request
			if (generatedPath != nullptr)
			{
				//Last & not last cases:
					//We don't want to introduce the first one since it will overlap with the last one already refined
					/// If our code for following the path is solid this isn't necessary but it's nice to have
				if (pathToFill->size() > 0)
					pathToFill->insert(pathToFill->end(), generatedPath->begin() + 1, generatedPath->end());
				else
					pathToFill->insert(pathToFill->end(), generatedPath->begin(), generatedPath->end());
			}


			//Delete the abstract nodes we just refined
			abstractPath->erase(abstractPath->begin() + from, abstractPath->begin() + i);

			return true;
		}
		//---
		else
		{
			abstractPath->erase(abstractPath->begin() + 1);
			pathSize--;
			continue;
		}

	}

	return false;
}

And that is it! Our path would look something like this:

IMPORTANT: this is not the Full Code, if you want to know more about it, I encourage you to take a look at the Full Code or do the TODO’s listed below.

The Results

A* HPA*
O (N) O( log(N))

Being N the length of the path to check. I got this information from the GDC Hierarchical Dynamic Pathfinding presentation I mentioned before

  Computation Time Solution Length
A* 7.3ms 126
HPA* ( on avg.) 1.18ms (0.2ms to Insert + 0.9ms to Pathfind) 34

TODO’s

With all the explanation out of the window, we are now ready to complete the TODO’s. These are small exercises guided by code comments that are intended to taught you about the HPA* in a more practical sense. I’ll walk you through all the process explained above so you can fully understand the implementation. Once you finish all the TODO’s you will have a fully functional HPA*.

IMPORTANT NOTE: All the places where you have to write code are marked with “ //—– “.

IMPORTANT NOTE: To check all the debug functionallities, please check the README inside the project, thanks!

TODO 0

TODO 1

TODO 1.1

void HPAGraph::BuildClusters(int lvl)
{
	//TODO 1.1: The clusters are created regulary here.
	// The code to calculate how big & where the clusters are is done, just add it to the buffer vector (*1) & then add it to the
	// vector of clusters vectors (ask me about this :P) we have already declared (*2)

	//Cluster distance in the current level
	int clustSize = CLUSTER_SIZE_LVL * lvl;

	//Buffer vector
	std::vector <Cluster> clusterVector;

	int width = App->pathfinding->mapWidth;
	int height = App->pathfinding->mapHeight;

	Cluster c;

	//We will create the cluster in a row-column order
	for (int i = 0; i < width; i += clustSize)
	{
		//Check if the cluster would extend beyond the map
		if (i + clustSize > width)
			c.width = width - (i);
		else
			c.width = clustSize;

		for (int k = 0; k < height; k += clustSize)
		{

			//Check if the cluster would extend beyond the map
			if (k + clustSize > height)
				c.height = height - (k);
			else
				c.height = clustSize;

			//Introduce the Cluster into the vector buffer
			c.pos = { i,k };
			//------ (*1)

		}
	}

	//Introduce the vector to the main one
	//------ (*2)

}
}

void HPAGraph::BuildClusters(int lvl)
{
	//TODO 1.1: The clusters are created regulary here.
	// The code to calculate how big & where the clusters are is done, just add it to the buffer vector (*1) & then add it to the
	// vector of clusters vectors (ask me about this :P) we have already declared (*2)


	//Cluster distance in the current level
	int clustSize = CLUSTER_SIZE_LVL * lvl;

	//Buffer vector
	std::vector <Cluster> clusterVector;


	int width = App->pathfinding->mapWidth;
	int height = App->pathfinding->mapHeight;

	Cluster c;

	//We will create the cluster in a row-column order
	for (int i = 0; i < width; i += clustSize)
	{

		//Check if the cluster would extend beyond the map
		if (i + clustSize > width)
			c.width = width - (i);
		else
			c.width = clustSize;

		for (int k = 0; k < height; k += clustSize)
		{

			//Check if the cluster would extend beyond the map
			if (k + clustSize > height)
				c.height = height - (k);
			else
				c.height = clustSize;

			//Introduce the Cluster into the vector buffer
			c.pos = { i,k };
			//------ (*1)
			clusterVector.push_back(Cluster(c));
		}
	}

	//Introduce the vector to the main one
	//------ (*2)
	this->lvlClusters.push_back(clusterVector);
}

TODO 1.2

void HPAGraph::BuildEntrances()
{
	Cluster* c1 = nullptr;
	Cluster* c2 = nullptr;
	ADJACENT_DIR adjacentDir = ADJACENT_DIR::DIR_NONE;

	// Since the Entrances are just created in the lowest abstraction LVL, their lvl is always 0
	int EntranceLvl = 1;

	//TODO 1.2: Here we iterate through all the clusters; just check if a pair of clusters are adjacents & if so create an entrance between them
	// Check the functions ClustersAreAdjacent() & CreateEntrance()

	//We simply iterate though the clusters & check if they are adjacent, if so we put an entrance between them
	for (uint i = 0; i < this->lvlClusters[0].size(); ++i)
	{
		c1 = &this->lvlClusters[0].at(i);

		for (uint k = i + 1; k < this->lvlClusters[0].size(); ++k)
		{
			c2 = &this->lvlClusters[0].at(k);

			//----

		}
	}
}



void HPAGraph::BuildEntrances()
{
	Cluster* c1;
	Cluster* c2;
	ADJACENT_DIR adjacentDir = ADJACENT_DIR::DIR_NONE;

	// Since the Entrances are just created in the lowest abstraction LVL, their lvl is always 0
	int EntranceLvl = 1;

	//TODO 1.2: Here we iterate through all the clusters; just check if a pair of clusters is adjacent & if so create entrances for it
	// Check the functions ClustersAreAdjacent() & CreateEntrance()

	//We simply iterate though the clusters & check if they are adjacent, if so we put an entrance between them
	for (uint i = 0; i < this->lvlClusters[0].size(); ++i)
	{
		c1 = &this->lvlClusters[0].at(i);

		for (uint k = i + 1; k < this->lvlClusters[0].size(); ++k)
		{
			c2 = &this->lvlClusters[0].at(k);

			//----
			adjacentDir = ClustersAreAdjacent(c1, c2, EntranceLvl);

			if (adjacentDir != ADJACENT_DIR::DIR_NONE)
				CreateEntrance(c1, c2, adjacentDir, EntranceLvl);

		}
	}
}

TODO 2

TODO 2.1

void HPAGraph::CreateInterNodes(int lvl)
{
	Entrance* currEntrance;
	Cluster* c1, * c2;

	//TODO 2.1: Here the Nodes between Clusters are created.
	// Just check how lateral is created & then do the vertical one (the logic is the same, you just have to change the axis)
	// Be careful with the position!

	for (uint i = 0; i < entrances.size(); i++)
	{
		currEntrance = &entrances[i];

		switch (currEntrance->dir)
		{
		case ADJACENT_DIR::LATERAL:
		{

			//First of all we check if the clusters are adjacents
			c1 = DetermineCluster(currEntrance->pos, lvl);
			c2 = DetermineCluster({ currEntrance->pos.x + 1, currEntrance->pos.y }, lvl);

			if (ClustersAreAdjacent(c1, c2, lvl) != ADJACENT_DIR::LATERAL)
				continue;


			//Build the max. number of nodes that can we built based on the cluster size & the min distance we established
			int maxSize = (currEntrance->pos.y + currEntrance->height);

			for (int i = currEntrance->pos.y; i < maxSize; i += NODE_MIN_DISTANCE)
			{
				BuildInterNode({ currEntrance->pos.x,i }, c1, { currEntrance->pos.x + 1, i }, c2, lvl);
			}

			//We make sure that the last spot has a node for path stability
			BuildInterNode({ currEntrance->pos.x, maxSize - 1 }, c1, { currEntrance->pos.x + 1, maxSize - 1 }, c2, lvl);

		}
		break;
		case ADJACENT_DIR::VERTICAL:
		{
			//---

		}
		break;
		}

	}
}

void HPAGraph::CreateInterNodes(int lvl)
{
	Entrance* currEntrance = nullptr;
	Cluster* c1, * c2;

	//TODO 2.1: Here the Nodes between Clusters are created.
	// Just check how lateral is created & then do the vertical one (the logic is the same, you just have to change the axis)
	// Be careful with the position!

	for (uint i = 0; i < entrances.size(); i++)
	{
		currEntrance = &entrances[i];

		switch (currEntrance->dir)
		{
		case ADJACENT_DIR::LATERAL:
		{

			//First of all we check if the clusters are adjacents
			c1 = DetermineCluster(currEntrance->pos, lvl);
			c2 = DetermineCluster({ currEntrance->pos.x + 1, currEntrance->pos.y }, lvl);

			if (ClustersAreAdjacent(c1, c2, lvl) != ADJACENT_DIR::LATERAL)
				continue;


			//Build the max. number of nodes that can we built based on the cluster size & the min distance we established
			int maxSize = (currEntrance->pos.y + currEntrance->height);

			for (int i = currEntrance->pos.y; i < maxSize; i += NODE_MIN_DISTANCE)
			{
				BuildInterNode({ currEntrance->pos.x,i }, c1, { currEntrance->pos.x + 1, i }, c2, lvl);
			}

			//We make sure that the last spot has a node for path stability
			BuildInterNode({ currEntrance->pos.x, maxSize - 1 }, c1, { currEntrance->pos.x + 1, maxSize - 1 }, c2, lvl);

		}
		break;
		case ADJACENT_DIR::VERTICAL:
		{
			//---
			//First of all we check if the clusters are adjacents
			c1 = DetermineCluster(currEntrance->pos, lvl);
			c2 = DetermineCluster({ currEntrance->pos.x, currEntrance->pos.y + 1 }, lvl);

			if (ClustersAreAdjacent(c1, c2, lvl) != ADJACENT_DIR::VERTICAL)
				continue;


			//Build the max. number of nodes that can we built based on the cluster size & the min distance we established
			int maxSize = (currEntrance->pos.x + currEntrance->width);

			for (int i = currEntrance->pos.x; i < maxSize; i += NODE_MIN_DISTANCE)
			{
				//if (!NodeExists({ i+1, currEntrance->pos.y }) && !NodeExists({ currEntrance->pos.x,i - 1 }))
				BuildInterNode({ i, currEntrance->pos.y }, c1, { i, currEntrance->pos.y + 1 }, c2, lvl);
			}

			//We make sure that the last spot has a node for path stability
			BuildInterNode({ maxSize - 1, currEntrance->pos.y }, c1, { maxSize - 1, currEntrance->pos.y + 1 }, c2, lvl);

		}
		break;
		}

	}
}

TODO 2.2

void HPAGraph::CreateIntraNodes(int lvl)
{
	Cluster* clusterIt;
	float distanceTo = 0;
	int clusterLevel = lvl - 1;
	int edgeLevel = lvl;

	HierNode* n1 = nullptr;
	HierNode* n2 = nullptr;

	if (clusterLevel > lvlClusters.size() || lvlClusters.empty())
		return;

	//TODO 2.2: Here we iterate trough all the nodes in a same cluster for all the cluster in a same level
	// Simply call the function to create edges between the different nodes with the INTRA type
	// Check the function CreateEdges()

	for (int i = 0; i < lvlClusters[clusterLevel].size(); i++)
	{
		clusterIt = &lvlClusters[clusterLevel].at(i);

		for (int y = 0; y < clusterIt->clustNodes.size(); y++)
		{
			n1 = clusterIt->clustNodes[y];
			for (int k = y + 1; k < clusterIt->clustNodes.size(); k++)
			{
				n2 = clusterIt->clustNodes[k];

				//--
				
			}
		}
	}
}

oid HPAGraph::CreateIntraNodes(int lvl)
{
	Cluster* clusterIt;
	float distanceTo = 0;
	int clusterLevel = lvl - 1;
	int edgeLevel = lvl;

	HierNode* n1 = nullptr;
	HierNode* n2 = nullptr;

	if (clusterLevel > lvlClusters.size() || lvlClusters.empty())
		return;

	//TODO 2.2: Here we iterate trough all the nodes in a same cluster for all the cluster in a same level
	// Simply call the function to create edges between the different nodes with the INTRA type
	// Check the function CreateEdges()

	for (int i = 0; i < lvlClusters[clusterLevel].size(); i++)
	{
		clusterIt = &lvlClusters[clusterLevel].at(i);

		for (int y = 0; y < clusterIt->clustNodes.size(); y++)
		{
			for (int k = y + 1; k < clusterIt->clustNodes.size(); k++)
			{
				//--
				CreateEdges(clusterIt->clustNodes[y], clusterIt->clustNodes[k], edgeLevel, EDGE_TYPE::INTRA);

			}
		}
	}
}


TODO 3

-This is simple, if the node exist in the cluster we do not do anything. If it doesn’t exist, create it: you’ll have to push it in the cluster Node Vector & connect it t the other nodes in the same cluster (Check ConnectNodeToBorder()).

HierNode* HPAGraph::insertNode(iPoint pos, int Lvl, bool* toDelete)
{
	BROFILER_CATEGORY("Insert Node", Profiler::Color::Cornsilk);

	HierNode* newNode = nullptr;
	Cluster* c;

	if (!App->pathfinding->IsWalkable(pos))
		return nullptr;

	//Determine the cluster
	c = DetermineCluster(pos, Lvl);
	if (!c)
		return nullptr;


	//TODO 3: This is simple, if the node exist in the cluster we do not do anything 
	// If it doesn't exist, create it: you'll have to push it in the cluster Node Vector (ClustNodes) & connect it t the other nodes in the same
	// cluster (Check ConnectNodeToBorder()).


	//Check if already exists
	newNode = NodeExists(pos, c);


	//If can be placed inside a cluster & there is no node already there, we create one
	if (!newNode)
	{
		//----

		*toDelete = true;
	}

	//---

	return newNode;
}



HierNode* HPAGraph::insertNode(iPoint pos, int Lvl, bool* toDelete)
{
	BROFILER_CATEGORY("Insert Node", Profiler::Color::Cornsilk);

	HierNode* newNode = nullptr;
	Cluster* c;

	if (!App->pathfinding->IsWalkable(pos))
		return nullptr;

	//Determine the cluster
	c = DetermineCluster(pos, Lvl);
	if (!c)
		return nullptr;

	//TODO 3: This is simple, if the node exist in the cluster we do not do anything 
	// If it doesn't exist, create it: you'll have to push it in the cluster Node Vector (ClustNodes)  & connect it t the other nodes in the same
	// cluster (Check ConnectNodeToBorder()).

	//Check if already exists
	newNode = NodeExists(pos, c);

	//If can be placed inside a cluster & there is no node already there, we create one
	if (!newNode)
	{
		newNode = new HierNode(pos);
		c->clustNodes.push_back(newNode);
		ConnectNodeToBorder(newNode, c, Lvl);
		*toDelete = true;
	}

	return newNode;
}

TODO 4

uint HierNode::FindWalkableAdjacents(std::vector<HierNode>& list_to_fill, int lvl)
{
	int edgeNum = edges.size();
	HierNode* currNode = nullptr;
	Edge* currEdge = nullptr;


	// TODO 4: Let's find the walkable node adjacents. We have to iterate though all the HierNode's edges 
	// But that is already done, you just have to Create new Hiernodes & Insert them into the list
	//The Hiernodes will we determined by the Edges movement Cost & it's destination Node (Take a look at the Edge Struct)
	//Remeber that the G has to be calculated when the Hiernode is inserted!
	
	for (int i = 0; i < edgeNum; i++)
	{
		currEdge = edges[i];

		//LOOK OUT!! Checks if the edge has the correct level to put it in the list (Ask me about this :P):
			//INTRA: have to be from the same level
			//INTER: can be from the same level or superior
		if (currEdge->type == EDGE_TYPE::INTRA && currEdge->lvl == lvl || currEdge->type == EDGE_TYPE::INTER && currEdge->lvl >= lvl)
		{
			//----

		}

	}

	return list_to_fill.size();
}
}



uint HierNode::FindWalkableAdjacents(std::vector<HierNode>& list_to_fill, int lvl)
{
	int edgeNum = edges.size();
	HierNode* currNode = nullptr;
	Edge* currEdge = nullptr;

	// TODO 4: Let's find the walkable node adjacents. We have to iterate though all the HierNode's edges 
	// But that is already done, you just have to Create new Hiernodes & Insert them into the list
	//The Hiernodes will we determined by the Edges movement Cost & it's destination Node (Take a look at the Edge Struct)
	//Remeber that the G has to be calculated when the Hiernode is inserted!


	//Iterate though all the node edges
	for (int i = 0; i < edgeNum; i++)
	{
		currEdge = edges[i];

		//Checks if the edge has the correct level to put it in the list
			//INTRA: have to be from the same level
			//INTER: can be from the same level or superior
		if (currEdge->type == EDGE_TYPE::INTRA && currEdge->lvl == lvl || currEdge->type == EDGE_TYPE::INTER && currEdge->lvl >= lvl)
		{
			//----
			currNode = currEdge->dest;
			list_to_fill.push_back(HierNode(currEdge->moveCost + this->g, currNode->pos, this, myDirection, 0, currNode->edges));
		}

	}


	return list_to_fill.size();
}

TODO 5

TODO 5.1

TODO 5.2

TODO 5.3:

This TODO is divided in three parts to better understanding, but, since it’s all in the same function here you will find it as one.

bool ModulePathfinding::RefineAndSmoothPath(std::vector<iPoint>* abstractPath, int lvl, std::vector<iPoint>* pathToFill)
{
	BROFILER_CATEGORY("Refine And Smooth Path", Profiler::Color::RosyBrown);

	iPoint currPos = { -1, -1 };
	iPoint startPos = { -1, -1 };

	int from = -1;
	int pathSize = abstractPath->size();

	Cluster* fromC = nullptr;

	for (int i = 0; i < pathSize; i)
	{
		currPos = abstractPath->at(i);

		//Get the first node
		if (startPos.x == -1)
		{
			startPos = currPos;
			from = i;
			fromC = absGraph.DetermineCluster(startPos, lvl);
			i++;
			continue;
		}

		//TODO 5: This is a big one. First of all read the whole function & understand what is happening

		//TODO 5.1: Currently the code just does nothing with the hierchial path, what you have to do is set the 
				// conditions to refine the path & actually refined. 
				// The conditions are: 
					//Check that the current position is from a diferent cluster than the Start pne OR
					// It is the last node in the Abstract Path
		//----
		if (true)
		{
			//TODO 5.2: Then you will have to refine the Path, we have two ways to do this:
							//RayCasting: Very fast, doesn't work through obstacles
							// Direct A*: slower but can make whatever path you send to it.
						//The ideal situation would be to check with a RayCast first & if not succesfull make an A*
						//To insert the path, check the functions on the std::vector (Insert() might be usefull :P)
			//-----


			// TODO 5.3: Do not forget to delete the nodes from the hierchial path after you have refined them!
			// Check the function on the std::vector erase()
			//------

			return true;
		}
		else
		{
			abstractPath->erase(abstractPath->begin() + 1);
			pathSize--;
			continue;
		}

	}

	return false;
}

bool ModulePathfinding::RefineAndSmoothPath(std::vector<iPoint>* abstractPath, int lvl, std::vector<iPoint>* pathToFill)
{
	BROFILER_CATEGORY("Refine And Smooth Path", Profiler::Color::RosyBrown);

	iPoint currPos = { -1, -1 };
	iPoint startPos = { -1, -1 };

	int from = -1;
	Cluster* startCluster = nullptr;

	int pathSize = abstractPath->size();

	for (int i = 0; i < pathSize; i)
	{
		currPos = abstractPath->at(i);

		//Grab the first node
		if (startPos.x == -1)
		{
			startPos = currPos;
			from = i;
			startCluster = absGraph.DetermineCluster(startPos, lvl);
			i++;
			continue;
		}

		//TODO 5: This is a big one. First of all read the whole function & understand what is happening

		//TODO 5.1: Currently the code just does nothing with the hierchial path, what you have to do is set the 
				// conditions to refine the path & actually refined. 
				// The conditions are: 
					//Check that the current position is from a diferent cluster than the Start pne OR
					// It is the last node in the Abstract Path

		//Two Conditions to make path:
			//Check that Distance is not greater than Cluster Size
			//Not be the last node

		//----
		if (startCluster != absGraph.DetermineCluster(currPos, lvl) || (i == pathSize - 1 && pathSize > 0))
		{

			//TODO 5.2: Then you will have to refine the Path, we have two ways to do this:
							//RayCasting: Very fast, doesn't work through obstacles
							// Direct A*: slower but can make whatever path you send to it.
						//The ideal situation would be to check with a RayCast first & if not succesfull make an A*
						//To insert the path, check the functions on the std::vector (Insert() might be usefull :P)
			//-----

			std::vector <iPoint>* generatedPath = nullptr;

			//First Quick Check w/Ray Cast
			if (LineRayCast(startPos, currPos) && !last_line.empty())
			{
				generatedPath = &last_line;
			}

			//If the Ray Cast fails, we have to do A* to connect the nodes
			else if (SimpleAPathfinding(startPos, currPos) && !last_path.empty())
			{
				generatedPath = &last_path;
			}

			//If the refinement was succesfull, we added to the request
			if (generatedPath != nullptr)
			{
				//Last & not last cases:
					//We don't want to introduce the first one since it will overlap with the last one already refined
					/// If our code for following the path is solid this isn't necessary but it's nice to have
				if (pathToFill->size() > 0)
					pathToFill->insert(pathToFill->end(), generatedPath->begin() + 1, generatedPath->end());
				else
					pathToFill->insert(pathToFill->end(), generatedPath->begin(), generatedPath->end());
			}

			// TODO 5.3: Do not forget to delete the nodes from the hierchial path after you have refined them!
			// Check the function on the std::vector erase()
			//------
			//Delete the abstract nodes we just refined
			abstractPath->erase(abstractPath->begin() + from, abstractPath->begin() + i);

			return true;
		}
		//---
		else
		{
			abstractPath->erase(abstractPath->begin() + 1);
			pathSize--;
			continue;
		}

	}

	return false;
}

TODO 6

	//TODO 6 (EXTRA): Let's have entities & make them HPA*. Uncomment the code

	//if (App->input->GetKey(SDL_SCANCODE_1) == KEY_DOWN)
	//{
	//	int x, y;
	//	App->input->GetMousePosition(x, y);
	//	iPoint mousePos = App->render->ScreenToWorld(x, y);

	//	if (App->pathfinding->IsWalkable(App->map->WorldToMap(mousePos.x, mousePos.y)))
	//		App->entMan->AddNewEntity(ENTITY_TYPE::DYNAMIC, { (float)mousePos.x, (float)mousePos.y });
	//}
	//TODO 6.0: Let's have entities & make them HPA*. Uncomment the code
	//----
	if (App->input->GetKey(SDL_SCANCODE_1) == KEY_DOWN)
	{
		int x, y;
		App->input->GetMousePosition(x, y);
		iPoint mousePos = App->render->ScreenToWorld(x, y);

		if (App->pathfinding->IsWalkable(App->map->WorldToMap(mousePos.x, mousePos.y)))
			App->entMan->AddNewEntity(ENTITY_TYPE::DYNAMIC, { (float)mousePos.x, (float)mousePos.y });
	}
	//----

TODO 6.1

bool Entity::Move(float dt)
{

	fPoint pathSpeed = { 0,0 };
	fPoint nextPoint = { 0,0 };

	//TODO 6.1 (EXTRA): With everything set up, we just have to call the RequestPath() function and fill the entity's path
		// We can do this every frame, with a time condition, with a path legnth conditio...n; whatever fits your project!
	//----

	if (path.size() > 0)
	{
	....
bool Entity::Move(float dt)
{
	//Speed is resetted to 0 each iteration
	// ----------------------------------------------------------------

	fPoint pathSpeed = { 0,0 };
	fPoint nextPoint = { 0,0 };


	//TODO 6.1 (EXTRA): With everything set up, we just have to call the RequestPath function and fill the entity's path
	// We can do this every frame, with a time condition, with a path legnth conditio...n; whatever fits your project!
	//----
	if (path.size() < 2)
	{
		App->pathfinding->RequestPath(this, &path);
	}
	//----


	if (path.size() > 0)
	{
	...

TODO 6.2

bool Entity::GeneratePath(int x, int y, int lvl)
{
	iPoint goal = { 0,0 };

	origin = App->map->WorldToMap((position.x ), (position.y ));
	goal = { x,y };

	if (App->pathfinding->CreatePath(origin, goal, lvl, this) != PATH_TYPE::NO_TYPE)
	{
		path.clear();

		//TODO 6.2 (EXTRA): This is optional, but it's nice to call a single RequestPath() when you generate the path
		//----


		if (!path.empty())
			path.erase(path.begin());

		return true;
	}

	return false;
}
bool Entity::Move(float dt)
{
	//Speed is resetted to 0 each iteration
	// ----------------------------------------------------------------

	fPoint pathSpeed = { 0,0 };
	fPoint nextPoint = { 0,0 };


	//TODO 6.1 (EXTRA): With everything set up, we just have to call the RequestPath function and fill the entity's path
	// We can do this every frame, with a time condition, with a path legnth conditio...n; whatever fits your project!
	//----
	if (path.size() < 2)
	{
		App->pathfinding->RequestPath(this, &path);
	}
	//----


	if (path.size() > 0)
	{
	...

TODO 7



- Solution:

```cpp   
	//TODO 7 (EXTRA): If you want more abstraction levels, just uncomment this 
	// Just make sure the MAX_LEVELS define is above 1 & the TODO 4 is done correctly

	//Creates the rest of the graph levels
	for (int l = 2; l <= maxLevel; l++)
	{
		absGraph.CreateGraphLvl(l);
	}


Homework

Possible Improvements & Changes

Thanks

I can’t leave without thanking people that provide some help when making the research. I would like to thank Oscar Pérez for helping me with the RayCast functionallity, Jose Luís Redondo for helping me with the optiimization of the std containers for pathfinding & Marc Garrigó, for giving me support though all the process. And, also, thanks to you for sitting through all of this. If you have any trouble with anything I explained, you can find me on my GitHub, Linkedin or e-mail me to melenchonatona@gmail.com.

References and Extra Content