Game Jam 01: Climbing System

Hello everybody,
I would like to try to implement with jMonkeyEngine the mechanics to climb on high surfaces and give some verticality to the game levels. I looked for some material and as a first goal I proposed simple features like those indicated in the video.

  • The first step of hanging on the edge can be done without too many problems.
  • I can’t complete the climbing step.

Could you give me some advice on how to do it? Many tutorials rely on rootMotion which we don’t have here. Who wants to take up the challenge and help me solve the puzzle? :grinning:

6 Likes

I have no clue how to code a climbing system, but I do know of a youtube video by Game Maker’s Tool Kit on climbing mechanics that could give some inspiration.

So, could be → central force on -ve y → Animation climbing state → advancing using central force on +ve z axis → finish animation climbing state → back to stand resting position ?:thinking:

I am not sure what you mean by this. Can you please elaborate on this?

1 Like

I clean up my test case and try to upload it to github.
I haven’t found a solution yet but a lot of failures. :grin:

I would suggest using the Mixamo site animations as a reference.

1 Like

I know exactly what you mean, and I have also been investigating this @capdevon, we are not making the same game are we? :grin:

The point is that some animations are not animating “in place”, root motion animations, meaning the root of the character is not staying at the exact same translation. Translation over XZ axes have different consequences than changes over only Y axis.

In Blender you can model climbing a ramp, or falling off a ramp with the exact translation as the animator thinks it feels most natural. This part is important, “feels natural”. On the contrary you can model the animation in place, and you have to somehow in the game guess the velocity of the character and make it move forward, whilst preventing it from sliding.

Now here’s the problem: if you start a root motion animation that does use translations, so it looks most natural, and you start a new animation right after that, then the character will probably jump back to zero translation (haven’t tested it).

I was thinking of fixing this by playing the in-place animation but using the translation information from the root motion animation and applying that on the model myself to make it move. We could make something generic for this possibly, that does this out of the box. What do you think?

3 Likes

I see. Probably this a missing feature in the current API.

Yes, I guess we need to deal with that as you said. A utility to extract root motion track from the root bone and apply it to spatial while properly converting transforms from anim space to the model world space.

2 Likes

Would that work if you have a TransformTrack that keeps adding on the current model translation info ?

I think I have tried that before & it did work…

Also, could that be done using DynamicAnim of physics ?

Not sure this will work. Probably we need to catch model transform at the beginning of the animation and keep applying to that one instead of the current model transform. But I might be wrong! :slightly_smiling_face:

1 Like

I mean something like :

      stackTwoTrack.setKeyframesTranslation(new Vector3f[]{
                stackTwo.getLocalTranslation(),
                stackTwo.getLocalTranslation().addLocal(new Vector3f(stackTwo.getLocalScale().x+1f,0,0)),
                stackTwo.getLocalTranslation().addLocal(new Vector3f(0,stackTwo.getLocalScale().y+0.2f,0)),
                stackTwo.getLocalTranslation().addLocal(new Vector3f(-stackTwo.getLocalScale().x,0,0)),
        });

Should that catch the current model transform ?

Hello everybody,
you have posted many interesting ideas!

I uploaded a basic test case with all the animations on my github account, so you can add and test your ideas. Test case

Come on guys, maybe we are close to a solution. Share your ideas. If you like this game-jam format, I have other interesting puzzles to share with you. :grinning:

@daBlesr I’m glad we have the same goals, so we can help each other. :wink:

4 Likes

Just tried your test, and it looks cool. I like your approach. :+1:

It is simple and does not deal with root motion complexity specially if physics is involved. :slightly_smiling_face:

1 Like

I’m not currently in the position to test out your code, but I’m interested in your solution. Could you please briefly explain how you approached the problem? Did it involve manual speed and rotation corrections?

For example, my dragon has an animation where it runs and turns at the same time, so it moves forward and rotates, in a parabolic curve. It’s really hard to get the velocities aligned with the animation without using root motion.

Btw, unreal engine has a really interesting article about it: Root Motion | Unreal Engine Documentation

1 Like

Unfortunately my idea is unstable and limited. I looked for different approaches: some use in-place animations and move the character along parabolic or linear functions. Others take advantage of the root motion functionality available to the engine. Very very interesting the article on the Unreal Engine, are we able to replicate it?

My test case is based on this video:

Here are other reference videos:

2 Likes

Hello everybody,
maybe I was able to take a step forward. I tried to translate this idea you suggested into java code.

To quickly write a test case, I created a reference geometry and attached it to the root node. During the climbing animation I tried to apply the transformations of the character’s root bone to this geometry respecting the sequence of time. I extracted the TransformTrack from the climbing animation using @sgold Heart-6.4.2 super library. :grinning:

The test works! Here are the main instructions:

Geometry rootBoneRef = debugShape.createWireSphere(0.4f, ColorRGBA.White);
rootNode.attachChild(rootBoneRef);
private class PlayerControl extends AdapterControl implements ActionListener {

	// -------- NEW
	public Geometry rootBoneRef;
	
	TransformTrack tt;
	Transform rootMotion = new Transform();
	// -------- END NEW
	
	...

	@Override
	public void setSpatial(Spatial sp) {
		super.setSpatial(sp);
		if (spatial != null) {
			this.bcc = getComponent(BetterCharacterControl.class);
			this.animComposer = getComponentInChild(AnimComposer.class);

			// setup animations
			animComposer.getAnimClipsNames().forEach(animName -> animComposer.action(animName));

			String animName = AnimDefs.Climbing;
			Action action = animComposer.getAction(animName);
			action = new BaseAction(Tweens.sequence(action, Tweens.callMethod(this, "onClimbingDone")));
			animComposer.addAction(animName, action);
			
			// -------- NEW
			SkinningControl skeleton = getComponentInChild(SkinningControl.class); 
			Joint hips = skeleton.getArmature().getJoint("Armature_mixamorig:" + MixamoBodyBones.Hips);
			tt = MyAnimation.findJointTrack(animComposer.getAnimClip(AnimDefs.Climbing), hips.getId());
			// -------- END NEW
		}
	}

	@Override
	protected void controlUpdate(float tpf) {
		if (!isClimbingMode) {
			// Player is in NORMAL state
			updateMovement(tpf);

		} else {
			// Player is in CLIMBING state
			if (startClimb && !isClimbingAnimDone) {
				// align with wall
				//spatial.getWorldRotation().slerp(helper.getRotation(), tpf * 5);
					
				// -------- NEW
				tt.getDataAtTime(animComposer.getTime(), rootMotion);
				Vector3f vec = animComposer.getSpatial().localToWorld(rootMotion.getTranslation(), null);
				rootBoneRef.setLocalTranslation(vec);
				rootBoneRef.setLocalRotation(rootMotion.getRotation());
				// -------- END NEW

			} else if (isClimbingAnimDone) {
				isClimbingMode = false;
				startClimb = false;
				//spatial.setLocalTranslation(goalPosition);
				//bcc.setEnabled(true);
				bcc.warp(goalPosition);
			}
		}
	}
	
}

