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?