"Dynamic" NavMesh

Hello, im making a simple pathfinding and path following system using jme3AI library, and i’ve chosen the SDK navMesh generation way. The thing i want to achieve is to have a terrain and on it some static, destructible objects that are taken into account when pathfinding (like walls). However, if i generate the navmesh for a scene with terrain only the pathfinding wont take into account the walls, and if i generate the pathfinding for terrain+walls scene then the place where the walls were (once they are destroyed) will still be impassable. How can i achieve such “dynamic” in potential pathfinding routes? From what i figured, generating new nav mesh in real time is impossible as for larger terrains the nav mesh generation can take a long time (minutes, even hours).


So, say if instead of those hills i’d place impassable walls, after i destroy them, the navMesh would of course remain unchanged.

2 Likes

thats why i use Recast4J library where i can re-generate Tiles.

@mitm had some unfinished jme-integration of it as i remember. i were using part of it.

2 Likes

First, jme3 ai is only good for simple path finding. What you are showing us in this image is its major fault of only being able to build a solo mesh, i.e. a navmesh consisting of one tile.

Second, this stuff gets real complicated real fast.

To do dynamic navmesh like you want requires a tiled navmesh and a tile cache. This allows you to build a single cache of tiles identical to the navmesh of tiles to swap out when an object blocks part of the navmesh.

As an example, someone pushes a box onto a tile and that box now blocks the path across that tile. You cut a hole into the navmesh by removing that tile that has been blocked by the box. The path finding routine will find there is a hole in the navmesh and try to find a path around it if possible or just end at the nearest walkable tile. You can only cut squares from a navmesh but you can use a circle or boxed shaped object to do the cutting i.e selecting of what tile to remove. Recast will accept only those two shapes if I remember correctly. Basically barrels or boxes.

If the offending box gets removed, simply grab the tile from the cache and insert it back into the navmesh and you now have a walkable tile. Recast does this all for you.

I can give you examples of building a tiled navmesh but they relate to a stalled project I was assisting someone on. Although it is specific to that project, I explain the process of building a tiled navmesh and tile cache. This project is based off recast4j.

I suggest reading that wiki I linked, not just the two linked parts of it, as its loaded with details on whats going on with recast4j. Recast 4j is still very actively maintained and well worth the effort to try to learn.

Read the wiki homepage for the example project I linked to for links to recast4j.

Each part of recast4j has a test folder that contains tests that give great clarity to how each part works but it will require lots of reading to figure things out. I spent a year on it myself but I am not as skilled with java as most here so others were able to figure it out in weeks or days. As I was a newbie java programmer, I would get side tracked learning other things just to continue on with recast4j.

Hopefully your journey will be shorter than mine. I am sidelined with real life still so I cant really help as much as I would like. You could try to use that project I linked to as a wrapper but it will require alot of effort to get working as its not finished.

See this link to try and do that.

https://hub.jmonkeyengine.org/t/solved-a-problem-in-using-navmesh-downward-face-normals/43504/4

5 Likes

@oxplay2 thank you all, forgot to reply. i’ve already found my way through the forum to this
github page NavMesh Generation · MeFisto94/jme3-recast4j-demo Wiki · GitHub and prerequisites for it to work. Im working on it and will ask any questions here if i still have any questions after i read all the examples and documentation provided by @mitm

You could try to use that project I linked to as a wrapper but it will require alot of effort to get working as its not finished

Alright. What is there still to be done in jme-recast4j I’ve read through it and had a look at recast example tests, which i believe i didnt understand. I couldnt in fact even get the test to run because of the InputStream.class.getClass().getClassLoader().getResourceAsStream(“test_tiles.voxels”); (changed it so i could use it in static context) would always throw NPE, regardless of how “detailed” filepath i’d write, nor the position of the file in files hierarchy so to speak. seems like it’s gonna be a pain in the back for a while

May i ask how you got this part of code to work (in recast4j not the jme-recast4j)