The white sphere is the rootBoneRef

Now there are two more things to do:

  1. Remove the root bone TransformTrack from the AnimClip while running the climbing animation.
  2. Make the origin of the character coincide with the root bone (using Blender?).

How can we do? what do you think?

3 Likes

Maybe you can do something like this

        Node wrapper = new Node();
        Spatial model = assetManager.loadModel(CHARACTER_MODEL);
        wrapper.attachChild(model);
        SkinningControl skeleton = getComponentInChild(SkinningControl.class);
        Joint hips = skeleton.getArmature().getJoint("Armature_mixamorig:" + MixamoBodyBones.Hips);
        Vector3f negate = hips.getModelTransform().getTranslation().negate();
        model.setLocalTranslation(negate);

this should move the model origin to the character’s hip.

1 Like

Hi @Ali_RS, I didn’t quite understand what to do. I uploaded my changes to github. Can you check if your code works and let me know please?

Sorry, It did not work :stuck_out_tongue:

not sure why Y component returned by hips.getModelTransform() is so big. (0.0, 99.672066, 0.2470683).

Edit:

by the way, In case this helps, I found this tutorial that shows how to add a root motion bone at the model origin on a rigged model in Blender.

1 Like

Found some time to play around with your test.

I modified it to use root motion.

Here is the idea:

I converted the original climbing animation to an in-place animation by removing translations from the hips track

            hipsTrack = MyAnimation.findJointTrack(climbing, hips.getId());

            AnimTrack[] tracks = climbing.getTracks();
            for (int i = 0; i < tracks.length; i++) {
                if(tracks[i] == hipsTrack) {
                    // Convert it to an in-place animation by removing translations data
                    tracks[i] = new TransformTrack(hipsTrack.getTarget(), hipsTrack.getTimes(), null, hipsTrack.getRotations(), hipsTrack.getScales());
                }
            }

then created a new spatial animation track by those translations and set the player node as its target.

Then combined these two animations in a parallel tween and add it to AnimComposer.

                // Create a root motion track for player node
                TransformTrack climbingRootMotionTrack = new TransformTrack(player, hipsTrack.getTimes(), translations, null, null);

                animComposer.addAction(AnimDefs.Climbing, new BaseAction(
                        Tweens.parallel(animComposer.getAction(AnimDefs.Climbing), new RootMotion(climbingRootMotionTrack))));

Also because the model origin and hips origin were not coinciding, I needed to translate root motion animation data back to the model origin (0, 0, 0).

            Vector3f[] translations = hipsTrack.getTranslations();
            for (Vector3f translation : translations) {
                // Because model is scaled by 0.01 we must scale animation data by 0.01 as well!
                translation.multLocal(0.01f);
                // Because hip origin(0.0, 0.99, 0.002) and model origin(0, 0, 0) is not coincide,
                // we must translate it back to model origin
                translation.subtractLocal(hipsOrigin);
            }

At its core, RootMotion is a tween that runs in parallel with its counterpart in-place action.

 private static class RootMotion implements Tween {
        private final TransformTrack track;
        private final Spatial spatial;
        private final Transform transform = new Transform();

        private Vector3f startLoc;

        public RootMotion(TransformTrack track) {
            this.track = track;
            if(!(track.getTarget() instanceof Spatial)) {
                throw new IllegalArgumentException("Target of root motion track must be a spatial.");
            }

            this.spatial = (Spatial) track.getTarget();
        }

        @Override
        public double getLength() {
            return track.getLength();
        }

        @Override
        public boolean interpolate(double t) {
            if (t > getLength()) {
                startLoc = null;
                return false;
            }

            if (startLoc == null) {
                startLoc = spatial.getLocalTranslation().clone();
            }

            track.getDataAtTime(t, transform);
            Vector3f newLocation = startLoc.add(transform.getTranslation());
            spatial.setLocalTranslation(newLocation);
            return true;
        }
    }

here is the full example:

package com.capdevon.demo;

import com.capdevon.animation.MixamoBodyBones;
import com.capdevon.control.AdapterControl;
import com.capdevon.debug.DebugShape;
import com.capdevon.engine.FVector;
import com.capdevon.physx.Physics;
import com.capdevon.physx.PhysxDebugAppState;
import com.capdevon.physx.RaycastHit;
import com.jme3.anim.*;
import com.jme3.anim.tween.Tween;
import com.jme3.anim.tween.Tweens;
import com.jme3.anim.tween.action.Action;
import com.jme3.anim.tween.action.BaseAction;
import com.jme3.anim.tween.action.ClipAction;
import com.jme3.app.Application;
import com.jme3.app.FlyCamAppState;
import com.jme3.app.SimpleApplication;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.collision.shapes.CollisionShape;
import com.jme3.bullet.control.BetterCharacterControl;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.bullet.debug.DebugTools;
import com.jme3.bullet.util.CollisionShapeFactory;
import com.jme3.input.ChaseCamera;
import com.jme3.input.InputManager;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.Trigger;
import com.jme3.light.AmbientLight;
import com.jme3.light.DirectionalLight;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Ray;
import com.jme3.math.Transform;
import com.jme3.math.Vector3f;
import com.jme3.post.FilterPostProcessor;
import com.jme3.renderer.Camera;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.shadow.DirectionalLightShadowFilter;
import com.jme3.system.AppSettings;
import com.jme3.util.SkyFactory;

import jme3utilities.MyAnimation;

/**
 * @author capdevon
 */
public class Test_Climbing extends SimpleApplication {

    /**
     * @param args
     */
    public static void main(String[] args) {
    	
        Test_Climbing app = new Test_Climbing();
        AppSettings settings = new AppSettings(true);
        settings.setUseJoysticks(true);
        settings.setResolution(1280, 720);
        settings.setFrequency(60);
        settings.setFrameRate(200);
        settings.setSamples(4);
        settings.setBitsPerPixel(32);
        settings.setVSync(false);
        
        app.setSettings(settings);
        app.setShowSettings(false);
        app.setPauseOnLostFocus(false);
        app.start();
    }

    private BulletAppState physics;
    private Node scene;
    private Node player;
    private final String CHARACTER_MODEL = "Models/Climbing/climbing-export.gltf";
    private final String SCENE_MODEL = "Models/Climbing/scene.j3o";

    @Override
    public void simpleInitApp() {
        viewPort.setBackgroundColor(new ColorRGBA(0.5f, 0.6f, 0.7f, 1.0f));
        rootNode.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);

