These classes are from my personal testing implementation of recast and there are better ways to accomplish something similar. I re-wrote recast4j crowd to use it in a jme environment after this implementation so I could learn it better and its more robust than this implementation. Maybe it will get you on your way to understanding how to use it.
jme3-recast4j is much better implementation as this is all just experimental crap to see if I could get recast working.
Not sure how any of the new changes Piotr has added to recast4j will affect this.
Its likely I will miss something but here goes.
DetourMoveControl calculates the path and sets positions and passes waypoints to PCControl which does the movement.
Animation control just looks for a position change and sets the animation accordingly.
DetourMoveControl.java
package mygame.recast;
import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.cinematic.MotionPath;
import com.jme3.export.InputCapsule;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule;
import com.jme3.math.Spline;
import com.jme3.math.Vector2f;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.AbstractControl;
import com.jme3.scene.control.Control;
import mygame.enums.EnumPosType;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import mygame.controls.AnimationControl;
import mygame.controls.PCControl;
import mygame.interfaces.DataKey;
import mygame.interfaces.ListenerKey;
import mygame.interfaces.Pickable;
import org.recast4j.detour.DefaultQueryFilter;
import org.recast4j.detour.FindNearestPolyResult;
import org.recast4j.detour.NavMesh;
import org.recast4j.detour.NavMeshQuery;
import org.recast4j.detour.QueryFilter;
import org.recast4j.detour.Result;
import org.recast4j.detour.StraightPathItem;
/**
*
* @author mitm
*/
public class DetourMoveControl extends AbstractControl implements Pickable {
private ScheduledExecutorService executor;
private static final Logger LOGGER = Logger.getLogger(DetourMoveControl.class.getName());
private Vector3f target, wayPosition, nextWaypoint;
private boolean finding, showPath;
private SimpleApplication app;
private List<StraightPathItem> straightPath;
private List<Vector3f> wayPoints;
private MotionPath motionPath;
private DetourMoveControl() {
}
public DetourMoveControl(Application app) {
this.app = (SimpleApplication) app;
wayPoints = new ArrayList<>();
motionPath = new MotionPath();
motionPath.setPathSplineType(Spline.SplineType.Linear);
NavMesh recastNavMesh = getNavMesh();
executor = Executors.newScheduledThreadPool(1);
startRecastQuery(recastNavMesh);
}
@Override
public void setSpatial(Spatial spatial) {
super.setSpatial(spatial);
if (spatial == null) {
shutdownAndAwaitTermination(executor);
}
}
private void shutdownAndAwaitTermination(ExecutorService pool) {
pool.shutdown(); // Disable new tasks from being submitted
try {
// Wait a while for existing tasks to terminate
if (!pool.awaitTermination(6, TimeUnit.SECONDS)) {
pool.shutdownNow(); // Cancel currently executing tasks
// Wait a while for tasks to respond to being cancelled
if (!pool.awaitTermination(6, TimeUnit.SECONDS)) {
LOGGER.log(Level.SEVERE, "Pool did not terminate {0}", pool);
}
}
} catch (InterruptedException ie) {
// (Re-)Cancel if current thread also interrupted
pool.shutdownNow();
// Preserve interrupt status
Thread.currentThread().interrupt();
}
}
@Override
protected void controlUpdate(float tpf) {
if (getWayPosition() != null) {
Vector3f spatialPosition = spatial.getWorldTranslation();
Vector2f aiPosition = new Vector2f(spatialPosition.x, spatialPosition.z);
Vector2f waypoint2D = new Vector2f(getWayPosition().x, getWayPosition().z);
float distance = aiPosition.distance(waypoint2D);
if (distance > 1f) {
Vector2f direction = waypoint2D.subtract(aiPosition);
direction.mult(tpf);
getPCControl().setViewDirection(new Vector3f(direction.x, 0, direction.y).normalize());
getPCControl().onAction(ListenerKey.MOVE_FORWARD, true, 1);
} else {
setWayPosition(null);
}
} else if (!this.isPathfinding() && getNextWaypoint() != null && !isAtGoalWaypoint()) {
//must be called from the update loop
if (showPath) {
showPath();
showPath = false;
}
goToNextWaypoint();
setWayPosition(new Vector3f(getNextWaypoint()));
if (getAutorun() && getPositionType() != EnumPosType.POS_RUNNING.pos()) {
setPosition(EnumPosType.POS_RUNNING.pos());
stopPlaying();
} else if (!getAutorun() && getPositionType() != EnumPosType.POS_WALKING.pos()) {
setPosition(EnumPosType.POS_WALKING.pos());
stopPlaying();
}
// System.out.println("Next wayPosition = " + getWayPosition() + " SpatialWorldPosition " + spatialPosition);
} else {
if (canMove() && getPositionType() != EnumPosType.POS_STANDING.pos()) {
setPosition(EnumPosType.POS_STANDING.pos());
stopPlaying();
}
getPCControl().onAction(ListenerKey.MOVE_FORWARD, false, 1);
}
}
@Override
protected void controlRender(RenderManager rm, ViewPort vp) {
//Only needed for rendering-related operations,
//not called when spatial is culled.
}
@Override
public Control cloneForSpatial(Spatial spatial) {
DetourMoveControl control = new DetourMoveControl(app);
control.setSpatial(spatial);
return control;
}
@Override
public void read(JmeImporter im) throws IOException {
super.read(im);
InputCapsule in = im.getCapsule(this);
//TODO: load properties of this Control, e.g.
//this.value = in.readFloat("name", defaultValue);
}
@Override
public void write(JmeExporter ex) throws IOException {
super.write(ex);
OutputCapsule out = ex.getCapsule(this);
//TODO: save properties of this Control, e.g.
//out.write(this.value, "name", defaultValue);
}
private void startRecastQuery(org.recast4j.detour.NavMesh navMesh) {
NavMeshQuery query = new NavMeshQuery(navMesh);
executor.scheduleWithFixedDelay(() -> {
if (target != null) {
finding = true;
clearPath();
QueryFilter filter = new DefaultQueryFilter();
Vector3f spatialPos = getSpatial().getWorldTranslation();
float[] extents = {2, 4, 2};
boolean success;
float[] startArray = new float[3];
spatialPos.toArray(startArray);
float[] endArray = new float[3];
target.toArray(endArray);
FindNearestPolyResult startPos = query.findNearestPoly(startArray, extents, filter);
FindNearestPolyResult endPos = query.findNearestPoly(endArray, extents, filter);
if (startPos.getNearestRef() == 0 || endPos.getNearestRef() == 0) {
success = false;
} else {
Result<List<Long>> path = query.findPath(startPos.getNearestRef(), endPos.getNearestRef(), startPos.getNearestPos(), endPos.getNearestPos(), filter);
straightPath = query.findStraightPath(startPos.getNearestPos(), endPos.getNearestPos(), path.result, Integer.MAX_VALUE, 0).result;
for (int i = 0; i < straightPath.size(); i++) {
float[] pos = straightPath.get(i).getPos();
Vector3f vector = new Vector3f(pos[0], pos[1], pos[2]);
wayPoints.add(vector);
}
nextWaypoint = this.getFirst();
success = true;
}
System.out.println("RECAST SUCCESS " + success);
if (success) {
target = null;
showPath = true;
}
finding = false;
}
}, 0, 500, TimeUnit.MILLISECONDS);
}
private void showPath() {
if (motionPath.getNbWayPoints() > 0) {
motionPath.clearWayPoints();
motionPath.disableDebugShape();
}
for (Vector3f wp : getWaypoints()) {
motionPath.addWayPoint(wp);
}
motionPath.enableDebugShape(this.app.getAssetManager(), this.app.getRootNode());
}
public boolean canMove() {
int position = getPositionType();
boolean move = true;
for (EnumPosType pos : EnumPosType.values()) {
if (pos.pos() == position) {
switch (pos) {
case POS_DEAD:
case POS_MORTAL:
case POS_INCAP:
case POS_STUNNED:
case POS_TPOSE:
move = false;
break;
}
}
}
return move;
}
/**
* @param target the target to set
*/
@Override
public void setTarget(Vector3f target) {
this.target = target;
}
/**
* @return the pathfinding
*/
public boolean isPathfinding() {
return finding;
}
/**
* @return the wayPosition
*/
public Vector3f getWayPosition() {
return wayPosition;
}
/**
* @param wayPosition the wayPosition to set
*/
public void setWayPosition(Vector3f wayPosition) {
this.wayPosition = wayPosition;
}
/**
* @return the straightPath
*/
public List<StraightPathItem> getStraightPath() {
return straightPath;
}
/**
* @return the wayPoints
*/
public List<Vector3f> getWaypoints() {
return wayPoints;
}
public void goToNextWaypoint() {
int from = getWaypoints().indexOf(nextWaypoint);
nextWaypoint = getWaypoints().get(from + 1);
}
public Vector3f getNextWaypoint() {
return nextWaypoint;
}
public Vector3f getFirst() {
return wayPoints.get(0);
}
public Vector3f getLast() {
return wayPoints.get(wayPoints.size() - 1);
}
public boolean isAtGoalWaypoint() {
return nextWaypoint == this.getLast();
}
public void clearPath() {
wayPoints.clear();
nextWaypoint = null;
setWayPosition(null);
}
/**
* @return the motionPath
*/
public MotionPath getMotionPath() {
return motionPath;
}
private void stopPlaying() {
spatial.getControl(AnimationControl.class).getAnimChannel()
.setTime(spatial.getControl(AnimationControl.class)
.getAnimChannel().getAnimMaxTime());
}
private PCControl getPCControl() {
return spatial.getControl(PCControl.class);
}
private boolean getAutorun() {
return (Boolean) spatial.getUserData(DataKey.AUTORUN);
}
private int getPositionType() {
return (Integer) spatial.getUserData(DataKey.POSITION_TYPE);
}
private void setPosition(int position) {
spatial.setUserData(DataKey.POSITION_TYPE, position);
}
private NavMesh getNavMesh() {
return app.getStateManager().getState(RecastMeshGenState.class).getNavMesh();
}
}
PCControl.java
package mygame.controls;
import com.jme3.bullet.collision.shapes.CollisionShape;
import com.jme3.bullet.collision.shapes.CompoundCollisionShape;
import com.jme3.bullet.collision.shapes.CylinderCollisionShape;
import com.jme3.bullet.collision.shapes.SphereCollisionShape;
import com.jme3.bullet.control.BetterCharacterControl;
import com.jme3.input.controls.ActionListener;
import com.jme3.math.Vector3f;
import com.jme3.scene.Spatial;
import mygame.enums.EnumPosType;
import mygame.interfaces.DataKey;
import mygame.interfaces.ListenerKey;
/**
* Controls the spatials movement. Speed is derived from EnumPosType.
*
* @author mitm
*/
public class PCControl extends BetterCharacterControl implements ActionListener {
private boolean forward;
private float moveSpeed;
private int position;
public PCControl(float radius, float height, float mass) {
super(radius, height, mass);
}
@Override
public void update(float tpf) {
super.update(tpf);
this.moveSpeed = 0;
Vector3f modelForwardDir = spatial.getWorldRotation().mult(Vector3f.UNIT_Z);
walkDirection.set(0, 0, 0);
if (forward) {
position = getPositionType();
for (EnumPosType pos : EnumPosType.values()) {
if (pos.pos() == position) {
switch (pos) {
case POS_SWIMMING:
moveSpeed = EnumPosType.POS_SWIMMING.speed();
break;
case POS_WALKING:
moveSpeed = EnumPosType.POS_WALKING.speed();
break;
case POS_RUNNING:
moveSpeed = EnumPosType.POS_RUNNING.speed();
break;
default:
moveSpeed = 0f;
break;
}
}
}
// if (this.rigidBody.getLinearVelocity().length() > this.getMoveSpeed()) {
// System.out.println("Velocity = " + this.rigidBody.getLinearVelocity().length());
// }
walkDirection.addLocal(modelForwardDir.mult(moveSpeed));
}
setWalkDirection(walkDirection);
}
@Override
public void onAction(String name, boolean isPressed, float tpf) {
if (name.equals(ListenerKey.MOVE_FORWARD)) {
forward = isPressed;
}
if (name.equals((ListenerKey.JUMP))) {
jump();
}
}
//Override default collisionshape due to .7 offset.
@Override
protected CollisionShape getShape() {
@SuppressWarnings("LocalVariableHidesMemberVariable")
float radius = getFinalRadius();
@SuppressWarnings("LocalVariableHidesMemberVariable")
float height = getFinalHeight();
float cylinder_height = height - (2.0f * radius);
CylinderCollisionShape cylinder = new CylinderCollisionShape(
new Vector3f(radius, cylinder_height / 2f, radius)/*NB constructor want half extents*/, 1);
SphereCollisionShape sphere = new SphereCollisionShape(getFinalRadius());
CompoundCollisionShape compoundCollisionShape = new CompoundCollisionShape();
compoundCollisionShape.addChildShape(sphere,
new Vector3f(0,/*sphere half height*/ radius, 0)); // bottom sphere
compoundCollisionShape.addChildShape(cylinder,
new Vector3f(0,/*half sphere height*/ (radius) +/*cylinder half height*/ (cylinder_height / 2.f), 0)); // cylinder, on top of the bottom sphere
compoundCollisionShape.addChildShape(sphere,
new Vector3f(0,/*half sphere height*/ (radius) +/*cylinder height*/ (cylinder_height), 0)); // top sphere
return compoundCollisionShape;
}
//need to overide because we extended BetterCharacterControl
@Override
public PCControl cloneForSpatial(Spatial spatial) {
try {
PCControl control = (PCControl) super.clone();
control.setSpatial(spatial);
return control;
} catch (CloneNotSupportedException ex) {
throw new RuntimeException("Clone Not Supported", ex);
}
}
//need to override because we extended BetterCharacterControl
@Override
public PCControl jmeClone() {
try {
return (PCControl) super.clone();
} catch (CloneNotSupportedException ex) {
throw new RuntimeException("Clone Not Supported", ex);
}
}
//gets the physical pos of spatial
private int getPositionType() {
return (int) spatial.getUserData(DataKey.POSITION_TYPE);
}
}
AnimationControl.java
package mygame.controls;
import com.jme3.animation.AnimChannel;
import com.jme3.animation.AnimControl;
import com.jme3.animation.AnimEventListener;
import com.jme3.animation.LoopMode;
import com.jme3.animation.SkeletonControl;
import com.jme3.export.InputCapsule;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.export.OutputCapsule;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Node;
import com.jme3.scene.SceneGraphVisitorAdapter;
import com.jme3.scene.Spatial;
import com.jme3.scene.control.AbstractControl;
import com.jme3.scene.control.Control;
import mygame.enums.EnumPosType;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import mygame.interfaces.AnimKey;
import mygame.interfaces.DataKey;
/**
* Implements all animations of a spatial by reading the spatials physical
* pos. Spatial must have AnimControl to use this control.
*
* @author mitm
*/
public class AnimationControl extends AbstractControl {
private AnimChannel animChannel;
private AnimControl animControl;
private SkeletonControl skeletonControl;
private static final Logger LOG = Logger.getLogger(AnimationControl.class.getName());
private int posType;
public AnimationControl() {
}
@Override
public void setSpatial(Spatial spatial) {
super.setSpatial(spatial);
if (spatial == null) {
return;
}
spatial.depthFirstTraversal(new SceneGraphVisitorAdapter() {
@Override
public void visit(Node node) {
if (node.getControl(AnimControl.class) != null) {
animControl = node.getControl(AnimControl.class);
animControl.addListener(new AnimationEventListener());
animChannel = animControl.createChannel();
}
if (node.getControl(SkeletonControl.class) != null) {
skeletonControl = node.getControl(SkeletonControl.class);
}
}
});
//no animControl so bail
if (animControl == null) {
LOG.log(Level.SEVERE, "No AnimControl {0}", spatial);
throw new RuntimeException();
}
//no SkeletonControl so bail
if (skeletonControl == null) {
LOG.log(Level.SEVERE, "No SkeletonControl {0}", spatial);
throw new RuntimeException();
}
posType = getPosType();
for (EnumPosType pos : EnumPosType.values()) {
if (pos.pos() == posType) {
switch (pos) {
case POS_STANDING:
animChannel.setAnim(AnimKey.IDLE);
animChannel.setLoopMode(LoopMode.Loop);
//channel.setSpeed(1f);
break;
case POS_WALKING:
animChannel.setAnim(AnimKey.WALK);
animChannel.setLoopMode(LoopMode.Loop);
//channel.setSpeed(1f);
break;
case POS_RUNNING:
animChannel.setAnim(AnimKey.RUN);
animChannel.setLoopMode(LoopMode.Loop);
//channel.setSpeed(1f);
break;
default:
animChannel.setAnim(AnimKey.TPOSE);
animChannel.setLoopMode(LoopMode.DontLoop);
//channel.setSpeed(1f);
break;
}
}
}
}
@Override
protected void controlUpdate(float tpf) {
}
@Override
protected void controlRender(RenderManager rm, ViewPort vp) {
//Only needed for rendering-related operations,
//not called when spatial is culled.
}
@Override
public Control cloneForSpatial(Spatial spatial) {
AnimationControl control = new AnimationControl();
control.setSpatial(spatial);
return control;
}
@Override
public void read(JmeImporter im) throws IOException {
super.read(im);
InputCapsule in = im.getCapsule(this);
//TODO: load properties of this Control, e.g.
//this.value = in.readFloat("name", defaultValue);
}
@Override
public void write(JmeExporter ex) throws IOException {
super.write(ex);
OutputCapsule out = ex.getCapsule(this);
//TODO: save properties of this Control, e.g.
//out.write(this.value, "name", defaultValue);
}
//Checks spatial physical pos whenver an animation ends. Sets animation
//based off that pos.
private class AnimationEventListener implements AnimEventListener {
@Override
public void onAnimCycleDone(AnimControl control, AnimChannel channel, String animName) {
//position is set by MovementControl after game start
posType = getPosType();
//One animation must run otherwise default to TPose to show theres a
//problem. Has to be an int, boolean, string, float, array pos
//because it's stored in userData.
for (EnumPosType pos : EnumPosType.values()) {
if (pos.pos() == posType) {
switch (pos) {
case POS_STANDING:
channel.setAnim(AnimKey.IDLE);
channel.setLoopMode(LoopMode.Loop);
//channel.setSpeed(1f);
break;
case POS_SWIMMING:
case POS_WALKING:
channel.setAnim(AnimKey.WALK);
channel.setLoopMode(LoopMode.Loop);
//channel.setSpeed(1f);
break;
case POS_RUNNING:
channel.setAnim(AnimKey.RUN);
channel.setLoopMode(LoopMode.Loop);
//channel.setSpeed(1f);
break;
default:
channel.setAnim(AnimKey.TPOSE);
channel.setLoopMode(LoopMode.DontLoop);
//channel.setSpeed(1f);
break;
}
}
}
}
@Override
public void onAnimChange(AnimControl control, AnimChannel channel, String animName) {
}
}
/**
* @return the AnimChannel
*/
public AnimChannel getAnimChannel() {
return animChannel;
}
/**
*
* @return spatials physical pos
*/
private int getPosType() {
return (Integer) spatial.getUserData(DataKey.POSITION_TYPE);
}
}