Detour Crowd vs Steering Behaviors

I fully implemented it.
That doesn’t mean I did it the best way since I tend to over code things. I did opt to use his math classes instead of jme.

I actually liked the way crowd worked so much I hacked my own version for all player movement so instead of having each char with their own movement control that calculates positions, I have StraightPathCrowd that uses straight path navigation with smoothed path navigation. That means when your char is moving, it slowly turns towards the next waypoint over several frames rather than just snapping directly towards the next one. You can easily switch between the two types by switching one method call.

I extend this class in a class that implements a jme control.
https://github.com/ppiastucki/recast4j/blob/master/detourcrowd/src/main/java/org/recast4j/detour/crowd/CrowdAgentParams.java#L23

How crowd works is when you create a crowd, it will prepopulate the crowd list with inactive agents using this class.

https://github.com/ppiastucki/recast4j/blob/master/detourcrowd/src/main/java/org/recast4j/detour/crowd/CrowdAgent.java#L41

When you add your npc/char to the crowd (that extends CrowdAgentParams), it will fill one of those inactive agent slots and set it to active and start updating it every frame based off the CrowdAgentParams.

When you add it to the Crowd, you will receive a index number which is your agent inside the crowd.

In your control, you can use a object to store the index of your agent and a reference to the Crowd so you can poll the agent to get information about it.

package mygame.detour.crowd;

import org.recast4j.detour.NavMeshQuery;
import org.recast4j.detour.crowd.Crowd;
import org.recast4j.detour.crowd.CrowdAgent;

/**
 *
 * @author mitm
 */
public class UserDataObj {
    private int idx;
    private Crowd crowd;
    private NavMeshQuery query;
    
    public UserDataObj() {
        this.idx = -1;
    }
    
    /**
     * @return the idx
     */
    public int getIdx() {
        return idx;
    }

    /**
     * @param idx the idx to set
     */
    public void setIdx(int idx) {
        this.idx = idx;
    }

    /**
     * @return the crowd
     */
    public Crowd getCrowd() {
        return crowd;
    }

    /**
     * @param crowd the crowd to set
     */
    public void setCrowd(Crowd crowd) {
        this.crowd = crowd;
    }
    
    public CrowdAgent getCrowdAgent() {
        return getCrowd().getAgent(idx);
    }

    /**
     * @return the query
     */
    public NavMeshQuery getQuery() {
        return query;
    }

    /**
     * @param query the query to set
     */
    public void setQuery(NavMeshQuery query) {
        this.query = query;
    }
    
}

If the idx = -1, it aint movin.

Then in your control all you do is check UserDataObj to see if the index is -1 or not, if it is, you are moving. you can then use your UserDataObj to query your npc’s CrowdAgent object inside the crowd.

Since the crowd is uncoupled from the jme engine, you don’t have to worry about enqueing any data from it. You only enque the data you need to start the process of navigation, ie. getting npc initial position for the Crowd target or setting npc direction or position once you have your data.

You are only really interested in a few things from CrowdAgent, the npos, and direction and if the agent is active. You can use those to set your npc next position and direction inside your control and to set the UserdataObj idx to -1 if the agent is inactive.

If you look at CrowdAgents public methods and variables, you have all the data you need for movement.

If you want your npc to stop moving, just remove it from the Crowd. If it reaches the target, it will be removed automatically.

I sent up some code in these threads,

I can give you this for an idea what I did but this is not game ready code and not what I use. It does not enque but does use the UserDataObj. Its full of crude code that I used for testing many different things so should not be used as anything other than an example of how to use the crowd imo. Its not documented either.

package mygame.detour.crowd;

import com.jme3.bounding.BoundingBox;
import com.jme3.export.JmeExporter;
import com.jme3.export.JmeImporter;
import com.jme3.math.Quaternion;
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.Control;
import java.io.IOException;
import mygame.controls.AnimationControl;
import mygame.controls.UserDataControl;
import mygame.enums.EnumPosType;
import mygame.interfaces.DataKey;
import mygame.interfaces.Pickable;
import static org.recast4j.detour.DetourCommon.vCopy;
import org.recast4j.detour.FindNearestPolyResult;
import org.recast4j.detour.QueryFilter;
import org.recast4j.detour.crowd.CrowdAgent;
import org.recast4j.detour.crowd.CrowdAgentParams;
/**
 *
 * @author mitm
 */
public class CrowdAgentControl extends CrowdAgentParams implements Control, Pickable {

    private Spatial spatial;
    private CrowdAgent ag;
    private final float[] target;
    private final float[] spatialPos;
    private final Vector3f up;