        initPhysics();
//        setupSky();
        setupScene();
        setupPlayer();
        setupLights();
    }
    
    private void initPhysics() {
        physics = new BulletAppState();
        //physics.setThreadingType(ThreadingType.SEQUENTIAL);
        stateManager.attach(physics);
        physics.setDebugAxisLength(1);
        physics.setDebugEnabled(true);
        
        // press 0 to toggle physics debug
        stateManager.attach(new PhysxDebugAppState());
    }
    
    /**
     * a sky as background
     */
    private void setupSky() {
        Spatial sky = SkyFactory.createSky(assetManager, "Scenes/Beach/FullskiesSunset0068.dds", SkyFactory.EnvMapType.CubeMap);
        sky.setShadowMode(RenderQueue.ShadowMode.Off);
        rootNode.attachChild(sky);
    }

    private void setupScene() {
        scene = (Node) assetManager.loadModel(SCENE_MODEL);
        rootNode.attachChild(scene);
        
        CollisionShape shape = CollisionShapeFactory.createMeshShape(scene);
        RigidBodyControl rgb = new RigidBodyControl(shape, 0f);
        scene.addControl(rgb);
        physics.getPhysicsSpace().add(rgb);
    }
    
    private void setupLights() {
        AmbientLight ambient = new AmbientLight();
        ambient.setColor(ColorRGBA.White.clone());
        rootNode.addLight(ambient);
        ambient.setName("ambient");
        
        DirectionalLight sun = new DirectionalLight();
        sun.setDirection(new Vector3f(-0.5f, -0.5f, 0.5f).normalizeLocal());
        sun.setColor(ColorRGBA.White.clone());
        rootNode.addLight(sun);
        sun.setName("sun");
        
        FilterPostProcessor fpp = new FilterPostProcessor(assetManager);
        viewPort.addProcessor(fpp);
        
        DirectionalLightShadowFilter shadowFilter = new DirectionalLightShadowFilter(assetManager, 2_048, 3);
        shadowFilter.setLight(sun);
        shadowFilter.setShadowIntensity(0.4f);
        shadowFilter.setShadowZExtend(256);
        fpp.addFilter(shadowFilter);
    }

    private void setupPlayer() {
    	DebugShape debugShape = new DebugShape(assetManager);
    	
    	//
        player = new Node("MainCharacter");
        player.attachChild(debugShape.getAxisCoordinate());
        player.setLocalTranslation(0, 1, -1);
        rootNode.attachChild(player);
        
        // vertical
        Node ledgeRayV = new Node("LedgeRayV");
        ledgeRayV.attachChild(debugShape.createWireBox(0.1f, ColorRGBA.Red));
        player.attachChild(ledgeRayV);
        ledgeRayV.setLocalTranslation(FVector.forward(player).multLocal(0.5f).addLocal(0, 3, 0));
        
        // horizontal
        Node ledgeRayH = new Node("LedgeRayH");
        ledgeRayH.attachChild(debugShape.createWireBox(0.1f, ColorRGBA.Blue));
        player.attachChild(ledgeRayH);
        ledgeRayH.setLocalTranslation(FVector.forward(player).multLocal(0.2f).addLocal(0, 1.5f, 0));
        
        // setup model
        Spatial model = assetManager.loadModel(CHARACTER_MODEL);
        model.setName("Character.Model");
        player.attachChild(model);
        
        // setup physics character
        BetterCharacterControl bcc = new BetterCharacterControl(.4f, 1.8f, 40f);
        player.addControl(bcc);
        physics.getPhysicsSpace().add(bcc);
        
        // setup third person camera
        setupChaseCamera();
        
        Geometry rootBoneRef = debugShape.createWireSphere(0.4f, ColorRGBA.White);
        rootNode.attachChild(rootBoneRef);
        
        // setup player control
        PlayerControl pControl = new PlayerControl(this);
        pControl.ledgeRayH = ledgeRayH;
        pControl.ledgeRayV = ledgeRayV;
        pControl.model = model;
        pControl.rootBoneRef = rootBoneRef;
        player.addControl(pControl);
    }
    
    private void setupChaseCamera() {
        // disable the default 1st-person flyCam!
        stateManager.detach(stateManager.getState(FlyCamAppState.class));
        flyCam.setEnabled(false);
        
        ChaseCamera chaseCam = new ChaseCamera(cam, player, inputManager);
        chaseCam.setUpVector(Vector3f.UNIT_Y.clone());
        chaseCam.setLookAtOffset(new Vector3f(0f, 1f, 0f));
        chaseCam.setMaxDistance(8f);
        chaseCam.setMinDistance(5f);
        chaseCam.setDefaultDistance(chaseCam.getMaxDistance());
        chaseCam.setMaxVerticalRotation(FastMath.QUARTER_PI);
        chaseCam.setMinVerticalRotation(-FastMath.QUARTER_PI);
        chaseCam.setRotationSpeed(2f);
        chaseCam.setRotationSensitivity(1.5f);
        chaseCam.setZoomSensitivity(4f);
        chaseCam.setDownRotateOnCloseViewOnly(false);
    }

    private interface AnimDefs {
        final String Idle               = "Idle";
        final String Running            = "Running";
        final String Running_2          = "Running_1";
        final String SneakingForward    = "SneakingForward";
        final String Climbing           = "Climbing";
        final String CrouchedToStanding = "CrouchedToStanding";

        final String RightShimmy        = "RightShimmy";
        final String LeftShimmy         = "LeftShimmy";
        final String HangingIdle        = "HangingIdle";
        final String HangingIdle_1      = "HangingIdle_1";
        final String ClimbingUpWall     = "ClimbingUpWall";
        final String FreeHangToBraced   = "FreeHangToBraced";
    }

    /**
     * ---------------------------------------------------------
     * @class PlayerControl
     * ---------------------------------------------------------
     */
    private class PlayerControl extends AdapterControl implements ActionListener {

        public Node ledgeRayV;
        public Node ledgeRayH;
        public Spatial model;
        public Geometry rootBoneRef;
        
        Camera camera;
        DebugTools debugTools;
        InputManager inputManager;
        AnimComposer animComposer;
        BetterCharacterControl bcc;
        
        private final Vector3f walkDirection = new Vector3f(0, 0, 0);
        private final Vector3f viewDirection = new Vector3f(0, 0, 1);
        private final Vector3f camDir = new Vector3f();
        private final Vector3f camLeft = new Vector3f();
        private final Quaternion lookRotation = new Quaternion();
        private final RaycastHit hitInfo = new RaycastHit();

        float m_MoveSpeed = 4.5f;
        float m_TurnSpeed = 10f;
        boolean _MoveForward, _MoveBackward, _MoveLeft, _MoveRight;
        boolean isClimbingMode, startClimb;
        boolean isClimbingAnimDone = true;
        TransformTrack hipsTrack;
        Transform rootMotion = new Transform();
        
        /**
         * Constructor.
         * 
         * @param app
         */
        public PlayerControl(Application app) {
            this.camera = app.getCamera();
            this.debugTools = new DebugTools(app.getAssetManager());
            registerWithInput(app.getInputManager());
        }
        
        @Override
        public void setSpatial(Spatial sp) {
            super.setSpatial(sp);
            if (spatial != null) {
                this.bcc = getComponent(BetterCharacterControl.class);
                this.animComposer = getComponentInChild(AnimComposer.class);

                // setup animations
                animComposer.getAnimClipsNames().forEach(animName -> animComposer.action(animName));

                String animName = AnimDefs.Climbing;
                Action action = animComposer.getAction(animName);
                action = new BaseAction(Tweens.sequence(action, Tweens.callMethod(this, "onClimbingDone")));
                animComposer.addAction(animName, action);
                
                SkinningControl skeleton = getComponentInChild(SkinningControl.class);
                skeleton.getArmature().applyBindPose();
                Joint hips = skeleton.getArmature().getJoint("Armature_mixamorig:" + MixamoBodyBones.Hips);
                Vector3f hipsOrigin = hips.getModelTransform().getTranslation().clone();
                // Because model is scaled by 0.01 we must scale joint location by 0.01 as well!
                hipsOrigin.multLocal(0.01f);
                AnimClip climbing = animComposer.getAnimClip(AnimDefs.Climbing);
                hipsTrack = MyAnimation.findJointTrack(climbing, hips.getId());

                AnimTrack[] tracks = climbing.getTracks();
                for (int i = 0; i < tracks.length; i++) {
                    if(tracks[i] == hipsTrack) {
                        // Convert it to an in-place animation by removing translations data
                        tracks[i] = new TransformTrack(hipsTrack.getTarget(), hipsTrack.getTimes(), null, hipsTrack.getRotations(), hipsTrack.getScales());
                    }
                }

                Vector3f[] translations = hipsTrack.getTranslations();
                for (Vector3f translation : translations) {
                    // Because model is scaled by 0.01 we must scale animation data by 0.01 as well!
                    translation.multLocal(0.01f);
                    // Because hip origin(0.0, 0.99, 0.002) and model origin(0, 0, 0) is not coincide,
                    // we must translate it back to model origin
                    translation.subtractLocal(hipsOrigin);
                }
                // Create a root motion track for player node
                TransformTrack climbingRootMotionTrack = new TransformTrack(player, hipsTrack.getTimes(), translations, null, null);

                animComposer.addAction(AnimDefs.Climbing, new BaseAction(
                        Tweens.parallel(animComposer.getAction(AnimDefs.Climbing), new RootMotion(climbingRootMotionTrack))));
            }
        }
                
        @Override
        public void onAction(String name, boolean isPressed, float tpf) {
            if (name.equals(InputMapping.MOVE_LEFT)) {
                _MoveLeft = isPressed;
            } else if (name.equals(InputMapping.MOVE_RIGHT)) {
                _MoveRight = isPressed;
            } else if (name.equals(InputMapping.MOVE_FORWARD)) {
                _MoveForward = isPressed;
            } else if (name.equals(InputMapping.MOVE_BACKWARD)) {
                _MoveBackward = isPressed;
            } else if (name.equals(InputMapping.ACTION) && isPressed && isClimbingAnimDone) {
                checkLedgeGrab();
            }
        }
        
        @Override
        protected void controlUpdate(float tpf) {
            if (!isClimbingMode) {
                // Player is in NORMAL state
                updateMovement(tpf);

            } else {
                // Player is in CLIMBING state
                if (startClimb && !isClimbingAnimDone) {
                    // align with wall
                    //spatial.getWorldRotation().slerp(helper.getRotation(), tpf * 5);
                	
                	hipsTrack.getDataAtTime(animComposer.getTime(), rootMotion);
                	Vector3f vec = animComposer.getSpatial().localToWorld(rootMotion.getTranslation(), null);
                	rootBoneRef.setLocalTranslation(vec);
                	rootBoneRef.setLocalRotation(rootMotion.getRotation());

                } else if (isClimbingAnimDone) {
                    isClimbingMode = false;
                    startClimb = false;
                    //spatial.setLocalTranslation(goalPosition);
                    //bcc.setEnabled(true);
                    bcc.warp(goalPosition);
                }
            }
        }

        float hDistAwayFromLedge = 0.1f;
        float vDistAwayFromLedge = 0.1f;
        Transform helper = new Transform();
        Vector3f goalPosition = new Vector3f();

        private void checkLedgeGrab() {

            if (!isClimbingMode && bcc.isOnGround()) {

                Ray vRay = new Ray(ledgeRayV.getWorldTranslation(), Vector3f.UNIT_Y.negate());
                debugTools.setRedArrow(vRay.getOrigin(), vRay.getDirection());

                if (Physics.Raycast(vRay, hitInfo, 2)) {

                    System.out.println(hitInfo);
                    Vector3f hRayPosition = ledgeRayH.getWorldTranslation().clone();
                    hRayPosition.setY(hitInfo.point.y - 0.01f);

                    Ray hRay = new Ray(hRayPosition, ledgeRayH.getWorldRotation().mult(Vector3f.UNIT_Z));
                    debugTools.setBlueArrow(hRay.getOrigin(), hRay.getDirection());

                    if (Physics.Raycast(hRay, hitInfo, 2)) {
                        System.out.println(hitInfo);
                        debugTools.setPinkArrow(hitInfo.point, hitInfo.normal);

                        goalPosition.set(hitInfo.point.add(0, 0.01f, 0));

                        bcc.setViewDirection(hitInfo.normal.negate()); // align with wall
                        bcc.setWalkDirection(Vector3f.ZERO); // stop walking
                        //bcc.setEnabled(false);

                        //helper.setTranslation(hitInfo.normal.negate().multLocal(hDistAwayFromLedge).addLocal(spatial.getWorldTranslation()));
                        //helper.getTranslation().setY(hitInfo.point.y - vDistAwayFromLedge);
                        //helper.setRotation(FRotator.lookRotation(hitInfo.normal.negate()));
                        setAnimation(AnimDefs.Climbing);

                        isClimbingMode = true;
                        startClimb = true;
                        isClimbingAnimDone = false;
                        System.out.println("startClimbing");
                    }
                }
            } else {
                isClimbingMode = false;
                //bcc.setEnabled(true);
            }
        }

        void onClimbingDone() {
            isClimbingAnimDone = true;
            System.out.println("climbingDone");
        }
        
        private void updateMovement(float tpf) {

            camera.getDirection(camDir).setY(0);
            camera.getLeft(camLeft).setY(0);
            walkDirection.set(0, 0, 0);

            if (_MoveForward) {
                walkDirection.addLocal(camDir);
            } else if (_MoveBackward) {
                walkDirection.addLocal(camDir.negateLocal());
            }

            if (_MoveLeft) {
                walkDirection.addLocal(camLeft);
            } else if (_MoveRight) {
                walkDirection.addLocal(camLeft.negateLocal());
            }

            walkDirection.normalizeLocal();
            boolean isMoving = walkDirection.lengthSquared() > 0;

            if (isMoving) {
                float angle = FastMath.atan2(walkDirection.x, walkDirection.z);
                lookRotation.fromAngleNormalAxis(angle, Vector3f.UNIT_Y);
                spatial.getWorldRotation().slerp(lookRotation, m_TurnSpeed * tpf);
                spatial.getWorldRotation().mult(Vector3f.UNIT_Z, viewDirection);
                bcc.setViewDirection(viewDirection);
            }
            
            bcc.setWalkDirection(walkDirection.multLocal(m_MoveSpeed));
            setAnimation(isMoving ? AnimDefs.Running : AnimDefs.Idle);
        }
        
        private void setAnimation(String animName) {
            if (animComposer.getCurrentAction() != animComposer.getAction(animName)) {
                animComposer.setCurrentAction(animName);
            }
        }
        
        private void stopMove() {
            _MoveForward   = false;
            _MoveBackward  = false;
            _MoveLeft      = false;
            _MoveRight     = false;
        }

        @Override
        protected void controlRender(RenderManager rm, ViewPort vp) {
            if (debugTools != null) {
                debugTools.show(rm, vp);
            }
        }
        
        /**
         * Custom Keybinding: Map named actions to inputs.
         */
        private void registerWithInput(InputManager inputManager) {
            this.inputManager = inputManager;
            
            addMapping(InputMapping.MOVE_FORWARD, new KeyTrigger(KeyInput.KEY_W));
            addMapping(InputMapping.MOVE_BACKWARD, new KeyTrigger(KeyInput.KEY_S));
            addMapping(InputMapping.MOVE_LEFT, new KeyTrigger(KeyInput.KEY_A));
            addMapping(InputMapping.MOVE_RIGHT, new KeyTrigger(KeyInput.KEY_D));
            addMapping(InputMapping.ACTION, new KeyTrigger(KeyInput.KEY_SPACE));
        }

        private void addMapping(String mapping, Trigger... triggers) {
            inputManager.addMapping(mapping, triggers);
            inputManager.addListener(this, mapping);
        }

    }
    
    private interface InputMapping {

        final String MOVE_LEFT = "MOVE_LEFT";
        final String MOVE_RIGHT = "MOVE_RIGHT";
        final String MOVE_FORWARD = "MOVE_FORWARD";
        final String MOVE_BACKWARD = "MOVE_BACKWARD";
        final String ACTION = "ACTION";
    }

    private static class RootMotion implements Tween {
        private final TransformTrack track;
        private final Spatial spatial;
        private final Transform transform = new Transform();

        private Vector3f startLoc;

        public RootMotion(TransformTrack track) {
            this.track = track;
            if(!(track.getTarget() instanceof Spatial)) {
                throw new IllegalArgumentException("Target of root motion track must be a spatial.");
            }

            this.spatial = (Spatial) track.getTarget();
        }

        @Override
        public double getLength() {
            return track.getLength();
        }

        @Override
        public boolean interpolate(double t) {
            if (t > getLength()) {
                startLoc = null;
                return false;
            }

            if (startLoc == null) {
                startLoc = spatial.getLocalTranslation().clone();
            }

            track.getDataAtTime(t, transform);
            Vector3f newLocation = startLoc.add(transform.getTranslation());
            spatial.setLocalTranslation(newLocation);
            return true;
        }
    }

}

