AnimStateMachine

Hi, as I am working on animations, I have created a very rudimentary blend tree implementation. I’m currently working a lot on this. If anyone is interested, I am happy to explain more in detail.

Basic idea: separation of concerns, I don’t want animation code in a character controller, so I added a control that pulls information from a character instead of pushing it from the character control code.

A first implementation would look like:

public class SinbadAnimControl extends AbstractControl {

    AnimBlendTree blendTree = new AnimBlendTree();

    @Override
    public void setSpatial(Spatial spatial) {
        super.setSpatial(spatial);
        spatial.addControl(blendTree);

        AnimComposer animComposer = this.getSpatial().getControl(AnimComposer.class);

        ActionState idleState = blendTree.addState("idle", new ClipAction(animComposer.getAnimClip("IdleTop")));

        EventAction jumpStart = new EventAction(new ClipAction(animComposer.getAnimClip("JumpStart")));
        jumpStart.setSpeed(0.8f);
        ActionState startJumpingState = blendTree.addState("start jumping", jumpStart);

        ActionState inAirState = blendTree.addState("in air", new ClipAction(animComposer.getAnimClip("JumpLoop")));

        ClipAction runAction = new ClipAction(animComposer.getAnimClip("RunBase"));
        ActionState runningState = blendTree.addState("running", runAction);

        Link idleToStartJumping = blendTree.addLink(idleState, startJumpingState);
        Link startJumpingToInAir = blendTree.addLink(startJumpingState, inAirState);
        Link inAirToIdle = blendTree.addLink(inAirState, idleState);
        Link idleToRunning = blendTree.addLink(idleState, runningState);
        Link runningToIdle = blendTree.addLink(runningState, idleState);
        Link runningToStartJumping = blendTree.addLink(runningState, startJumpingState);

        blendTree.setDefaultState(idleState);
        BetterCharacterControl characterControl = this.getSpatial().getParent().getControl(BetterCharacterControl.class);

        BooleanSupplier didJump = () -> !characterControl.isOnGround() && characterControl.getVelocity().y > 0;
        idleToStartJumping.setCondition(didJump);
        runningToStartJumping.setCondition(didJump);

        startJumpingToInAir.setCondition(() -> true);
        inAirToIdle.setCondition(characterControl::isOnGround);

        Supplier<Float> getForwardVelocity = () -> characterControl.getSpatial()
            .getLocalRotation()
            .inverse()
            .mult(characterControl.getVelocity())
            .z;

        idleToRunning.setCondition(() -> {
            if (!characterControl.isOnGround()) return false;
            return getForwardVelocity.get() >= 0.01f;
        });

        runningToIdle.setCondition(() -> {
            if (!characterControl.isOnGround()) return false;
            return getForwardVelocity.get() < 0.01f;
        });
    }

    @Override
    protected void controlUpdate(float tpf) {

    }

    @Override
    protected void controlRender(RenderManager rm, ViewPort vp) {

    }
}

This way I’m able to control which animations are played when and at what speed and other things of the animations based on parameters from the character controller. The main advantage is that this information is pulled when the AnimBlendTree control find it necessary to pull this information.

The result of the above code: AnimBlendTree example

5 Likes

Nice

A suggestion: :slightly_smiling_face:

I think naming it a blend tree is a bit confusing because anim blending is something different and you can already do that with the new animation system (even though it’s lacking some features, atm).

IMHO anim state machine might be better. :stuck_out_tongue:

You can get some inspiration from this topic. Especially the state transition manager part.

It’s from an older (unofficial) animation system which later replaced by the new API we have today but the state transition thing has never implemented into the new one.

2 Likes

I have uploaded a first version on Github :slight_smile:, GitHub - daBlesr/jme-anim-state-machine: jMonkeyEngine Animation State Machine . Do note, it is a first version :stuck_out_tongue: , but if you have any remarks or feature requests, please let me know!

3 Likes

