A tip for animation blending!

I tried to extend the BlendAction class but the code that stretches the animations is in the constructor, so I discarded this option. The MyBlendSpace class cannot implement the BlendSpace interface because the method setBlendAction(BlendAction action) expects a BlendAction which I cannot extend for the above reasons. In the end I opted for the fastest way, here are the 2 classes:

MyBlendAction

package com.capdevon.anim.fsm;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import com.jme3.anim.tween.action.Action;
import com.jme3.anim.tween.action.BlendableAction;
import com.jme3.anim.util.HasLocalTransform;
import com.jme3.math.Transform;

public class MyBlendAction extends BlendableAction {

    private int firstActiveIndex;
    private int secondActiveIndex;
    private final MyBlendSpace blendSpace;
    private final BlendMode blendMode;
    private float blendWeight;
    private double lastTime;
    // In stretch mode it contains time factor and in loop mode it contains the time
    final private double[] timeData;
    final private Map<HasLocalTransform, Transform> targetMap = new HashMap<>();

    public enum BlendMode {Stretch, Loop}

    public MyBlendAction(MyBlendSpace blendSpace, BlendableAction... actions) {
        this(blendSpace, BlendMode.Loop, actions);
    }

    public MyBlendAction(MyBlendSpace blendSpace, BlendMode blendMode, BlendableAction... actions) {
        super(actions);
        this.blendMode = blendMode;
        timeData = new double[actions.length];
        this.blendSpace = blendSpace;
        blendSpace.setBlendAction(this);

        for (BlendableAction action : actions) {
            if (action.getLength() > getLength()) {
                setLength(action.getLength());
            }
            Collection<HasLocalTransform> targets = action.getTargets();
            for (HasLocalTransform target : targets) {
                Transform t = targetMap.get(target);
                if (t == null) {
                    t = new Transform();
                    targetMap.put(target, t);
                }
            }
        }

        if (blendMode == BlendMode.Stretch) {
            //Blending effect maybe unexpected when blended animation don't have the same length
            //Stretching any action that doesn't have the same length.
            for (int i = 0; i < this.actions.length; i++) {
                this.timeData[i] = 1;
                if (this.actions[i].getLength() != getLength()) {
                    double actionLength = this.actions[i].getLength();
                    if (actionLength > 0 && getLength() > 0) {
                        this.timeData[i] = this.actions[i].getLength() / getLength();
                    }
                }
            }
        }
    }
    
    @Override
	public boolean interpolate(double t) {
		boolean interpolate = super.interpolate(t);
		if (!interpolate) {
			lastTime = 0;
		}
		return interpolate;
	}

    @Override
    public void doInterpolate(double t) {
        blendWeight = blendSpace.getWeight();
        BlendableAction firstActiveAction = (BlendableAction) actions[firstActiveIndex];
        BlendableAction secondActiveAction = (BlendableAction) actions[secondActiveIndex];
        firstActiveAction.setCollectTransformDelegate(this);
        secondActiveAction.setCollectTransformDelegate(this);

        //only interpolate the first action if the weight if below 1.
        if (blendWeight < 1f) {
            firstActiveAction.setWeight(1f);
            //firstActiveAction.interpolate(t * timeFactor[firstActiveIndex]);
            interpolate(firstActiveAction, firstActiveIndex, t);
            if (blendWeight == 0) {
                for (HasLocalTransform target : targetMap.keySet()) {
                    collect(target, targetMap.get(target));
                }
            }
        }

        //Second action should be interpolated
        secondActiveAction.setWeight(blendWeight);
        //secondActiveAction.interpolate(t * timeFactor[secondActiveIndex]);
        interpolate(secondActiveAction, secondActiveIndex, t);

        firstActiveAction.setCollectTransformDelegate(null);
        secondActiveAction.setCollectTransformDelegate(null);

        lastTime = t;
    }

	private void interpolate(BlendableAction action, int index, double time) {
		if (blendMode == BlendMode.Stretch) {
			// In stretch mode timeData represents time factor
			action.interpolate(time * timeData[index]);
		} else { // Loop mode
			double tpf = time - lastTime;
			timeData[index] += tpf;
			// In loop mode timeData represents time
			if (!action.interpolate(timeData[index])) {
				timeData[index] = 0;
			}
		}
	}

    protected Action[] getActions() {
        return actions;
    }

    public MyBlendSpace getBlendSpace() {
        return blendSpace;
    }

