Detour Crowd vs Steering Behaviors

Hey Guys,
I’ve come to the point where I want my NPCs to be aware of each other, so they don’t collide. In addition to that I want them to move in Groups potentially.

Now I have two/three choices. One is to use Detour Crowd, which is a pretty good solution despite being a native library, because it is already built around the path finder and probably only requires configuration.
The downsite is that I expect issues to lie within the flexibility.

On the other hand I could use jme3-ai or an own, entity based implementation and this help. While it might be more work, it would be pathfinding algorithm independent, could easily be synchronized using entity components (is this even a good idea or should the behaviors just be deterministic and run on each computer? But then I’d need a locked framerate or rounding errors might drift of?).

TLDR: Steering Behaviors - Reinvent the Wheel?

1 Like

If it helps, we are using LibGDX-AI for this, plus pspeed’s ES for the entities. And it wasn’t much work. Really what it calculates is forces to apply and you can either update the position straight or apply the force if using physics all around.

LibGDX-AI’s steering is hard to master. Easy to implement, but tinkering with all the parameters and getting all exactly right is little bit harder. Or I’m just dumper than the library requires.

You can see in our videos that currently the movement follows kinda bézier path. And I did implement some group behaviors like collision avoidance. But the results were not very satisfactory. The examples make it look easy though. So maybe I just missed something.

3 Likes

Yeah that’s what I fear as well, that it’s kinda hard. I looked into detour crowd within the last minutes and it doesn’t seem to include real formation at all, just somehow a fancy interface for a flock/herd behavior where it just applys a collision avoidance behavior for each other. libgdx explains in detail how you can have dynamic slots and all. AI done right is complex :smiley:

But hey… since we are opensource. We already have this all… You can try with our product and fix our steering while learning :wink:

1 Like

Any reason why you are not into jme MonkeyBrains steering ?

Edit:
In case you are interested how I am integrating it with es and bullet:

It’s based on bullet-char demo

I just created my own driver called CharSteerDriver which gets a CompoundSteeringBehavior which can contain multiple steer behaviors.

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package com.otm.moona.ai.state.steer;

import com.jme3.ai.agents.behaviors.npc.steering.CompoundSteeringBehavior;
import com.jme3.bullet.collision.PhysicsCollisionEvent;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.otm.moona.ai.EntityAgent;
import com.otm.moona.physics.*;
import com.simsilica.es.Entity;
import com.simsilica.mathd.Grid;
import com.simsilica.sim.SimTime;

/**
 *
 * @author ali
 */
public class CharSteerDriver implements ControlDriver {
    
    private Vector3f vTemp = new Vector3f();
    private Quaternion qTemp = new Quaternion();
    
    private final Entity entity;
    private EntityRigidBody body;
    private CharPhysics charPhysics;
    private Vector3f groundVelocity = new Vector3f();
    private float walkSpeed = 3;
    private float rotationSpeed = FastMath.TWO_PI;
    private float groundImpulse = 300;
    private CompoundSteeringBehavior behavior;
    private Vector3f desiredVelocity = new Vector3f();
    private Vector3f force = new Vector3f();
    private final Grid grid;
    private EntityAgent agent;
    
    private float[] angles = new float[3];
    
    public CharSteerDriver(Entity entity, CharPhysics charPhysics, CompoundSteeringBehavior behavior, Grid grid) {
        this.entity = entity;
        setCharPhysics(charPhysics);
        this.behavior = behavior;
        this.grid = grid;
    }
    
    public void setCharPhysics(CharPhysics charPhysics) {
        this.charPhysics = charPhysics;
        if (body != null) {
            // Make sure gravity is current
            body.setGravity(charPhysics.gravity);
        }
//        this.groundImpulse = charPhysics.groundImpulse;
//        this.airImpulse = charPhysics.airImpulse;
//        this.jumpForce = charPhysics.jumpForce;
//        this.shortJumps = charPhysics.shortJumps;
//        this.autoBounce = charPhysics.autoBounce;
    }
    
    public Vector3f getPhysicsLocation(Vector3f trans) {
        return body.getPhysicsLocation(trans);
    }
    
    public Quaternion getPhysicsRotation(Quaternion rot) {
        return body.getPhysicsRotation(rot);
    }
    
    @Override
    public void initialize(EntityRigidBody body) {
        this.body = body;
        body.setGravity(charPhysics.gravity);
        this.agent = (EntityAgent) behavior.getAgent();
        this.agent.setRadius((float) body.getBounds().getMax().subtract(body.getBounds().getMin()).length() / 2);
        this.agent.setMaxMoveSpeed(1);
        this.agent.setRotationSpeed(rotationSpeed);
        this.agent.setMass(1);
        
    }
    