Hi everyone, I was also working on a prototype of AnimStateMachine. I did some experiments for fun and would like to share the result with you.
The basic idea is similar to @daBlesr but I would like to keep the physics and any other functions decoupled from the animations.
Multilayer is not yet supported.
I have documented almost all the functions and in my opinion you could easily build a Visual Editor on top of it.

Usage Example 1: Int and Float parameters

Int and Float variables only work with a threshold and

  • AnimatorConditionMode.Greater
  • AnimatorConditionMode.Less
  • AnimatorConditionMode.Equals
  • AnimatorConditionMode.NotEquals
// Create the controller and add it to the player node. The controller will look for the AnimComposer in the node and its children.
AnimatorController animator = new AnimatorController();
player.addControl(animator);

// Create the parameters by which you want to control the transitions between states.
animator.addParameter("moveSpeed", AnimatorControllerParameterType.Float);

// Define states for animations.
AnimatorStateMachine sm = animator.getStateMachine();
AnimatorState idle = sm.addState("IdleState", "idle");
AnimatorState walk = sm.addState("WalkState", "walking_inPlace");

// Define the transitions and conditions for each state using the previously created parameters.
AnimatorStateTransition idleToWalk = idle.addTransition(walk);
// changes state when the 'moveSpeed' parameter is greater than 1 
idleToWalk.addCondition(AnimatorConditionMode.Greater, 1f, "moveSpeed");

AnimatorStateTransition walkToIdle = walk.addTransition(idle);
// changes state when the 'moveSpeed' parameter is less than 0.8 
walkToIdle.addCondition(AnimatorConditionMode.Less, 0.8f, "moveSpeed");

// Don't forget to set an initial state.
sm.setDefaultState(idle);

Now you can control the state machine through the parameters you defined earlier…

public class PlayerControl extends AbstractControl {
	
	private AnimatorController animator;
	
	...
	
	@Override
	public void controlUpdate(float tpf) {

		camera.getDirection(cameraDir).setY(0);
		camera.getLeft(cameraLeft).setY(0);

		walkDirection.set(0, 0, 0);

		if (_MoveForward) {
			walkDirection.addLocal(cameraDir);
		} else if (_MoveBackward) {
			walkDirection.subtractLocal(cameraDir);
		}
		if (_TurnLeft) {
			walkDirection.addLocal(cameraLeft);
		} else if (_TurnRight) {
			walkDirection.subtractLocal(cameraLeft);
		}

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

		if (isMoving) {
			// smooth rotation
			float angle = FastMath.atan2(walkDirection.x, walkDirection.z);
			dr.fromAngleNormalAxis(angle, Vector3f.UNIT_Y);
			spatial.getWorldRotation().slerp(dr, m_TurnSpeed * tpf);
			bcc.setViewDirection(spatial.getWorldRotation().mult(Vector3f.UNIT_Z));
		}
		
		bcc.setWalkDirection(walkDirection.multLocal(m_MoveSpeed));
		
		// set the parameter value 
		animator.setFloat("moveSpeed", bcc.getVelocity().length());
	}
}

Usage Example 2: Boolean parameter

Boolean variables only work with AnimatorConditionMode.If and AnimatorConditionMode.IfNot.

AnimatorController animator = new AnimatorController();
player.addControl(animator)

animator.addParameter("isRunning", AnimatorControllerParameterType.Bool);

// Define states for animations.
AnimatorStateMachine sm = animator.getStateMachine();
AnimatorState idle = sm.addState("IdleState", "idle");
AnimatorState run = sm.addState("RunnigState", "running_inPlace");

AnimatorStateTransition idleToRun = idle.addTransition(run);
// change state when 'isRunning' parameter is equals to true
// the numerical threshold can be set to zero because it will be ignored
idleToRun.addCondition(AnimatorConditionMode.If, 0, "isRunning"); 