Feel free to ask if you have a question.
Regards

3 Likes

Great job @Ali_RS. We’re on the right path. There is still some flaw. Try this approach, it seems more stable to me. I think that by correcting the position of the root motion and the origin of the character in Blender maybe we can have a perfect synchrony even with the final position of the capsule (I haven’t tried it yet).

I used this method to cancel the translation while keeping the original animation intact.

		private void toInPlaceAnimation(AnimClip clip, int jointId) {

			AnimTrack[] tracks = clip.getTracks();
			for (AnimTrack track : tracks) {
				if (track instanceof TransformTrack) {
					TransformTrack transformTrack = (TransformTrack) track;
					HasLocalTransform target = transformTrack.getTarget();
					if (target instanceof Joint) {
						Joint joint = (Joint) target;
						if (jointId == joint.getId()) {
							
							// Convert it to an in-place animation by removing translations data
							CompactVector3Array vec = new CompactVector3Array();
							for (int i = 0; i < transformTrack.getLength(); i++) {
								vec.add(new Vector3f(0, 0, 0));
							}
							
							transformTrack.setKeyframesTranslation(vec.toObjectArray());
						}
					}
				}
			}
		}

Try running this solution, tell me what you think:

package com.capdevon.demo;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.ListIterator;

import com.capdevon.animation.MixamoBodyBones;
import com.capdevon.control.AdapterControl;
import com.capdevon.debug.DebugShape;
import com.capdevon.engine.FVector;
import com.capdevon.physx.Physics;
import com.capdevon.physx.PhysxDebugAppState;
import com.capdevon.physx.RaycastHit;
import com.jme3.anim.AnimClip;
import com.jme3.anim.AnimComposer;
import com.jme3.anim.AnimTrack;
import com.jme3.anim.Joint;
import com.jme3.anim.SkinningControl;
import com.jme3.anim.TransformTrack;
import com.jme3.anim.tween.Tween;
import com.jme3.anim.tween.Tweens;
import com.jme3.anim.tween.action.Action;
import com.jme3.anim.tween.action.BaseAction;
import com.jme3.anim.util.HasLocalTransform;
import com.jme3.animation.CompactVector3Array;
import com.jme3.app.Application;
import com.jme3.app.FlyCamAppState;
import com.jme3.app.SimpleApplication;
import com.jme3.bullet.BulletAppState;
import com.jme3.bullet.collision.shapes.CollisionShape;
import com.jme3.bullet.control.BetterCharacterControl;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.bullet.debug.DebugTools;
import com.jme3.bullet.util.CollisionShapeFactory;
import com.jme3.input.ChaseCamera;
import com.jme3.input.InputManager;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.input.controls.Trigger;
import com.jme3.light.AmbientLight;
import com.jme3.light.DirectionalLight;
import com.jme3.math.ColorRGBA;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Ray;
import com.jme3.math.Transform;
import com.jme3.math.Vector3f;
import com.jme3.post.FilterPostProcessor;
import com.jme3.renderer.Camera;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.ViewPort;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.shadow.DirectionalLightShadowFilter;
import com.jme3.system.AppSettings;
import com.jme3.util.SkyFactory;