    @Override
    public void update(SimTime time, EntityRigidBody body) {
       
        body.getPhysicsLocation(vTemp);
        agent.setWorldTranslation(vTemp);
        body.getPhysicsRotation(qTemp);
        agent.setWorldRotation(qTemp);
        //agent.updateCell(grid);

        
        behavior.updateAI((float) time.getTpf());

        //body.getPhysicsRotation(qTemp);
        body.getAngularVelocity(vTemp);
        
        // Kill any non-yaw orientation
        /*qTemp.toAngles(angles);
        if( angles[0] != 0 || angles[2] != 0 ) {
            angles[0] = 0;
            angles[2] = 0;
            body.setPhysicsRotation(qTemp.fromAngles(angles));
        }*/

       // Kill any non-yaw rotation
        if( vTemp.x != 0 && vTemp.z != 0 ) {
            vTemp.x = 0;
            vTemp.y *= 0.95f; // Let's see if we can dampen the spinning
            vTemp.z = 0;
            body.setAngularVelocity(vTemp);
        }
        
       

        //body.setPhysicsRotation(agent.getPredictedRotation());
        desiredVelocity.set(agent.getVelocity()).normalizeLocal().multLocal(walkSpeed);
        
        // See how much our velocity has to change to reach the
        // desired velocity
        body.getLinearVelocity(vTemp);

        // Calculate a force that will either break or accelerate in the
        // appropriate direction to achieve the desired velocity
        force.set(desiredVelocity).subtractLocal(vTemp);
        force.y = 0;
        
        body.applyCentralForce(force.multLocal(groundImpulse));
    }
    
    @Override
    public void terminate(EntityRigidBody body) {
        
    }
    
    @Override
    public void addCollision(EntityPhysicsObject otherBody, PhysicsCollisionEvent event) {
        
    }
}

then fed it to BulletSystem

This is exactly what I see when using it. The difference in performance when compared to what @tonihele shows is where his npc will clip through walls and move more “kinda bézier”, crowd will not clip. It will stick to the navigation mesh. however, when transitioning from open areas through narrower corridors or doors it will tend to “bounce” the npc from one side of the mesh to the other, in ever narrowing arcs until it succeeds in traversing to door.

When there is a lot of npc targeting the same point, they will start using a “circle the drain” type movement until one of them breaks free to the target and is removed. The circling will continue like this unless you remove npc to break the pattern. You can see crowd trying to do this on its own by the movement of the npc, where they will “step back” and restart the circling motion ever so often.

Detour is impressively efficient with large numbers of npc moving at once though. Up to 300, you may see a 3-5 fps loss. Up to about 600 to 900 you will have about a 50% drop, At 1300, Im down to about 6 fps. This is on a intel ie3 with onboard grphics setup though.

Crowd handles all types of terrain very nicely, hills, valleys, flat but its those narrowing areas is where they will stack up. This is without jme physcis, add that into the mix and your frame rate is toasted by 300 npc. These testing npc are only 1 meter balls so they have very low amount of vertices, keep that in mind.

You can also use it for dynamic terrain changes like when a door closes, you can change that tile in the nav mesh to prevent movement through the door but I haven’t tried that yet. I know it can do it though.

The bouncing around at narrow transitions could probably be overcome with changes to the code or there may be a way to do it now and I am just not aware of it due to the lack of documentation. Like you can use the crowd for the longer, open movement, but then when near the door use the straight path method.

That’s the way I tried to overcome this was two types of Crowd navigation controls. I wrote my own for the straight path by hacking the crowd code but did not finish it to the point of being game ready code. It does not account for the npc collision avoidance and is not as efficient as crowd. At 300 npc, it bogs down the system exactly as if I were using jme physics.

Take into account that I am not as proficient as you are with code so you may be able to make this thing sing but it will take some effort.

I haven’t tried monkey brains since I adapted detour because at the time it wasn’t working at all very well for me. I would be interested in how it moves now. I can tell you that the jme nav mesh creation did have a bug that I fixed where it was offsetting the nav mesh by a .5f in two directions. Couldn’t figure out if that was intentional or not for some specific reason related to jme AI.

The jme nav mesh creation is not very good at all when compared to Recast though. Recast produces very accurate Tiled meshes. Jme nav mesh is a single, large tile mesh. You can reproduce the exact thing using recast soloMeshGenerator.