    public CrowdAgentControl(int updateFlags, int obstacleAvoidanceType) {
        this.target = new float[3];
        this.spatialPos = new float[3];
        this.up = new Vector3f(0, 1, 0);
        this.updateFlags = updateFlags;
        this.obstacleAvoidanceType = obstacleAvoidanceType;
        //Have to create empty object and use setters to set UserObjData after 
        //control is active due to private package use of crowd.
        this.userData = new UserDataObj();
    }

    @Override
    public Control cloneForSpatial(Spatial spatial) {
        try {
            CrowdAgentControl c = (CrowdAgentControl) clone();
            c.spatial = null; // to keep setSpatial() from throwing an exception
            c.setSpatial(spatial);
            return c;
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException("Can't clone control for spatial", e);
        }
    }

    @Override
    public void setSpatial(Spatial spatial) {
        if (this.spatial != null && spatial != null && spatial != this.spatial) {
            throw new IllegalStateException(
                    "This control has already been added to a Spatial");
        }
        this.spatial = spatial;
        if (spatial != null) {
            BoundingBox bounds = (BoundingBox) spatial.getWorldBound();
            this.height = bounds.getYExtent() * 2;
            this.radius = bounds.getXExtent();
            int position = this.getPositionType();
            for (EnumPosType pos : EnumPosType.values()) {
                if (pos.pos() == position) {
                    switch (pos) {
                        case POS_SWIMMING:
                            this.maxSpeed = EnumPosType.POS_SWIMMING.speed();
                            this.maxAcceleration = EnumPosType.POS_SWIMMING.speed();
                            break;
                        case POS_WALKING:
                            this.maxSpeed = EnumPosType.POS_WALKING.speed();
                            this.maxAcceleration = EnumPosType.POS_WALKING.speed();
                            break;
                        case POS_RUNNING:
                            this.maxSpeed = EnumPosType.POS_RUNNING.speed();
                            this.maxAcceleration = EnumPosType.POS_RUNNING.speed();
                            break;
                        default:
                            this.maxSpeed = 0f;
                            this.maxAcceleration = 0.0f;
                            break;
                    }
                }
            }
            this.collisionQueryRange = this.radius * 12f;
            this.pathOptimizationRange = this.radius * 30f;
//            this.separationWeight = 0f;
        }
    }
    float prev_dist = 0;
    @Override
    public void update(float tpf) {
        
        if (((UserDataObj)userData).getIdx() != -1) {
            ag = ((UserDataObj)userData).getCrowdAgent();
            
            if (ag.isActive()) {       
                vCopy(spatialPos, ag.npos);
                vCopy(target, ag.targetPos); 
                Vector2f aiPosition = new Vector2f(spatialPos[0], spatialPos[2]);  
                Vector2f waypoint2D = new Vector2f(target[0], target[2]); 
                float distance = aiPosition.distance(waypoint2D);
                
                if (distance > 1f) {
                    Vector3f spatPos = spatial.getWorldTranslation().clone();
                    Vector3f spatPos1 = new Vector3f(spatPos.x, 0, spatPos.z);
                    Vector3f tarPos = new Vector3f(spatialPos[0], 0, spatialPos[2]);
                    Vector3f direction = tarPos.subtract(spatPos1);
                    direction.normalize();
//                    Vector2f direction = waypoint2D.subtract(aiPosition); 
                    prev_dist = distance;
                    direction.mult(tpf);
//                    System.out.println("Distance: " + distance + " Direction: "
//                            + direction);
                    Quaternion q = new Quaternion();
                    q.lookAt(direction,up);
                    spatial.setLocalRotation(q);
                    if (spatial.getParent().getName().equals("offsetNode")) {
                        spatial.getParent().setLocalTranslation(new Vector3f(ag.npos[0], ag.npos[1], ag.npos[2]));
                    } else {
                        spatial.setLocalTranslation(new Vector3f(ag.npos[0], ag.npos[1], ag.npos[2]));
                    }
                    if (getAutorun() && getPositionType() != EnumPosType.POS_RUNNING.pos()) {
                        setPositionType(EnumPosType.POS_RUNNING.pos());
                        stopPlaying();
                    } else if (!getAutorun() && getPositionType() != EnumPosType.POS_WALKING.pos()) {
                        setPositionType(EnumPosType.POS_WALKING.pos());
                        stopPlaying();
                    }
                } else {
                    ((UserDataObj)userData).getCrowd().resetMoveTarget(ag.idx);
                    ((UserDataObj)userData).getCrowd().removeAgent(ag.idx);
                    ((UserDataObj)userData).setIdx(-1);
                    if (spatial.getParent().getName().equals("offsetNode")) {
                        spatial.getParent().setLocalTranslation(spatial.getUserData(DataKey.START_POSITION));
                    }
                    
                    if (canMove() && getPositionType() != EnumPosType.POS_STANDING.pos()) {
                        setPositionType(EnumPosType.POS_STANDING.pos());
                        stopPlaying();
                    }
                }
            }
        }

    }