AnimatorStateTransition runToIdle = run.addTransition(idle);
// change state when 'isRunning' parameter is equals to false
// the numerical threshold can be set to zero because it will be ignored
runToIdle.addCondition(AnimatorConditionMode.IfNot, 0, "isRunning"); 

// set an initial state.
sm.setDefaultState(idle);

// Then activate the transition in the PlayerControl
animator.setBool("isRunning", true);

You can now access the AnimatorController variables to perform other actions such as turning on / off the sound of footsteps

public class FootstepsControl extends AbstractControl {
	
	private AnimatorController animator;
	private AudioNode footstepsSFX;
	
	...
	
	@Override
	public void controlUpdate(float tpf) {
		if (animator.getBool("isRunning")) {
			footstepsSFX.play();
		} else {
			footstepsSFX.stop();
		}
	}
	
}

Usage example 3: Trigger parameter

Trigger variables only work with AnimatorConditionMode.If
If you have to wait for an animation to finish before transitioning to another state, specify in the transition the percentage of completion between 0 and 1 that the animation must perform before changing state.

AnimatorController animator = new AnimatorController();
player.addControl(animator);

animator.addParameter("isJumping", AnimatorControllerParameterType.Trigger);
		
// Define states for animations.
AnimatorStateMachine sm = animator.getStateMachine();
AnimatorState idle = sm.addState("IdleState", "idle");
AnimatorState jump = sm.addState("JumpState", "jump_inPlace");

AnimatorStateTransition idleToJump = idle.addTransition(jump);
// changes state when the 'isJumping' parameter is activated by the trigger 
// the numerical threshold can be set to zero because it will be ignored
runToJump.addCondition(AnimatorConditionMode.If, 0, "isJumping"); 

// execute 90% of the jump animation before returning to idle state
AnimatorStateTransition jumpToIdle = jump.addTransition(idle, 0.90f); 

// set an initial state.
sm.setDefaultState(idle);

// Then activate the trigger in the PlayerControl

public class PlayerControl extends AbstractControl implements ActionListener {

	@Override
	public void onAction(String name, boolean isPressed, float tpf) {
		//To change body of generated methods, choose Tools | Templates.
		if (name.equals(InputMapping.JUMP) && isPressed) {
			animator.setTrigger("isJumping");
		}
	}
	
	...
}	

The source code is available on my github account, where you can find other useful features.

I wrote a demo here:

At the moment it is only a prototype. Let me know if it can be useful and write me your ideas to improve it.

Have fun :wink:

Edit: Code and examples are inspired by the following video which explains very well what I have described to you.

3 Likes

Haha very nice! I see the similarity of the float/boolean input parameter with Mechanim indeed. A visual tool would be very nice but unfortunately QT’s open source libraries of (good looking) node editors are all in c++ :frowning:.

I was writing a complicated (read: tangled code with lots of if else branches) controller similar to Bettercharactercontrol for the dragons, and guess what… input almost have the same “states” as animations where the state needs to act on the situation and possibly move to a new state :see_no_evil:. So I had some ideas of stretching the idea of a state machine further than just animations.

The book Game Programming Patterns has a nice chapter on this, and I’m currently implementing his ideas where you have layered state machines, and move to a new state if:

  • Input is registered
  • animations have completed
  • the situation has changed in a prephysics tick, e.g. dragon is landing

Programming Patterns - State - Bob Nystrom

3 Likes

Yes, I know that book too, it’s really very useful. Your idea is interesting, go ahead and show us your progress :wink:

It is true that a simpler and more flexible graphics editor would help a lot. I noticed you developed one yourself in the videos you showed right? I am working on a tool similar to that of Unity with the Hierarchy window and the Inspector window. I am trying to develop it with the idea that it must be simple to maintain (for those who want to expand it) and to use. It must work during the game and must be outside the game window. Obviously it cannot have all the functionality that Unity has, but it is based on the same SerializeField annotations that you put on top of the class variables in the MonoBehavior. If the result is good, I’ll share it on github.

2 Likes