import jme3utilities.MyAnimation;

/**
 * @author capdevon
 */
public class Test_Climbing extends SimpleApplication {

    /**
     * @param args
     */
    public static void main(String[] args) {
    	
        Test_Climbing app = new Test_Climbing();
        AppSettings settings = new AppSettings(true);
        settings.setUseJoysticks(true);
        settings.setResolution(1280, 720);
        settings.setFrequency(60);
        settings.setFrameRate(200);
        settings.setSamples(4);
        settings.setBitsPerPixel(32);
        settings.setVSync(false);
        
        app.setSettings(settings);
        app.setShowSettings(false);
        app.setPauseOnLostFocus(false);
        app.start();
    }

    private BulletAppState physics;
    private Node scene;
    private Node player;
    private final String CHARACTER_MODEL = "Models/Climbing/climbing-export.gltf";
    private final String SCENE_MODEL = "Models/Climbing/scene.j3o";

    @Override
    public void simpleInitApp() {
        viewPort.setBackgroundColor(new ColorRGBA(0.5f, 0.6f, 0.7f, 1.0f));
        rootNode.setShadowMode(RenderQueue.ShadowMode.CastAndReceive);

        initPhysics();
//        setupSky();
        setupScene();
        setupPlayer();
        setupLights();
    }
    