    //gets the physical pos of spatial
    private int getPositionType() {
        return (int) spatial.getUserData(DataKey.POSITION_TYPE);
    }

    /**
     * @param updateFlags the updateFlags to set
     */
    public void setUpdateFlags(int updateFlags) {
        this.updateFlags = updateFlags;
    }

    /**
     * @param obstacleAvoidanceType the obstacleAvoidanceType to set
     */
    public void setObstacleAvoidanceType(int obstacleAvoidanceType) {
        this.obstacleAvoidanceType = obstacleAvoidanceType;
    }

    public CrowdAgentParams getAgentParams() {
        CrowdAgentParams ap = new CrowdAgentParams();
        ap.radius = this.radius;
        ap.height = this.height;
        int position = this.getPositionType();
        for (EnumPosType pos : EnumPosType.values()) {
            if (pos.pos() == position) {
                switch (pos) {
                    case POS_SWIMMING:
                        ap.maxSpeed = EnumPosType.POS_SWIMMING.speed();
                        ap.maxAcceleration = EnumPosType.POS_SWIMMING.speed();
                        break;
                    case POS_WALKING:
                        ap.maxSpeed = EnumPosType.POS_WALKING.speed();
                        ap.maxAcceleration = EnumPosType.POS_WALKING.speed();
                        break;
                    case POS_RUNNING:
                        ap.maxSpeed = EnumPosType.POS_RUNNING.speed();
                        ap.maxAcceleration = EnumPosType.POS_RUNNING.speed();
                        break;
                    default:
                        ap.maxSpeed = 0f;
                        ap.maxAcceleration = 0.0f;
                        break;
                }
            }
        }
        ap.collisionQueryRange = this.collisionQueryRange;
        ap.pathOptimizationRange = this.pathOptimizationRange;
        ap.updateFlags = this.updateFlags;
        ap.obstacleAvoidanceType = this.obstacleAvoidanceType;
        ap.separationWeight = this.separationWeight;
        ap.userData = this.userData;
        return ap;
    }

    @Override
    public void render(RenderManager rm, ViewPort vp) {
    }

    @Override
    public void write(JmeExporter ex) throws IOException {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }

    @Override
    public void read(JmeImporter im) throws IOException {
        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
    }
    
    //Returns true is AutRun is set to run
    private boolean getAutorun() {
        return (boolean) spatial.getUserData(DataKey.AUTORUN);
    }
    
    //Sets the Physical position of Spatial
    private void setPositionType(int position) {
        spatial.setUserData(DataKey.POSITION_TYPE, position);
    }

    //Stops the animation thats currently playing
    private void stopPlaying() {
        if (spatial.getControl(AnimationControl.class) != null) {
            spatial.getControl(AnimationControl.class).getAnimChannel()
                    .setTime(spatial.getControl(AnimationControl.class)
                            .getAnimChannel().getAnimMaxTime());
        }
    }
    
        //Returns true if the Spatial can move
    private 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;
    }

    @Override
    public void setTarget(Vector3f target) {
        float[] pos = new float[3];
        spatial.getWorldTranslation().toArray(pos);
        spatial.getControl(UserDataControl.class).setPositionType(EnumPosType.POS_RUNNING.pos());
        stopPlaying();
        int addAgent = ((UserDataObj)userData).getCrowd().addAgent(pos, getAgentParams());
        CrowdAgent ag = ((UserDataObj)userData).getCrowd().getAgent(addAgent);
        float[] extents = ((UserDataObj)userData).getCrowd().getQueryExtents();
        QueryFilter filter = ((UserDataObj)userData).getCrowd().getFilter(0);
        if (ag.isActive()) {
            FindNearestPolyResult nearest = ((UserDataObj)userData).getQuery().findNearestPoly(target.toArray(null), extents, filter);
            boolean requestMoveTarget = ((UserDataObj)userData).getCrowd().requestMoveTarget(addAgent, nearest.getNearestRef(), nearest.getNearestPos());
            if (requestMoveTarget) {
                ((UserDataObj)userData).setIdx(addAgent);
            }
        }
    }
    
}

Note that the UserDataControl mentioned is nothing more than a control that writes to jme spatial UserData. I found about half a dozen different ways to implement this stuff myself.

if you need more stuff just ask, I will dig out what I have.

edit: forgot to add the import statements.