Pathfinding – using NavMesh

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.

Getting ready

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.

How to do it...

We start by defining the class that will generate the paths for us. This part will be implemented by performing the following steps:

  1. We create a new class called PathfinderThread, which extends the Thread class.
  2. It needs a couple of fields, a Vector3f called target, a NavMeshPathfinder called pathfinder, and two Booleans, pathfinding and running, where running should be set to true by default.
  3. The constructor should take a 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);
    }
  4. We override the 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;
    }
  5. If target is not null, we set pathfinding to true.
  6. Then we set the start position of the pathfinder to the AI's current position, as shown in the following code:
    pathfinder.setPosition(getSpatial().getWorldTranslation());
  7. If the pathfinder can find a path, we set target to null.
  8. In either case, pathfinding is done, and pathfinding is set to false.
  9. Finally, we tell the thread to sleep for one second until trying again, as shown in the following code:
    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:

  1. We create a new class that extends AbstractControl called NavMeshNavigationControl.
  2. It needs two fields, a PathfinderThread called pathfinderThread and a Vector3f called waypointPosition.
  3. Its constructor should take a node as input, and we use this to extract a 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();
    }
  4. Now, we create a method to pass a position it should pathfind to using the following code:
    public void moveTo(Vector3f target) {
      pathfinderThread.setTarget(target);
    }
  5. The controlUpdate method is what does the bulk of the work.
  6. We start by checking whether waypointPosition is null.
  7. If it is not null, we project 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);
  8. If the distance is more than 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);
    }
  9. If the distance is less than 1f, we set waypointPosition to null.
  10. If 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());
    }

How it works...

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.129.210.102