    private void initPhysics() {
        physics = new BulletAppState();
        //physics.setThreadingType(ThreadingType.SEQUENTIAL);
        stateManager.attach(physics);
//        physics.setDebugAxisLength(1);
        physics.setDebugEnabled(true);
        
        // press 0 to toggle physics debug
        stateManager.attach(new PhysxDebugAppState());
    }
    
    /**
     * a sky as background
     */
    private void setupSky() {
        Spatial sky = SkyFactory.createSky(assetManager, "Scenes/Beach/FullskiesSunset0068.dds", SkyFactory.EnvMapType.CubeMap);
        sky.setShadowMode(RenderQueue.ShadowMode.Off);
        rootNode.attachChild(sky);
    }

    private void setupScene() {
        scene = (Node) assetManager.loadModel(SCENE_MODEL);
        rootNode.attachChild(scene);
        
        CollisionShape shape = CollisionShapeFactory.createMeshShape(scene);
        RigidBodyControl rgb = new RigidBodyControl(shape, 0f);
        scene.addControl(rgb);
        physics.getPhysicsSpace().add(rgb);
    }
    
    private void setupLights() {
        AmbientLight ambient = new AmbientLight();
        ambient.setColor(ColorRGBA.White.clone());
        rootNode.addLight(ambient);
        ambient.setName("ambient");
        
        DirectionalLight sun = new DirectionalLight();
        sun.setDirection(new Vector3f(-0.5f, -0.5f, 0.5f).normalizeLocal());
        sun.setColor(ColorRGBA.White.clone());
        rootNode.addLight(sun);
        sun.setName("sun");
        
        FilterPostProcessor fpp = new FilterPostProcessor(assetManager);
        viewPort.addProcessor(fpp);
        
        DirectionalLightShadowFilter shadowFilter = new DirectionalLightShadowFilter(assetManager, 2_048, 3);
        shadowFilter.setLight(sun);
        shadowFilter.setShadowIntensity(0.4f);
        shadowFilter.setShadowZExtend(256);
        fpp.addFilter(shadowFilter);
    }

    private void setupPlayer() {
    	DebugShape debugShape = new DebugShape(assetManager);
    	
    	//
        player = new Node("MainCharacter");
        player.attachChild(debugShape.getAxisCoordinate());
        player.setLocalTranslation(0, 1, -1);
        rootNode.attachChild(player);
        
        // vertical
        Node ledgeRayV = new Node("LedgeRayV");
        ledgeRayV.attachChild(debugShape.createWireBox(0.1f, ColorRGBA.Red));
        player.attachChild(ledgeRayV);
        ledgeRayV.setLocalTranslation(FVector.forward(player).multLocal(0.5f).addLocal(0, 3, 0));
        
        // horizontal
        Node ledgeRayH = new Node("LedgeRayH");
        ledgeRayH.attachChild(debugShape.createWireBox(0.1f, ColorRGBA.Blue));
        player.attachChild(ledgeRayH);
        ledgeRayH.setLocalTranslation(FVector.forward(player).multLocal(0.2f).addLocal(0, 1.5f, 0));
        
        // setup model
        Spatial model = assetManager.loadModel(CHARACTER_MODEL);
        model.setName("Character.Model");
        player.attachChild(model);
        
        // setup physics character
        BetterCharacterControl bcc = new BetterCharacterControl(.4f, 1.8f, 40f);
        player.addControl(bcc);
        physics.getPhysicsSpace().add(bcc);
        
        // setup third person camera
        setupChaseCamera();
        
        Geometry rootBoneRef = debugShape.createWireSphere(0.4f, ColorRGBA.White);
        rootNode.attachChild(rootBoneRef);
        
        // setup player control
        PlayerControl pControl = new PlayerControl(this);
        pControl.ledgeRayH = ledgeRayH;
        pControl.ledgeRayV = ledgeRayV;
        pControl.model = model;
        pControl.rootBoneRef = rootBoneRef;
        player.addControl(pControl);
    }
    
    private void setupChaseCamera() {
        // disable the default 1st-person flyCam!
        stateManager.detach(stateManager.getState(FlyCamAppState.class));
        flyCam.setEnabled(false);
        
        ChaseCamera chaseCam = new ChaseCamera(cam, player, inputManager);
        chaseCam.setUpVector(Vector3f.UNIT_Y.clone());
        chaseCam.setLookAtOffset(new Vector3f(0f, 1f, 0f));
        chaseCam.setMaxDistance(8f);
        chaseCam.setMinDistance(5f);
        chaseCam.setDefaultDistance(chaseCam.getMaxDistance());
        chaseCam.setMaxVerticalRotation(FastMath.QUARTER_PI);
        chaseCam.setMinVerticalRotation(-FastMath.QUARTER_PI);
        chaseCam.setRotationSpeed(2f);
        chaseCam.setRotationSensitivity(1.5f);
        chaseCam.setZoomSensitivity(4f);
        chaseCam.setDownRotateOnCloseViewOnly(false);
    }

    private interface AnimDefs {
        final String Idle               = "Idle";
        final String Running            = "Running";
        final String Running_2          = "Running_1";
        final String SneakingForward    = "SneakingForward";
        final String Climbing           = "Climbing";
        final String CrouchedToStanding = "CrouchedToStanding";

