I've been trying to implement A* for weeks so an enemy can chase a player in my game, and I can't get it to work. I've been working on it the entire weekend, and I even ended up scraping most of it and re writing it. I can draw a path from the starting location to the goal, but I can't trace it back, as in actually write down the path. I'm using Vector2f (ordered pair of floats) and Sprite from SFML but all the code there is pretty simple so you won't really need to understand it.
Edit: the problem is with Node.cameFrom. For some reason, it doesn't cout anything but the walls.
Here's Node.h
#ifndef NODE_H
#define NODE_H
#include <SFML/Graphics.hpp>
using namespace sf;
class Node {
public:
Vector2f pos;
// Distance traveled already to reach node
int level;
// Level + estimated dist to goal
int priority;
Node *cameFrom;
Node(Vector2f npos, int lv, Vector2f dest, Node *cf=nullptr);
bool operator == (const Node &nhs) const {
return nhs.priority == priority;
}
};
#endif // NODE_H
Node.cpp
#include "Node.h"
#include <SFML/Graphics.hpp>
#include <math.h>
#include <iostream>
using namespace std;
using namespace sf;
int estimatedDist(Vector2f pos, Vector2f dest) {
return abs(dest.x - pos.x) + abs(dest.y - pos.y);
}
Node::Node(Vector2f npos, int lv, Vector2f dest, Node *cf) {
cameFrom = cf;
level = lv;
pos = npos;
priority = level + estimatedDist(pos, dest);
}
Enemy.cpp pathfind functions
bool occupies(Vector2f pos, vector<Wall> walls) {
for (unsigned w = 0; w < walls.size(); w++) {
if (walls.at(w).collisionBox.getGlobalBounds().contains(pos.x * 32, pos.y * 32)) {
return true;
}
}
return false;
}
bool nFind(Node n, vector<Node> nodes) {
for (unsigned i = 0; i < nodes.size(); i++) {
if (nodes.at(i).pos == n.pos) {
return true;
}
}
return false;
}
void Enemy::pathFind(Vector2f dest, vector<Wall> walls) {
char fullMap[32][22];
vector<Node> openSet;
vector<Node> closedSet;
int xStart, yStart;
for (unsigned y = 0; y < 22; y++) {
for (unsigned x = 0; x < 32; x++) {
if (sprite.getGlobalBounds().top >= y * 32 && sprite.getGlobalBounds().top <= (y + 1) * 32) {
if (sprite.getGlobalBounds().left >= x * 32 && sprite.getGlobalBounds().left <= (x + 1) * 32) {
xStart = x;
yStart = y;
}
} if (occupies(Vector2f(x, y), walls)) {
fullMap[x][y] = '2';
} else {
fullMap[x][y] = ' ';
}
}
}
fullMap[int(dest.x)][int(dest.y)] = 'D';
Node *current = new Node(Vector2f(xStart, yStart), 0, dest);
fullMap[int(current->pos.x)][int(current->pos.y)] = '2';
openSet.push_back(*current);
while (openSet.size() > 0) {
sort(openSet.begin(), openSet.end(), sortByPriority());
*current = openSet.front();
if (current->pos == dest) {
cout << "We gots it ";
for (unsigned y = 0; y < 22; y++) {
for (unsigned x = 0; x < 32; x++) {
if (occupies(Vector2f(x, y), walls)) {
fullMap[x][y] = '2';
} else {
fullMap[x][y] = ' ';
}
}
}
while (current->cameFrom) {
fullMap[int(current->pos.x)][int(current->pos.y)] = 'P';
current = current->cameFrom;
for (unsigned y = 0; y < 22; y++) {
for (unsigned x = 0; x < 32; x++) {
cout << fullMap[x][y];
}
cout << endl;
}
cout << endl;
} for (unsigned y = 0; y < 22; y++) {
for (unsigned x = 0; x < 32; x++) {
cout << fullMap[x][y];
}
cout << endl;
}
cout << endl;
return;
}
openSet.erase(remove(openSet.begin(), openSet.end(), *current), openSet.end());
closedSet.push_back(*current);
fullMap[int(current->pos.x)][int(current->pos.y)] = '2';
vector<Node> neighbors;
neighbors.push_back(Node(Vector2f(current->pos.x - 1, current->pos.y - 1), current->level + 1, dest));
neighbors.push_back(Node(Vector2f(current->pos.x, current->pos.y - 1), current->level + 1, dest));
neighbors.push_back(Node(Vector2f(current->pos.x + 1, current->pos.y - 1), current->level + 1, dest));
neighbors.push_back(Node(Vector2f(current->pos.x + 1, current->pos.y), current->level + 1, dest));
neighbors.push_back(Node(Vector2f(current->pos.x + 1, current->pos.y + 1), current->level + 1, dest));
neighbors.push_back(Node(Vector2f(current->pos.x, current->pos.y + 1), current->level + 1, dest));
neighbors.push_back(Node(Vector2f(current->pos.x - 1, current->pos.y + 1), current->level + 1, dest));
neighbors.push_back(Node(Vector2f(current->pos.x - 1, current->pos.y), current->level + 1, dest));
for (unsigned i = 0; i < neighbors.size(); i++) {
if (nFind(neighbors.at(i), closedSet) ||
neighbors.at(i).pos.x > 22 ||
neighbors.at(i).pos.y > 32 ||
neighbors.at(i).pos.x < 0 ||
neighbors.at(i).pos.y < 0 ||
occupies(neighbors.at(i).pos, walls)) {
continue;
} if (!nFind(neighbors.at(i), openSet)) {
openSet.push_back(neighbors.at(i));
}
neighbors.at(i).cameFrom = current;
}
}
}
MCVE would help to try on our side (see zett42 comment).
So by just a quick look I can give you some pointers where to look during debugging, but no clear answer.
These lines looks highly suspicious:
Node *current = new Node(Vector2f(xStart, yStart), 0, dest);
// ^ no delete in source, will leak memory
*current = openSet.front();
// will overwrite the heap memory with copy constructor
// but the pointer will remain the same
// so all of your nodes will always have "cameFrom"
// pointing to this same memory.
Overall this code looks a bit complicated. Do you have the game with fixed square 32x22 tiles? Why "walls" vector then?
I would maintain only single global tile map as level (but the A* search then shouldn't damage it, rather create it's own copy for search, or rather to have new map with to-reach-costs, that would probably simplify the code a lot).
xStart, yStart
can be computed directly, no need to iterate it every loop:
xStart = int(sprite.getGlobalBounds().left)>>5; // left/32
yStart = int(sprite.getGlobalBounds().top)>>5; // top/32
The bool operator == (const Node &nhs) const
looks unhealthy, but it's not even used anywhere.
And to see if neighbour is in wall, you don't need to use the O(N) occupies
, just test the map for == '2'
? (I mean if the code is designed that way, I didn't verify it will work as expected if you change it right away in your code).
Overall I don't like that code, you can streamline that into shorter version, if you focus on what data you want to process and how, and stop moving objects back and forth through several lists. For A* IIRC you should need single sorted queue with insert_at to keep it sorted vs map field to mark which squares were already processed.
Are those Vector2f positions important, for example:
...
P#D
...
If player "P" stands in lower part of square ("#" is wall, "D" is destination), should the A* find the bottom path, or you need only "tile" accuracy and the upper path would be good too?
It's not clear to me from you question, whether you work with sub-tile accuracy or not, if not, then you can drop most of that Vector2f
stuff and work only in the tile coordinates.
With sub-tile accuracy you can probably still drop most of it, but if actually tile has "32" size, and player is for example only "3" wide, so he can use the tile as some kind of "area" and move across it by different lines, avoiding in example above to go to full centre of middle tile, saving distance... Then you need to calculate those sub-tile positions somehow in to get at least roughly accurate "shortest" path.
When I was working on one game, we had linked list of nodes (classic math graph) instead of tiles, each node had it's "area radius", and after the shortest node-to-node path was found, another reiterative algorithm did few loops to move from node positions to some shadow-node position which was within the radius, but was closer to the other two shadow-nodes. After hitting max iterations, or the shadow-positions didn't change much (usually it took 3-5 iterations at most), it stopped "smoothing" the path and returned it. This way soldiers were running across desert in almost straight lines, while actually the waypoint nodes were like sparse grid with 20m area radius, so the soldier was actually going only like 2-3 nodes away and starting/ending far away from node centre going almost zig-zag in the node graph itself.