    protected void setFirstActiveIndex(int index) {
        this.firstActiveIndex = index;
    }

    protected void setSecondActiveIndex(int index) {
        this.secondActiveIndex = index;
    }

    @Override
    public Collection<HasLocalTransform> getTargets() {
        return targetMap.keySet();
    }

    @Override
    public void collectTransform(HasLocalTransform target, Transform t, float weight, BlendableAction source) {

        Transform tr = targetMap.get(target);
        if (weight == 1) {
            tr.set(t);
        } else if (weight > 0) {
            tr.interpolateTransforms(tr, t, weight);
        }

        if (source == actions[secondActiveIndex]) {
            collect(target, tr);
        }
    }

    private void collect(HasLocalTransform target, Transform tr) {
        if (collectTransformDelegate != null) {
            collectTransformDelegate.collectTransform(target, tr, this.getWeight(), this);
        } else {
            if (getTransitionWeight() == 1) {
                target.setLocalTransform(tr);
            } else {
                Transform trans = target.getLocalTransform();
                trans.interpolateTransforms(trans, tr, getTransitionWeight());
                target.setLocalTransform(trans);
            }
        }
    }

}

MyBlendSpace

package com.capdevon.anim.fsm;

import com.jme3.anim.tween.action.Action;

public class MyBlendSpace {

    private MyBlendAction action;
    private float value;
    final private float maxValue;
    final private float minValue;
    private float step;

    public MyBlendSpace(float minValue, float maxValue) {
        this.maxValue = maxValue;
        this.minValue = minValue;
    }

//    @Override
    public void setBlendAction(MyBlendAction action) {
        this.action = action;
        Action[] actions = action.getActions();
        step = (maxValue - minValue) / (actions.length - 1);
    }

//    @Override
    public float getWeight() {
        Action[] actions = action.getActions();
        float lowStep = minValue, highStep = minValue;
        int lowIndex = 0, highIndex = 0;
        for (int i = 0; i < actions.length && highStep < value; i++) {
            lowStep = highStep;
            lowIndex = i;
            highStep += step;
        }
        highIndex = lowIndex + 1;

        action.setFirstActiveIndex(lowIndex);
        action.setSecondActiveIndex(highIndex);

        if (highStep == lowStep) {
            return 0;
        }

        return (value - lowStep) / (highStep - lowStep);
    }

//    @Override
    public void setValue(float value) {
        this.value = value;
    }
}

Here are the new configuration parts of my test:

MyBlendSpace blendSpace = new MyBlendSpace(0, 1);
String[] clips = { "walking", "running" };
BlendableAction[] acts = new BlendableAction[clips.length];
for (int i = 0; i < acts.length; i++) {
	BlendableAction ba = (BlendableAction) animComposer.makeAction(clips[i]);
	acts[i] = ba;
}

MyBlendAction action = new MyBlendAction(blendSpace, acts);
animComposer.addAction("BlendTree", action);

And here the new parts of the player controller

public class PlayerMovementControl extends AbstractControl implements ActionListener {

	public float m_MoveSpeed = 4.5f;
	public float m_TurnSpeed = 10f;
	public float velocity = 0;
	public float acceleration = 0.4f;
	public float deceleration = 1f;

	private boolean _MoveForward, _MoveBackward, _TurnLeft, _TurnRight;

	...

	@Override
	public void onAction(String name, boolean isPressed, float tpf) {
		if (name.equals(InputMapping.MOVE_FORWARD)) {
			_MoveForward = isPressed;
		} else if (name.equals(InputMapping.MOVE_BACKWARD)) {
			_MoveBackward = isPressed;
		} else if (name.equals(InputMapping.MOVE_LEFT)) {
			_TurnLeft = isPressed;
		} else if (name.equals(InputMapping.MOVE_RIGHT)) {
			_TurnRight = isPressed;
		}
	}

	@Override
	public void controlUpdate(float tpf) {
		...

		if (isMoving) {
			velocity += acceleration * tpf;
		} else {
			velocity -= deceleration * tpf;
		}
		
		velocity = FastMath.clamp(velocity, 0, 1);
		
		MyBlendAction action = (MyBlendAction) animComposer.getAction("BlendTree");
		action.getBlendSpace().setValue(velocity );
	}

	...

}

But maybe there is still a problem with the execution of the two animations.
Sometimes the animation freezes or skips frames. It is not perfectly fluid. Am I forgetting something?

Could you paste your test here please?