Pathfinding can be done in many different ways, and in this recipe we'll look at how to use the NavMesh generated in the previous recipe for pathfinding. We'll use jMonkeyEngine's AI plugin, which has a pathfinder designed to navigate NavMeshes.
We achieve this using the Control pattern, and will also implement a way to generate paths in a thread-safe way separate from the main update thread, to not impact the performance of the application.
We'll need a scene with a NavMesh geometry in it. We also need to download the AI library plugin. Instructions on how to download a plugin in the SDK can be found in the Downloading the plugins section in Appendix, Information Fragments. The plugin is called jME3 AI Library
. Once we have downloaded the plugin, we need to add it to the project. Right-click on the project and select Properties, then select Libraries, and then select Add Library.... Select jME3 AI Library and click on Add Library.
We start by defining the class that will generate the paths for us. This part will be implemented by performing the following steps:
PathfinderThread
, which extends the Thread
class.Vector3f
called target
, a NavMeshPathfinder
called pathfinder
, and two Booleans, pathfinding
and running
, where running
should be set to true
by default.NavMesh
object as input, and we instantiate the pathfinder
with the same, as shown in the following code:public PathfinderThread(NavMesh navMesh) { pathfinder = new NavMeshPathfinder(navMesh); this.setDaemon(true); }
run
method to handle pathfinding
. While running is true
, the following logic should apply:if (target != null) { pathfinding = true; pathfinder.setPosition(getSpatial().getWorldTranslation()); boolean success = pathfinder.computePath(target); if (success) { target = null; } pathfinding = false; }
target
is not null
, we set pathfinding
to true
.pathfinder.setPosition(getSpatial().getWorldTranslation());
target
to null
.pathfinding
is set to false
.Thread.sleep(1000);
That's the first step of the pathfinding handling. Next, we'll define a class that will use this. This will be implemented by performing the following steps:
AbstractControl
called NavMeshNavigationControl
.PathfinderThread
called pathfinderThread
and a Vector3f
called waypointPosition
.NavMesh
from, and pass it on to pathfinderThread
, which is instantiated in the constructor as follows:public NavMeshNavigationControl(Node world) { Mesh mesh = ((Geometry) world.getChild("NavMesh")).getMesh(); NavMesh navMesh = new NavMesh(mesh); pathfinderThread = new PathfinderThread(navMesh); pathfinderThread.start(); }
public void moveTo(Vector3f target) { pathfinderThread.setTarget(target); }
controlUpdate
method is what does the bulk of the work.waypointPosition
is null
.waypointPosition
and the spatials worldTranslation
onto a 2D plane (by removing the y
value), to see how far apart they are as follows:Vector2f aiPosition = new Vector2f(spatialPosition.x, spatialPosition.z); Vector2f waypoint2D = new Vector2f(waypointPosition.x, waypointPosition.z); float distance = aiPosition.distance(waypoint2D);
1f
, we tell the spatial to move in the direction of the waypoint. This recipe uses the GameCharacterControl
class from Chapter 2, Cameras and Game Controls:if(distance > 1f){ Vector2f direction = waypoint2D.subtract(aiPosition); direction.mult(tpf); spatial.getControl(GameCharacterControl.class).setViewDirection(new Vector3f(direction.x, 0, direction.y).normalize()); spatial.getControl(GameCharacterControl.class).onAction("MoveForward", true, 1); }
1f
, we set waypointPosition
to null
.waypointPosition
is null, and there is another waypoint to get from the pathfinder, we tell the pathfinder to step to the next waypoint and apply its value to our waypointPosition
field as shown in the following code snippet:else if (!pathfinderThread.isPathfinding() && pathfinderThread.pathfinder.getNextWaypoint() != null && !pathfinderThread.pathfinder.isAtGoalWaypoint() ){ pathfinderThread.pathfinder.goToNextWaypoint(); waypointPosition = new Vector3f(pathfinderThread.pathfinder.getWaypointPosition()); }
The PathfinderThread
handles pathfinding. To do this in a thread-safe way, we use the pathfinding Boolean to let other threads know it's currently busy, so that they don't try to read from the pathfinder.
Target is the position the pathfinder should try to reach. This is set externally and will be used to indicate whether the thread should attempt to pathfind or not. This is why we set it to null once pathfinding is successful.
We keep the thread running all the time, to avoid having to initialize it every time. The thread will wake up once a second to see whether there is any pathfinding to perform. If the delay was not there, it would use up resources, unnecessarily.
This class uses the waypointPosition
field to store the current waypoint we're trying to reach. This is so that we don't need to look it up in the pathfinder every time, and thus risk interrupting an ongoing pathfinding. It also allows the AI to keep moving even if it's currently contemplating a new path.
The controlUpdate
method first checks whether the waypointPosition
is null
. Null indicates it has no current goal, and should go to the pathfinder to see whether there is a new waypoint for it.
It can only get a new waypoint if pathfinderThread
currently is not actively pathfinding
and if there is a next waypoint to get.
If it already has a waypointPosition
field, it will convert both the spatials position and the waypointPosition
to 2D and see how far apart they are. This is necessary as we can't guarantee that NavMesh
is exactly on the same plane as the spatial.
If it finds out that the distance is further than 1f
, it will find out the direction to the waypointPosition
field and tell the spatial to move in that direction. Otherwise (if it's close enough), it will set the waypointPosition
field to null
.
Once it has reached the final waypoint, it will tell the spatial to stop.
3.129.210.102