public TestTiledNavMeshBuilder() {
        this(new ObjImporter().load(TestTiledNavMeshBuilder.class.getClassLoader().getResourceAsStream("dungeon.obj")),
                PartitionType.WATERSHED, m_cellSize, m_cellHeight, m_agentHeight, m_agentRadius, m_agentMaxClimb, m_agentMaxSlope,
                m_regionMinSize, m_regionMergeSize, m_edgeMaxLen, m_edgeMaxError, m_vertsPerPoly, m_detailSampleDist,
                m_detailSampleMaxError, m_tileSize);
    }

i think i wrapped my head around tiled navmesh generation, there’s just this problem i cannot possibly
get this line to work:

ObjImporter().load(TestTiledNavMeshBuilder.class.getClassLoader().getResourceAsStream("dungeon.obj")

no matter what i do the path is incorrect and throws nullpointer exception
im talking about my own .obj file of course and its path in project assets

Update:
Managed to generate navmesh for my .obj file (cube with 10 loop cuts on each side in blender) with recast (look at the console output):


Now im trying to export it using ObjExporter provided in recast4j to export the navmesh back to blender to have a look how it looks (if you know ways to have a look at it let me know please).
If i manage to get it up and running (navmesh creation, pathfinding) i will try to create a tutorial on how to set everything up.
EDIT: im using Recast4j, not the jme-recast4j integration

Okay, the nav mesh generation works well. At the moment managed to:

  1. give the program its “source” mesh (.obj extension) for which the nav mesh is generated.
  2. generate a navMesh.
  3. Export the navmesh back to .obj for further investigation

    ^ source mesh

    ^NavMesh generated by the program

Hi @Mccurry, the topic is interesting. Keep us updated with your progress, perhaps with some sample code and images :wink:

I surely will. This thread will be basically me tinkering around with removing/adding tiles,pathfinding and some questions to more experienced users such as @mitm and @oxplay2. When i am done, i’ll create another thread which will be a simple sample code, my observations and other stuff i find out later

3 Likes

For more complex scenes scenes however the nav mesh generation didnt do so well (or it did, even too well)


Source mesh

everything there (apart from these strange patches of mesh under the walls and the tower) can be adressed and fixed directly by changing generation parameters

2 Likes

@mitm can you give me any hint about how to make use of found path? So i’ve imported my own scene, found a path from a starting position to end position (i’ve got all the tileRefs along the path), but i’ve no idea what to do further. Any hint how to easy my suffering?

You feed the information to a control, BCC if using physics. You don’t have to use physics though.

NavState.java

PhysicsAgentControl.java

AbstractNavMeshControl.java

This is using the old animation system.

The demo project should still work and has a Lemur gui that can be used to build and set every recast4j parameter, including using physics or not with pathfinding. It is fully integrated with the jme3-recast4j project.

I wrote a ton of notes in the code to try and help. I also explain every recast4j parameter which can be accessed through the demo help system or just by reading the source. Its some complicated stuff as it uses Lemur magic.

I did not add any actual tile cache use, just how to build them is demonstrated in code I showed above, as that was a scenario on the todo list.

These are the demo classes,
https://github.com/MeFisto94/jme3-recast4j-demo/tree/master/src/main/java/com/jme3/recast4j/demo/states

You have to manually choose which scenario to use by uncommenting lines as I did not integrate that into the Crowd Builder GUI. Was on the todo list.

https://github.com/MeFisto94/jme3-recast4j-demo/blob/52af8a0e1a9b26f03f326a9df8d3dd773a686f25/src/main/java/com/jme3/recast4j/demo/DemoApplication.java#L80-L93

Uncomment,

loadNavMeshLevel(); 

and comment out

loadPond();
loadPondSurface();
loadCrate();

to run the other scenario, which demonstrates how doors work in pathfinding with recast4j. The current setup scenario demonstrates how offMeshConnections work with path finding.

This is how to run the demo,
https://github.com/MeFisto94/jme3-recast4j-demo/wiki/Crowd-Builder

This is the jme3-recast4j code, which the demo uses, so you can find how they work together.

The demo has its own built jar using the last release so you don’t need jme3-recast4j for anything other than research.

1 Like

Thank you, but im using recast4j itself rather than jme-recast4j. Im still working on making the agent move but to no avail

Still stuck at having the path in tile refs, but cant see a way of utilising this to any kind of path following.


note the console output.


1 Like

@mitm
could you point me in the right direction (using recast4j itself)?

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);
    }

}

