Hi.
I just wanted to share my fairly basic pathfinder that i have implemented in jME.
It's not really pre-designed, but has rather been modified to suit my needs for the moment, and just recently got ported to 3d.
This may not be the most clever way to implement A*, very little research has gone into it, but they work. Maybe they can help someone.
Pathfinder class:
The way it works is that it runs on a separate thread. AI's call for it when they need a path, and are put in a queue. The pathfinder runs for as long as there are AI's waiting in the queue and sends the finished path back when done. When the queue is empty, it sleeps the thread.
It also handles cases when the AI has several goals to choose from and wants to find the one with the shortest path.
Cost is purely based on distance. This example needs a Brain class that calls for it.
Grid generation:
These are just a number of methods which i keep in my GameWorld class, which contains my terrain block and objects.
The navgrid is generated by creating A* nodes all over the terrain at equal distances, in a hexagonal pattern.
There are ray's cast to check that the node is not too close to an object, and moving it if too close, but it's unverified whether that works atm.
Then it goes through all the nodes and look for neighbours within a certain distance, check that the neighbour is reachable (through a collision check, and slope check). It sends multiple rays to make sure there is a "tunnel" the actor can walk through, this may have to be adjusted due to your models bounds.
I have some more stuff i want to cram in there, but don't have time or need to do that atm.
A* Node:
I Extended Sphere because i was planning to use the radius and a collision check, but currently i just check the distance to it. Makes for a good visual representation, though.
The node keeps a list of neighbours, with a maximum of 6 nodes
This implementation may not suit everyone's purpose, but could be modified as you see fit.
Tell me if you find it useful.
Classes are attached due to size.
Here are the navgrid generation methods. These you can put wherever you have your TerrainBlock. The methods are a bit clunky, feel free to improve them. You can get visual representations by removing the comments that attach them to the main node of the class. I inverted so that only broken connections are shown, rather than working ones, due to performance.
buildPathNodes() takes the distance you want between the nodes as input.
public void buildPathNodes(int sliceX, int sliceY)
{
int detailX = (int) (terrain.getSize() * terrainScale.x / sliceX);
int detailY = (int) (terrain.getSize() * terrainScale.z / sliceY);
int offset = 0;
int minDistanceToObject = 10;
//this.attachChild(pathNodeGrid);
for (int y = 0; y < detailY; y++)
{
for (int x = 0; x < detailX; x++)
{
// create path nodes all over the terrain
AStarNode node = new AStarNode(new Vector3f(
sliceX * x + offset, terrain.getHeight(
sliceX * x + offset, sliceY * y) + 0.5f,
sliceY * y),
1);
node.setIndex((x+y)*y);
// try to move the node if too close to an object
Vector3f offsetVector = new Vector3f();
if (checkCollisions(
buildingNodes, node.getLocalTranslation(), node.getLocalTranslation().add(minDistanceToObject, 0, 0)))
offsetVector.add(-minDistanceToObject, 0, 0);
if (checkCollisions(
buildingNodes, node.getLocalTranslation(), node.getLocalTranslation().add(-minDistanceToObject, 0, 0)))
offsetVector.add(minDistanceToObject, 0, 0);
if (checkCollisions(
buildingNodes, node.getLocalTranslation(), node.getLocalTranslation().add(0, 0, minDistanceToObject)))
offsetVector.add(0, 0, -minDistanceToObject);
if (checkCollisions(
buildingNodes, node.getLocalTranslation(), node.getLocalTranslation().add(0, 0, -minDistanceToObject)))
offsetVector.add(0, 0, minDistanceToObject);
node.setLocalTranslation(
node.getLocalTranslation().x + offsetVector.x, terrain.getHeight(
node.getLocalTranslation().x + offsetVector.x, node.getLocalTranslation().z + offsetVector.z) + 0.5f, node.getLocalTranslation().z + offsetVector.z);
pathNodeGrid.attachChild(node);
}
// offset to get a hexagonal grid
if (offset == 0) offset = sliceX / 2;
else offset = 0;
}
}
public void generateNavGrid()
{
Node connectionGrid = new Node("Failed Connections");
this.attachChild(connectionGrid);
connectionGrid.setCullHint(CullHint.Dynamic);
int maxDistance = (int) (terrain.getSize() * terrainScale.x / 32)+6;
int maxAngle = 25;
// parse through all nodes to check for neighbouring nodes
for (int i = 0; i < pathNodeGrid.getQuantity(); i++)
{
AStarNode currentNode = (AStarNode) pathNodeGrid.getChild(i); // grab the next node in the list
for (int j = 0; j < pathNodeGrid.getQuantity()-1; j++)
{
// parse through all other nodes
AStarNode nodeToCheck = (AStarNode) pathNodeGrid.getChild(j);
if (nodeToCheck != currentNode) // don't want to check the current node;
{
if (nodeToCheck.getLocalTranslation().distance(currentNode.getLocalTranslation()) < maxDistance) // if distance between nodes < 5
{
// normalize the vectors and find out the difference between them
Vector3f v = nodeToCheck.getLocalTranslation().subtract(currentNode.getLocalTranslation()).normalize();
float angle = FastMath.RAD_TO_DEG*(v.normalize().y); // normalize resulting vector and check the y-vector to find out the slope
if (angle > -maxAngle && angle < maxAngle) // check that it's not too steep
{
currentNode.lookAt(nodeToCheck.getLocalTranslation(), upAxis);
// do internal checks, makes sure that the connection list is not over crowded, and that the two nodes are not already connected
if (currentNode.checkIfConnectionAllowed(nodeToCheck)
&& nodeToCheck.checkIfConnectionAllowed(currentNode)
&& checkCollisions(buildingNodes, currentNode.getLocalTranslation(), nodeToCheck.getLocalTranslation()))
{
currentNode.addConnection(nodeToCheck);
nodeToCheck.addConnection(currentNode);
//connectionGrid.attachChild(new Line(currentNode.getName() + "<->" + nodeToCheck.getName(), new Vector3f []{currentNode.getLocalTranslation(), nodeToCheck.getLocalTranslation()}, null, debugColor, null));
}
// debug line to see failed connections
else if (!checkCollisions(buildingNodes, currentNode.getLocalTranslation(), nodeToCheck.getLocalTranslation()))
{
connectionGrid.attachChild(new Line(currentNode.getName() + "<->" + nodeToCheck.getName(), new Vector3f []{currentNode.getLocalTranslation(), nodeToCheck.getLocalTranslation()}, null, debugColor, null));
}
// TODO: check if this node is closer than an existing one and replace it
}
}
}
}
// TODO: increase range if no node is found
if (currentNode.getConnectedTo().size() == 0) pathNodeGrid.detachChild(currentNode);
}
connectionGrid.updateRenderState();
}
Here are a few convenience methods that goes along with the nav grid generation:
public AStarNode getClosestNode(Vector3f loc)
{
AStarNode closestNode = null;
float distance = 200;
for (int i = 0; i < pathNodeGrid.getQuantity(); i++)
{
AStarNode currentNode = (AStarNode) pathNodeGrid.getChild(i);
if (currentNode.getLocalTranslation().distance(loc) < distance)
{
closestNode = currentNode;
distance = currentNode.getLocalTranslation().distance(loc);
}
}
return closestNode;
}
public boolean checkCollisions(Node collisionNodes, Vector3f node1, Vector3f node2)
{
float distance = node1.distance(node2);
Vector3f direction = node2.subtract(node1).normalize();
Vector3f offsetVector = new Vector3f();
CollisionDetection collision = new CollisionDetection(null, collisionNodes);
Ray testRay;
for (int x = -1; x < 2; x++)
{
for(int y = 0; y < 3; y++)
{
offsetVector.set(direction.x * x*4, y*2, direction.z * x*4);
testRay = new Ray(node1.add(offsetVector), direction);
if (collision.ProcessBoundCollisions(testRay, (int) distance)) return false;
}
}
return true;
}