        final String RightShimmy        = "RightShimmy";
        final String LeftShimmy         = "LeftShimmy";
        final String HangingIdle        = "HangingIdle";
        final String HangingIdle_1      = "HangingIdle_1";
        final String ClimbingUpWall     = "ClimbingUpWall";
        final String FreeHangToBraced   = "FreeHangToBraced";
    }

    /**
     * ---------------------------------------------------------
     * @class PlayerControl
     * ---------------------------------------------------------
     */
    private class PlayerControl extends AdapterControl implements ActionListener {

        public Node ledgeRayV;
        public Node ledgeRayH;
        public Spatial model;
        public Geometry rootBoneRef;
        
        Camera camera;
        DebugTools debugTools;
        InputManager inputManager;
        AnimComposer animComposer;
        BetterCharacterControl bcc;
        
        private final Vector3f walkDirection = new Vector3f(0, 0, 0);
        private final Vector3f viewDirection = new Vector3f(0, 0, 1);
        private final Vector3f camDir = new Vector3f();
        private final Vector3f camLeft = new Vector3f();
        private final Quaternion lookRotation = new Quaternion();
        private final RaycastHit hitInfo = new RaycastHit();

        float m_MoveSpeed = 4.5f;
        float m_TurnSpeed = 10f;
        boolean _MoveForward, _MoveBackward, _MoveLeft, _MoveRight;
        boolean isClimbingMode, startClimb;
        boolean isClimbingAnimDone = true;
        
        TransformTrack hipsTrack;
        Transform rootMotion = new Transform();
        
        /**
         * Constructor.
         * 
         * @param app
         */
        public PlayerControl(Application app) {
            this.camera = app.getCamera();
            this.debugTools = new DebugTools(app.getAssetManager());
            registerWithInput(app.getInputManager());
        }
        
        @Override
        public void setSpatial(Spatial sp) {
            super.setSpatial(sp);
            if (spatial != null) {
                this.bcc = getComponent(BetterCharacterControl.class);
                this.animComposer = getComponentInChild(AnimComposer.class);
                
                // setup animations
                animComposer.getAnimClipsNames().forEach(animName -> animComposer.action(animName));

                String animName = AnimDefs.Climbing;
                Action action = animComposer.getAction(animName);
                action = new BaseAction(Tweens.sequence(action, Tweens.callMethod(this, "onClimbingDone")));
                animComposer.addAction(animName, action);
                
                // setup root motion
                SkinningControl skeleton = getComponentInChild(SkinningControl.class);
                skeleton.getArmature().applyBindPose();
                Joint hips = skeleton.getArmature().getJoint("Armature_mixamorig:" + MixamoBodyBones.Hips);
//                Vector3f hipsOrigin = hips.getModelTransform().getTranslation().clone();
//                // Because model is scaled by 0.01 we must scale joint location by 0.01 as well!
//                hipsOrigin.multLocal(0.01f);
                AnimClip climbing = animComposer.getAnimClip(AnimDefs.Climbing);
                hipsTrack = MyAnimation.findJointTrack(climbing, hips.getId()).jmeClone();
                toInPlaceAnimation(climbing, hips.getId());

//				AnimTrack[] tracks = climbing.getTracks();
//				for (int i = 0; i < tracks.length; i++) {
//					if (tracks[i] == hipsTrack) {
//						// Convert it to an in-place animation by removing translations data
//						tracks[i] = new TransformTrack(hipsTrack.getTarget(), hipsTrack.getTimes(), null,
//								hipsTrack.getRotations(), hipsTrack.getScales());
//					}
//				}

//                Vector3f[] translations = hipsTrack.getTranslations();
//                for (Vector3f translation : translations) {
//                    // Because model is scaled by 0.01 we must scale animation data by 0.01 as well!
//                    translation.multLocal(0.01f);
//                    // Because hip origin(0.0, 0.99, 0.002) and model origin(0, 0, 0) is not coincide,
//                    // we must translate it back to model origin
//                    translation.subtractLocal(hipsOrigin);
//                }
//                // Create a root motion track for player node
//                TransformTrack climbingRootMotionTrack = new TransformTrack(player, hipsTrack.getTimes(), translations, null, null);
//
//                animComposer.addAction(AnimDefs.Climbing, new BaseAction(
//                        Tweens.parallel(animComposer.getAction(AnimDefs.Climbing), new RootMotion(climbingRootMotionTrack))));
            }
        }
        
		private void toInPlaceAnimation(AnimClip clip, int jointId) {

			AnimTrack[] tracks = clip.getTracks();
			for (AnimTrack track : tracks) {
				if (track instanceof TransformTrack) {
					TransformTrack transformTrack = (TransformTrack) track;
					HasLocalTransform target = transformTrack.getTarget();
					if (target instanceof Joint) {
						Joint joint = (Joint) target;
						if (jointId == joint.getId()) {
							CompactVector3Array vec = new CompactVector3Array();
							for (int i = 0; i < transformTrack.getLength(); i++) {
								vec.add(new Vector3f(0, 0, 0));
							}
							// Convert it to an in-place animation by removing translations data
							transformTrack.setKeyframesTranslation(vec.toObjectArray());
						}
					}
				}
			}
		}
                
        @Override
        public void onAction(String name, boolean isPressed, float tpf) {
            if (name.equals(InputMapping.MOVE_LEFT)) {
                _MoveLeft = isPressed;
            } else if (name.equals(InputMapping.MOVE_RIGHT)) {
                _MoveRight = isPressed;
            } else if (name.equals(InputMapping.MOVE_FORWARD)) {
                _MoveForward = isPressed;
            } else if (name.equals(InputMapping.MOVE_BACKWARD)) {
                _MoveBackward = isPressed;
            } else if (name.equals(InputMapping.ACTION) && isPressed && isClimbingAnimDone) {
                checkLedgeGrab();
            }
        }
        
        @Override
        protected void controlUpdate(float tpf) {
            if (!isClimbingMode) {
                // Player is in NORMAL state
                updateMovement(tpf);

            } else {
                // Player is in CLIMBING state
                if (startClimb && !isClimbingAnimDone) {
                    // align with wall
                    //spatial.getWorldRotation().slerp(helper.getRotation(), tpf * 5);
                	
                	hipsTrack.getDataAtTime(animComposer.getTime(), rootMotion);
                	Vector3f vec = animComposer.getSpatial().localToWorld(rootMotion.getTranslation(), null);
                	rootBoneRef.setLocalTranslation(vec);
                	rootBoneRef.setLocalRotation(rootMotion.getRotation());
                	
                	spatial.setLocalTranslation(vec);
                	
                } else if (isClimbingAnimDone) {
                    isClimbingMode = false;
                    startClimb = false;
                    //spatial.setLocalTranslation(goalPosition);
//                    bcc.getRigidBody().setKinematic(false);
//                    bcc.setEnabled(true);
                    bcc.warp(goalPosition);
                }
            }
        }