Pickable.java

package mygame.interfaces;

import com.jme3.math.Vector3f;

/**
 * Sets the target for the NavigationControl.
 * 
 * @author mitm
 */
public interface Pickable {
    void setTarget(Vector3f target);
}

UserDataKey.java

package mygame.interfaces;

/**
 * Constants for UserData keys.
 * 
 * @author mitm
 */
public interface DataKey {
    static final String START_POS_TYPE = "start_pos_type";
    static final String MOVE = "move";
    static final String MAX_MOVE = "max_move";
    static final String AUTORUN = "autorun";
    static final String NAVMESH = "NavMesh";
    static final String NAVMESH_GENERATOR = "NavMeshGenerator";
    static final String START_POSITION = "start_position";
    static final String USER_ID = "user_id";
    /**
     * The position of this char.
     */
    static final String POSITION_TYPE = "pos_type";
    
}

AnimKey.java

package mygame.interfaces;

/**
 * Constants for Animation names.
 * 
 * @author mitm
 */
public interface AnimKey {
    static final String RUN_BASE = "RunBase";
    static final String RUN_TOP = "RunTop";
    static final String IDLE_TOP = "IdleTop";
    static final String IDLE_BASE = "IdleBase";
    static final String IDLE = "Idle";
    static final String WALK = "Walk";
    static final String RUN = "Run";
    static final String TPOSE = "TPose";
}

ListenerKey.java

/**
 * Constants for keyboard listener keys.
 * 
 * @author mitm
 */
public interface ListenerKey {
    static final String PICK = "pick";
    static final String PAUSE = "pause";
    static final String JUMP = "jump";
    static final String MOVE_FORWARD = "moveForward";
}

EnumPosType.java

package mygame.enums;

/**
 * A physical pos with speed settings.
 * 
 * @author mitm
 */
public enum EnumPosType {

    POS_DEAD(0, 0.0f),
    POS_MORTAL(1, 0.0f),
    POS_INCAP(2, 0.0f),
    POS_STUNNED(3, 0.0f),
    POS_SLEEPING(4, 0.0f),
    POS_RESTING(5, 0.0f),
    POS_SITTING(6, 0.0f),
    POS_FIGHTING(7, 0.0f),
    POS_TPOSE(8, 0.0f),
    POS_STANDING(9, 0.0f),
    POS_SWIMMING(10, 1.5f),
    POS_WALKING(11, 3.0f),
    POS_RUNNING(12, 6.0f);

    private final float speed;
    private final int pos;

    EnumPosType(int positionType, float speed) {
        this.speed = speed;
        this.pos = positionType;
    }

    /**
     * @return the speed
     */
    public float speed() {
        return speed;
    }

    /**
     * @return the pos
     */
    public int pos() {
        return pos;
    }
    
    public static EnumPosType getPosType(int pos) {
        for(EnumPosType mt : EnumPosType.values())
            if(mt.pos == pos)
                return mt;

        throw new IllegalArgumentException();
    }

}
1 Like

seems like you’ve pasted the DetourMoveControl to the PCcontrol :wink:
Also, what is that class

import mygame.enums.EnumPosType;

?
Thanks for help!

Fixed and added enum to end of second post.

Thank you. I’ll try to make it work just now, and sorry for replying after a couple days, have a lot going on due to my A-levels