The main difference being that when using a single, large jme tile nav mesh, large open areas lead to strange behavior. Like the nav path taking the path from point A, to the edge of the nav mesh point B (following the mesh triangle), then back to point C, when it should just straight path from A to C. This does not happen when using recast. Even on one large tile mesh it works as expected.

The advantage to recast is its navigation path for straight path navigation. Its beautiful. However it doesn’t account for collision so its only good for PC’s, where you may want that type of behavior, where the PC’s cant block other PC’c from a spot or stop them from walking through you and they all stack up nicely. Whereas for an NPC, you want that collision detection so they do exactly the opposite.

I have done exhaustive work on this and still am not completely satisfied with the end result. I have hacked around the problematic parts but its still a WIP to get it to be efficient enough to work in a game to the point where a player wont say “WTF was that, did you see that?”. I have all the work done for the recast part and jme where you can save everything and load it, etc.

There is a big drawback to any navmesh creation though and that is the time to build the mesh. You can prebuild a 512x512 nav mesh in about 5 min. A 2048x2048 with hills and valleys and all sorts of things can take 8+ hours or longer. The gentleman who ported Recast/Detour added a listener so we can time these things.

Overall, recast is awesome for PC movement and nav mesh generation. Detour is good, just needs more work to make it awesome. A caveat being it may already be awesome and I am not aware of how to make it work properly.

2 Likes

Forgot to mention, with Crowd, you are using a list where all you do is add the npc to the crowd with a target and crowd does all the work. You just add a control to the npc which extends an AgentParameters class so you can add it to the crowd to watch for the changes on one variable which is an array of x,y,z that you use for translation. You can stop the movement at any time by just removing them from the crowd.

Edit: Also forgot to mention that there are some changes that I asked for that make Crowd jme compatible as a library where certain variables were package private that were made public that will not take effect unless you wait for the 1.8 version or compile from source.

2 Likes

Complexity: GDX-AI already has much more complicated behaviors and at least their wiki has good information (so I’d need to port stuff to monkey brains for that).

I’ve now decided to give recast4j a shot now since at least the regular detour crowd does awesome things (like calculating a movement corridor instead of a path with size=0) and thus seems to automagically solve all the road blocks I had in my head where behaviors stuck each other and paths being unwalkable (in my type of game it’s worse than say in a RTS/Top Down something).

@mitm So you did some recast/jme bindings/code already? (I was looking into the code a bit and their tests seem really verbose with all the different classes which are created and individual parameters)

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.

@pspeed @nehon
Would this license preclude recast4j from being officially the AI part of the engine?

I guess the issue here is a complete different one: AI is a specific usecase and peripheral for most games and when something is “official” we have to support it, I guess the trend (regarding the team size) would rather be the opposite, to shift stuff out.

The license would be ready to go, because we would not claim that this is our code (that seems to be what the whole license is about).

