Game Jam 01: Climbing System

Yes, looks nice as well.

But you should change this line in toInPlaceAnimation

for (int i = 0; i < transformTrack.getLength(); i++) {

to

for (int i = 0; i < transformTrack.getTranslations().length; i++) {

else it will throw

java.lang.AssertionError
	at com.jme3.anim.TransformTrack.setKeyframesTranslation(TransformTrack.java:149)
	at com.capdevon.demo.Test_Climbing$PlayerControl.toInPlaceAnimation(Test_Climbing.java:352)
	at com.capdevon.demo.Test_Climbing$PlayerControl.setSpatial(Test_Climbing.java:310)
	at com.jme3.scene.Spatial.addControl(Spatial.java:777)
	at com.capdevon.demo.Test_Climbing.setupPlayer(Test_Climbing.java:203)
	at com.capdevon.demo.Test_Climbing.simpleInitApp(Test_Climbing.java:104)
	at com.jme3.app.SimpleApplication.initialize(SimpleApplication.java:240)
	at com.jme3.system.lwjgl.LwjglAbstractDisplay.initInThread(LwjglAbstractDisplay.java:139)
	at com.jme3.system.lwjgl.LwjglAbstractDisplay.run(LwjglAbstractDisplay.java:221)
	at java.base/java.lang.Thread.run(Thread.java:835)

transformTrack.getLength() will return the duration (in seconds) of the track.

2 Likes

you are right :+1:

I see the collision capsule is not moving with the character. For a generic root motion solution that needs to be done as well :stuck_out_tongue:

Also, I see only translations are taking account for root motion. Root motion also contains rotations. For example a turn left animation. Preferably you would be able to run the root motion animation without applying manual rotations to the spatial.

1 Like

How can I tell bullet to get the transform from the spatial?

Edit:
If I try to enable Kinematic mode it will throw an assertion error:

java.lang.AssertionError
	at com.jme3.bullet.objects.PhysicsRigidBody.getLinearVelocity(PhysicsRigidBody.java:487)
	at com.jme3.bullet.control.BetterCharacterControl.physicsTick(BetterCharacterControl.java:665)
	at com.jme3.bullet.PhysicsSpace.postTick_native(PhysicsSpace.java:1197)
	at com.jme3.bullet.PhysicsSpace.stepSimulation(Native Method)
	at com.jme3.bullet.PhysicsSpace.update(PhysicsSpace.java:825)
	at com.jme3.bullet.BulletAppState.render(BulletAppState.java:802)
	at com.jme3.app.state.AppStateManager.render(AppStateManager.java:388)
	at com.jme3.app.SimpleApplication.update(SimpleApplication.java:270)
	at com.jme3.system.lwjgl.LwjglAbstractDisplay.runLoop(LwjglAbstractDisplay.java:160)
	at com.jme3.system.lwjgl.LwjglDisplay.runLoop(LwjglDisplay.java:201)
	at com.jme3.system.lwjgl.LwjglAbstractDisplay.run(LwjglAbstractDisplay.java:242)
	at java.base/java.lang.Thread.run(Thread.java:835)

By the way, the below line is incorrect. It does not work as you expect it to work, because the spatial is moving.

Vector3f vec = animComposer.getSpatial().localToWorld(rootMotion.getTranslation(), null);

Strangely it works in your example, I think it is because physics is overwriting the location. If you disable BCC when the climb starts you will see character will fly to the air :wink:

Edit:
That’s why in RootMotion class I get the startLoc at the beginning and keep adding to it instead.

1 Like

Yes I had noticed it too, but I did not understand why. How do we solve it? Meanwhile, I added the extraction of rotations from the animation to apply them to the character node.

		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()) {
							
							cleanTranslation(transformTrack);
							cleanRotation(transformTrack);
						}
					}
				}
			}
		}
		
		/**
		 * Convert it to an in-place animation by removing translations data
		 * @param transformTrack
		 */
		private void cleanTranslation(TransformTrack transformTrack) {
			CompactVector3Array translation = new CompactVector3Array();
			for (int i = 0; i < transformTrack.getTranslations().length; i++) {
				translation.add(Vector3f.ZERO.clone());
			}
			transformTrack.setKeyframesTranslation(translation.toObjectArray());
		}
		
		/**
		 * Convert it to an in-place animation by removing rotations data
		 * @param transformTrack
		 */
		private void cleanRotation(TransformTrack transformTrack) {
			CompactQuaternionArray rotation = new CompactQuaternionArray();
			for (int i = 0; i < transformTrack.getRotations().length; i++) {
				rotation.add(Quaternion.IDENTITY.clone());
			}
			transformTrack.setKeyframesRotation(rotation.toObjectArray());
		}