        float hDistAwayFromLedge = 0.1f;
        float vDistAwayFromLedge = 0.1f;
        Transform helper = new Transform();
        Vector3f goalPosition = new Vector3f();

        private void checkLedgeGrab() {

            if (!isClimbingMode && bcc.isOnGround()) {

                Ray vRay = new Ray(ledgeRayV.getWorldTranslation(), Vector3f.UNIT_Y.negate());
                debugTools.setRedArrow(vRay.getOrigin(), vRay.getDirection());

                if (Physics.Raycast(vRay, hitInfo, 2)) {

                    System.out.println(hitInfo);
                    Vector3f hRayPosition = ledgeRayH.getWorldTranslation().clone();
                    hRayPosition.setY(hitInfo.point.y - 0.01f);

                    Ray hRay = new Ray(hRayPosition, ledgeRayH.getWorldRotation().mult(Vector3f.UNIT_Z));
                    debugTools.setBlueArrow(hRay.getOrigin(), hRay.getDirection());

                    if (Physics.Raycast(hRay, hitInfo, 2)) {
                        System.out.println(hitInfo);
                        debugTools.setPinkArrow(hitInfo.point, hitInfo.normal);

                        goalPosition.set(hitInfo.point.add(0, 0.01f, 0));

                        bcc.setViewDirection(hitInfo.normal.negate()); // align with wall
                        bcc.setWalkDirection(Vector3f.ZERO); // stop walking
//                        bcc.getRigidBody().setKinematic(true);
//                        bcc.setEnabled(false);

                        //helper.setTranslation(hitInfo.normal.negate().multLocal(hDistAwayFromLedge).addLocal(spatial.getWorldTranslation()));
                        //helper.getTranslation().setY(hitInfo.point.y - vDistAwayFromLedge);
                        //helper.setRotation(FRotator.lookRotation(hitInfo.normal.negate()));
                        setAnimation(AnimDefs.Climbing);

                        isClimbingMode = true;
                        startClimb = true;
                        isClimbingAnimDone = false;
                        System.out.println("startClimbing");
                    }
                }
            } else {
                isClimbingMode = false;
                bcc.getRigidBody().setKinematic(false);
//                bcc.setEnabled(true);
            }
        }

        void onClimbingDone() {
            isClimbingAnimDone = true;
            System.out.println("climbingDone");
        }
        
        private void updateMovement(float tpf) {

            camera.getDirection(camDir).setY(0);
            camera.getLeft(camLeft).setY(0);
            walkDirection.set(0, 0, 0);

            if (_MoveForward) {
                walkDirection.addLocal(camDir);
            } else if (_MoveBackward) {
                walkDirection.addLocal(camDir.negateLocal());
            }

            if (_MoveLeft) {
                walkDirection.addLocal(camLeft);
            } else if (_MoveRight) {
                walkDirection.addLocal(camLeft.negateLocal());
            }

            walkDirection.normalizeLocal();
            boolean isMoving = walkDirection.lengthSquared() > 0;

            if (isMoving) {
                float angle = FastMath.atan2(walkDirection.x, walkDirection.z);
                lookRotation.fromAngleNormalAxis(angle, Vector3f.UNIT_Y);
                spatial.getWorldRotation().slerp(lookRotation, m_TurnSpeed * tpf);
                spatial.getWorldRotation().mult(Vector3f.UNIT_Z, viewDirection);
                bcc.setViewDirection(viewDirection);
            }
            
            bcc.setWalkDirection(walkDirection.multLocal(m_MoveSpeed));
            setAnimation(isMoving ? AnimDefs.Running : AnimDefs.Idle);
        }
        
        private void setAnimation(String animName) {
            if (animComposer.getCurrentAction() != animComposer.getAction(animName)) {
                animComposer.setCurrentAction(animName);
            }
        }
        
        private void stopMove() {
            _MoveForward   = false;
            _MoveBackward  = false;
            _MoveLeft      = false;
            _MoveRight     = false;
        }

        @Override
        protected void controlRender(RenderManager rm, ViewPort vp) {
            if (debugTools != null) {
                debugTools.show(rm, vp);
            }
        }
        
        /**
         * Custom Keybinding: Map named actions to inputs.
         */
        private void registerWithInput(InputManager inputManager) {
            this.inputManager = inputManager;
            
            addMapping(InputMapping.MOVE_FORWARD, new KeyTrigger(KeyInput.KEY_W));
            addMapping(InputMapping.MOVE_BACKWARD, new KeyTrigger(KeyInput.KEY_S));
            addMapping(InputMapping.MOVE_LEFT, new KeyTrigger(KeyInput.KEY_A));
            addMapping(InputMapping.MOVE_RIGHT, new KeyTrigger(KeyInput.KEY_D));
            addMapping(InputMapping.ACTION, new KeyTrigger(KeyInput.KEY_SPACE));
        }

        private void addMapping(String mapping, Trigger... triggers) {
            inputManager.addMapping(mapping, triggers);
            inputManager.addListener(this, mapping);
        }

    }
    
    private interface InputMapping {

        final String MOVE_LEFT = "MOVE_LEFT";
        final String MOVE_RIGHT = "MOVE_RIGHT";
        final String MOVE_FORWARD = "MOVE_FORWARD";
        final String MOVE_BACKWARD = "MOVE_BACKWARD";
        final String ACTION = "ACTION";
    }
    
//	private class RootMotion implements Tween {
//
//		private final TransformTrack track;
//		private final Spatial spatial;
//		private final Transform transform = new Transform();
//
//		private Vector3f startLoc;
//
//		public RootMotion(TransformTrack track) {
//			this.track = track;
//			if (!(track.getTarget() instanceof Spatial)) {
//				throw new IllegalArgumentException("Target of root motion track must be a spatial.");
//			}
//
//			this.spatial = (Spatial) track.getTarget();
//		}
//
//		@Override
//		public double getLength() {
//			return track.getLength();
//		}
//
//		@Override
//		public boolean interpolate(double t) {
//			if (t > getLength()) {
//				startLoc = null;
//				return false;
//			}
//
//			if (startLoc == null) {
//				startLoc = spatial.getLocalTranslation().clone();
//			}
//
//			track.getDataAtTime(t, transform);
//			Vector3f newLocation = startLoc.add(transform.getTranslation());
//			spatial.setLocalTranslation(newLocation);
//			return true;
//		}
//	}

}

EDIT:

I like the RootMotion class you wrote. We can use it to compact the final solution and make it reusable for any situation.:wink:

excellent idea!

3 Likes