The best solution would probably be if you’d open up a repository jme-recast-and-detour (or something, jme3-recast4j) for the bindings (if that’s why you are asking about the license).
I’d love to help you with the code, if there is the need for it (however for my case I can’t use controls, so a more generic api would be nice (e.g. https://github.com/MeFisto94/MonkeyBrains/blob/myFork/src/com/jme3/ai/agents/ApplyType.java ).

One Question: The Position handed to Crowd#addAgent is that the positon on the grid of the crowd or just the world positon? (because I suspect detour crowd not having something like a formation movement?)

Its the spatials jme position.

This is the pattern to use, first add the spatial to the crowd so its parameters are set.

int addAgent = ((UserDataObj)userData).getCrowd().addAgent(pos, getAgentParams());

The crowd will automatically find the nearest position on the navmesh you used when you started the crowd.

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

and sets the variables in the CrowdAgent object.

If everything goes right, you will have an index number set ranging from 0 to your max crowd size, otherwise its set to -1.

You then use this line

CrowdAgent ag = ((UserDataObj)userData).getCrowd().getAgent(addAgent);

to get the appropriate CrowdAgent obj for the index.

This lets you see if the agent is active or not. If the agent is active it means the Crowd accepted your addAgent.

        if (ag.isActive()) {

        }

If they are active, you then request to set a target for the agent. You do so by finding the nearest poly for the target using a the query obj, the target being your pick targets jme location supplied by whatever picking method you use. I use the standard ray cast in this example and supply the closet filtered contact point returned by the ray cast. The Crowd uses arrays where jme uses vector3f so you just convert it.

FindNearestPolyResult nearest = ((UserDataObj)userData).getQuery().findNearestPoly(target.toArray(null), extents, filter);

EDIT: see edit at bottom of page.

Once you have a position on the navigation mesh you can then submit that to the crowd for your agents target using the FindNearestPolyResult object.

boolean requestMoveTarget = ((UserDataObj)userData).getCrowd().requestMoveTarget(addAgent, nearest.getNearestRef(), nearest.getNearestPos());

If it returns true, your agent is on the move so set your UserDataObj

            if (requestMoveTarget) {
                ((UserDataObj)userData).setIdx(addAgent);
            }

You then use the UserDataObject to check on the progress of your agent in the Crowd and to set positions and calculate direction in jme vectors3f by using the npos array or to make animations play or whatever you determine you need to do.

The Crowd is setup using the tpf from the update method of a BaseAppState. So everything is in sync.

I can show you how I setup the crowd.

The recast4j library is untouched by anyone. No changes need be made. All that’s needed for this to work with jme is the appropriate jme classes. Thats the only part that has to be maintained and its not that many classes either. Theres a Navesh generation class and you need a few others for saving objects and parameters. All total I think its 3 files or so for the whole shebang but that is probably due mostly to my over coding things. Those more familiar with the guts of the engine could probably par it down significantly.

EDIT: The query object is set when you instantiate the Crowd. You use it to query the navMesh. You can save a reference to it in the UserDataObj or just keep a reference in the state you instantiated Crowd with.

Here is how I setup crowd. This is a hack from the test AbstractCrowdTest.java

package mygame.detour.crowd;

import com.jme3.app.Application;
import com.jme3.app.state.BaseAppState;
import java.util.Arrays;
import mygame.recast.MeshParameters;
import static org.recast4j.detour.DetourCommon.vNormalize;
import static org.recast4j.detour.DetourCommon.vScale;
import static org.recast4j.detour.DetourCommon.vSub;
import org.recast4j.detour.FindNearestPolyResult;
import org.recast4j.detour.MeshData;
import org.recast4j.detour.NavMesh;
import org.recast4j.detour.NavMeshBuilder;
import org.recast4j.detour.NavMeshDataCreateParams;
import org.recast4j.detour.NavMeshQuery;
import org.recast4j.detour.QueryFilter;
import org.recast4j.detour.crowd.Crowd;
import org.recast4j.detour.crowd.CrowdAgent;
import org.recast4j.detour.crowd.CrowdAgentParams;
import org.recast4j.detour.crowd.ObstacleAvoidanceQuery.ObstacleAvoidanceParams;

/**
 *
 * @author mitm
 */
public class CrowdState extends BaseAppState {

    private NavMeshQuery query;
    private Crowd crowd;
    protected NavMesh navMesh;
    
    @Override
    protected void initialize(Application app) {
        boolean solo = false;
        
        if (solo) {
            //Load tiled recast navMesh from saved parameters
            MeshParameters meshParams = (MeshParameters) getApplication().getAssetManager().
                    loadAsset("Scenes/Recast/navmesh_solo_C64.j3o");

            NavMeshDataCreateParams params = meshParams.getNavMeshDataParameters(0);
            MeshData meshData = NavMeshBuilder.createNavMeshData(params);
            navMesh = new NavMesh(meshData, params.nvp, 0);
        } else {
            MeshParameters meshParams = (MeshParameters) getApplication().getAssetManager().
                loadAsset("Scenes/Recast/navmesh_tiled_ALL64.j3o");
            navMesh = new NavMesh(meshParams.getNavMeshParameters(), 6);
            for (int i = 0; i < meshParams.getNumTiles(); i++) {
                NavMeshDataCreateParams params = meshParams.getNavMeshDataParameters(i);
                navMesh.addTile(NavMeshBuilder.createNavMeshData(params), 0, 0);
            }
        }
        
        //start crowd
        setupCrowd(navMesh);  

    }

    @Override
    protected void cleanup(Application app) {

    }

    //onEnable()/onDisable() can be used for managing things that should 
    //only exist while the state is enabled. Prime examples would be scene 
    //graph attachment or input listener attachment.
    @Override
    protected void onEnable() {

    }

    @Override
    protected void onDisable() {
    }


    @Override
    public void update(float tpf) {

        getCrowd().update(tpf, null);

    }
    
//    protected void dumpActiveAgents() {
//        System.out.println("Active Agents: " + getCrowd().getActiveAgents().
//                size());
//        int count = 0;
//        for (CrowdAgent ag : getCrowd().getActiveAgents()) {
//            System.out.println(ag.state + ", " + ag.targetState);
//            System.out.println("Agent Pos: " + ag.npos[0] + ", " + ag.npos[1]
//                    + ", "
//                    + ag.npos[2]);
//            System.out.println("Desired Velocity: " + ag.nvel[0] + ", "
//                    + ag.nvel[1] + ", "
//                    + ag.nvel[2]);
//            System.out.println("Agent idx: " + ag.idx);
//            count++;
//        }
//        System.out.println("Count " + count + " =====================================================================");
//    }


    private void setupCrowd(NavMesh navMesh) {

        query = new NavMeshQuery(navMesh);
        crowd = new Crowd(1500, 0.6f, navMesh);
        ObstacleAvoidanceParams params = new ObstacleAvoidanceParams();
        params.velBias = 0.5f;
        params.adaptiveDivs = 5;
        params.adaptiveRings = 2;
        params.adaptiveDepth = 1;
        getCrowd().setObstacleAvoidanceParams(0, params);
        params = new ObstacleAvoidanceParams();
        params.velBias = 0.5f;
        params.adaptiveDivs = 5;
        params.adaptiveRings = 2;
        params.adaptiveDepth = 2;
        getCrowd().setObstacleAvoidanceParams(1, params);
        params = new ObstacleAvoidanceParams();
        params.velBias = 0.5f;
        params.adaptiveDivs = 7;
        params.adaptiveRings = 2;
        params.adaptiveDepth = 3;
        getCrowd().setObstacleAvoidanceParams(2, params);
        params = new ObstacleAvoidanceParams();
        params.velBias = 0.5f;
        params.adaptiveDivs = 7;
        params.adaptiveRings = 3;
        params.adaptiveDepth = 3;
        getCrowd().setObstacleAvoidanceParams(3, params);
    }


    protected CrowdAgentParams getAgentParams(int updateFlags, int obstacleAvoidanceType) {
        CrowdAgentParams ap = new CrowdAgentParams();
        ap.radius = 0.6f;
        ap.height = 2f;
        ap.maxAcceleration = 8.0f;
        ap.maxSpeed = 3.5f;
        ap.collisionQueryRange = ap.radius * 12f;
        ap.pathOptimizationRange = ap.radius * 30f;
        ap.updateFlags = updateFlags;
        ap.obstacleAvoidanceType = obstacleAvoidanceType;
        ap.separationWeight = 2f;
        return ap;
    }

    public void addAgentGrid(int size, float distance, int updateFlags,
            int obstacleAvoidanceType, float[] startPos) {
        CrowdAgentParams ap = getAgentParams(updateFlags, obstacleAvoidanceType);
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                float[] pos = new float[3];
                pos[0] = startPos[0] + i * distance;
                pos[1] = startPos[1];
                pos[2] = startPos[2] + j * distance;
                int addAgent = getCrowd().addAgent(pos, ap);
            }
        }
    }

    public void setMoveTarget(float[] pos, boolean adjust) {
        float[] ext = getCrowd().getQueryExtents();
        QueryFilter filter = getCrowd().getFilter(0);
        if (adjust) {
            for (int i = 0; i < getCrowd().getAgentCount(); i++) {
                CrowdAgent ag = getCrowd().getAgent(i);
                if (!ag.isActive()) {
                    continue;
                }
                float[] vel = calcVel(ag.npos, pos, ag.params.maxSpeed);
                getCrowd().requestMoveVelocity(i, vel);
            }
        } else {
            FindNearestPolyResult nearest = query.findNearestPoly(pos, ext,
                    filter);
            for (int i = 0; i < getCrowd().getAgentCount(); i++) {
                CrowdAgent ag = getCrowd().getAgent(i);
                if (!ag.isActive()) {
                    continue;
                }
                getCrowd().requestMoveTarget(i, nearest.getNearestRef(),
                        nearest.getNearestPos());
            }
        }
    }

    protected float[] calcVel(float[] pos, float[] tgt, float speed) {
        float[] vel = vSub(tgt, pos);
        vel[1] = 0.0f;
        vNormalize(vel);
        vel = vScale(vel, speed);
        return vel;
    }

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

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


    /**
     * @return the navMesh
     */
    public NavMesh getNavMesh() {
        return navMesh;
    }

}

Edit: You don’t need the testing stuff. I just left that in. Basically setup the crowd anyway you want. Pass your navmesh to it, update it in the update() method.

Yep. “Included with JME” has only down sides, really… for all involved.

Since the library doesn’t need anything to be done to use it, I can see how it would only add bloat.

Maybe just add the needed classes to the doc-examples repo.