I reapply the rotation taking into account the orientation of the character.

        @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(rootMotion.getTranslation()); // ??? doesn't work
                	spatial.getLocalRotation().multLocal(rootMotion.getRotation());
                	
                } else if (isClimbingAnimDone) {
                    isClimbingMode = false;
                    startClimb = false;
                    //spatial.setLocalTranslation(goalPosition);
//                    bcc.getRigidBody().setKinematic(false);
//                    bcc.setEnabled(true);
                    bcc.warp(goalPosition);
                }
            }
        }

I mean that is how scene-graph is working. If a node moves, everything inside it will move also so while their local translation is the same but their world translation will change.

Physics objects can only be properly moved using physics functions afaik. setPhysicsRotation, setPhysicsLocation, setLinearVelocity, etc.

One caveat, bettercharactercontrol will also update your physics, and will cause trouble probably.
Best to write your own ClimbableEvenBetterCharacterControl :wink:

Fixed it so the collision capsule is now moving with the character.

For root motion, I am using a RigidBodyControl which is set to Kinematic mode and gets enabled only when root motion is running.

This is how I am setting up the climbing animation:

3 Likes

Great job, we can do a little optimization to stabilize the camera. Hooking the ChaseCamera to the model instead of the player node the result is more fluid and natural. However, a slight flickering of the image remains to be eliminated, probably caused by the final teleportation of the bcc with the warp method

// setup third person camera
setupChaseCamera(model);
    private void setupChaseCamera(Spatial target) {
        // disable the default 1st-person flyCam!
        stateManager.detach(stateManager.getState(FlyCamAppState.class));
        flyCam.setEnabled(false);

        ChaseCamera chaseCam = new ChaseCamera(cam, target, 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);
    }

Edit: Progress update pt3,
we are close to the goal :wink:

5 Likes

A question about this though, while I use mixamo to build my own animations I don’t or almost never use them in their vanilla state, I have also been actively negating the root motions to create “in place” animations for all my stuff because I think it’s safer for me to manage all locomotion in jme3 itself would this still work without root motion, also how are the animations built/managed climb loop > pull up/vaulting separately or is in one animation file.

additional question how would u handle max climb distance and situations that would stop a climb, I am interested in this except that I want to d a vertical wall run thing since I mostly want the hero character to maintain a certain momentum as much as possible

Hi @smith,
the approach we are using is this: extract the Root Motion of the character animation and reapply the same motion to the character’s collision capsule or in our case to the character node. This article explains really well what we are talking about.

All animations are in-place except the climbing one. I downloaded the animations from the Mixamo site in fbx format and then I included all the tracks in a single gltf file with Blender.

Given the difficulty of the problem, I have simplified some conditions to give an achievable goal and write a first basic solution with you. For this reason I assumed that the height of the obstacles is fixed and determined by the height of the climbing animation. As soon as we have a stable solution, we will try to do further experiments.

To identify which obstacles are scalable and which are not there are a lot of techniques. In this video they use Raycast to identify the surfaces along which the character can move. https://www.youtube.com/watch?v=O91pjnCWOl0

The code, the animations and all the files of this game-jam is at your complete disposal. You can study it, modify it, add your ideas and share them with us if you like.

4 Likes

Oh I love this, really cool. Will this lead to some new features in jme3 (I didn’t yet read it through)?

Hi @ia97lies, we’re still working on it. For sure, if it were integrated into jme3, it would be a very useful feature and would give access to many new possibilities for video game development.

small somewhat unrelated note you can already kinda climb in jme, well more like teleport up slopes based on a tolerance, I set a high step height on CharacterController and a 90 degree max slope,
literally walked “sort of” “all over” Town.zip

character stepHeight of 50+ and
character.setMaxSlope(1.5708f); 90 degrees jmephysics
character.getCharacter().setMaxSlope(1.570796f) minie

l

Notes this is with CC not BCC, something similar might make it possible to not rely on root motion for climbing

Edit: the Teleport seems to only occur on slopes that are 90 degrees did not test for tolerance 80s and such otherwise
maybe a solution can be found somewhere between CC and BCC

2 Likes

I always enjoy hearing new ideas, especially if accompanied by a demonstration video. Go ahead and see if it’s the right way :wink:

1 Like

Well I am not the best coder, never truly dedicated my myself to the craft, but I have been looking into cobbling together a controller that does everything I need or as much as possible, fact is, other than the crouch, I don’t think that BCC is actually “Better” than CC, try getting up or over the smallest obstacle with that. I am trying to build rigid body control with some CC features and the useful bit of BCC, will see how that goes…just thought you guys should be aware of this…if you weren’t already, I hit upon it while looking at the code to get tips for my ideas

1 Like

adjusted my above post to include minie compatible call, minie also doesn’t take kindly to rounding max slope