Overview
A blend space mixes several looping animations based on values you supply every frame. You place each animation at a point, such as idle at speed 0, walk at speed 2 and run at speed 6, and the blend space plays the right mix for the current speed: at speed 4, half walk and half run.
Use them for anything that changes smoothly instead of switching:
- 1D: movement speed (idle, walk, run), aim pitch (aim down, level, up), lean amount.
- 2D: strafing (forward, back, left, right), aim yaw and pitch, swim direction.
Without a blend space you'd add a controller per animation and work out all their weights yourself. A blend space does that for you.
How It Works
Each animation in the blend space plays on its own controller. Every frame, just before the controllers update, the blend space:
- Reads your input and works out how much each animation counts for (all the shares add up to 1).
- Sets the controllers' weights so that, layered on top of each other, they produce exactly that mix.
- Keeps all the animations on one shared cycle, so a walk and a run blended together keep their feet in step.
Because the result is ordinary layered controllers, anything you add after the blend space layers on top of it, such as an attack on the upper body.
1D Blend Spaces
Build the blend space in registerControllers and add it to the container where it should sit in the layering:
public class ExampleEntityAnimator extends AzEntityAnimator<ExampleEntity> {@Overridepublic void registerControllers(AzAnimationControllerContainer<ExampleEntity> container) {AzBlendSpace1D.builder(this, "locomotion").input((entity, partialTick) -> entity.getDeltaMovement().horizontalDistance() * 20).sample(0, "idle").sample(2, "walk").sample(6, "run").smoothing(3).build().addTo(container);container.add(AzAnimationController.builder(this, "attack").setBoneMask(AzBoneMask.only("upper_body")).build());}// ... getAnimationLocation, etc.}
inputreturns the value to blend by each frame; here, horizontal speed in blocks per second.sample(position, animation)places a looping animation at a point. The order you add them in doesn't matter.- Below the first sample or above the last, the nearest sample plays on its own.
2D Blend Spaces
A 2D blend space takes two inputs and samples placed anywhere on a plane:
AzBlendSpace2D.builder(this, "strafe").input((entity, partialTick) -> entity.getStrafeSpeed(), // X: left (-) to right (+)(entity, partialTick) -> entity.getForwardSpeed() // Y: back (-) to forward (+)).sample(0, 0, "idle").sample(0, 1, "walk_forward").sample(0, -1, "walk_backward").sample(-1, 0, "walk_left").sample(1, 0, "walk_right").smoothing(2).build().addTo(container);
When the input lands exactly on a sample, that animation plays alone. Between samples, each sample's share falls off smoothly towards its neighbours, so walking diagonally forward-right blends walk_forward and walk_right. Samples don't need to be on a grid; add run_forward at (0, 2) and the space grows to fit.
getStrafeSpeed() and getForwardSpeed() above are your own methods. For an entity, you can get them from its movement relative to where it's facing:
public double getForwardSpeed() {var motion = getDeltaMovement();var yaw = Math.toRadians(yBodyRot);return (-Math.sin(yaw) * motion.x + Math.cos(yaw) * motion.z) * 20;}public double getStrafeSpeed() {var motion = getDeltaMovement();var yaw = Math.toRadians(yBodyRot);return -(Math.cos(yaw) * motion.x + Math.sin(yaw) * motion.z) * 20;}
Options
Both builders share these options:
| Builder method | Default | What it does |
|---|---|---|
smoothing(ticks) | 0 (off) | Eases the input towards new values with this time constant, so sudden changes in speed don't snap the blend |
syncPhase(boolean) | true | Keeps all samples on one shared cycle. Turn it off for samples that aren't cycles of the same motion, such as aim poses |
controller(builder -> ...) | none | Configures every sample controller, for example a bone mask or transition length |
AzBlendSpace1D.builder(this, "aim").input((entity, partialTick) -> entity.getViewXRot(partialTick)).sample(-60, "aim_up").sample(0, "aim_level").sample(60, "aim_down").syncPhase(false).controller(builder -> builder.setBoneMask(AzBoneMask.only("upper_body"))).build().addTo(container);
Phase Sync
Walk and run cycles usually have different lengths. Blending them while each plays at its own pace makes the feet slide and stutter. With phase sync on, every sample plays at the same point in its cycle (say, 30% through), and the shared cycle moves at the weighted average of the samples' speeds. Halfway between a 20 tick walk and a 12 tick run, both play at a 16 tick pace with their feet in step.
For phase sync to look right, start every cycle on the same foot.
Setting Inputs Yourself
Leave out input and set it from anywhere on the client instead. Keep the blend space in a field so you can reach it:
private AzBlendSpace1D<ExampleEntity> locomotion;@Overridepublic void registerControllers(AzAnimationControllerContainer<ExampleEntity> container) {locomotion = AzBlendSpace1D.builder(this, "locomotion").sample(0, "idle").sample(2, "walk").sample(6, "run").build();locomotion.addTo(container);}// Later, for example in setCustomAnimations:locomotion.setInput(speed);
AzBlendSpace2D has setInput(x, y). input() (inputX() and inputY() in 2D) returns the smoothed value as of the last frame, and weights() returns each sample's share, which is handy for debugging.
Fading the Whole Blend
A blend space can be weighted as a whole, so you can fade locomotion out while a full-body animation on a later controller takes over:
locomotion.fadeWeight(0, 5); // Ease the blend out over 5 ticks.locomotion.fadeWeight(1, 5); // And back in.locomotion.setWeight(0.5); // Or set it instantly.
Things to Know
- Samples should animate the same bones. If one sample doesn't animate a bone, that bone uses whatever is underneath (the default pose or earlier controllers) for that sample's share.
- All samples keep playing. Samples with no share are still updated so they stay in step, which costs a little per sample. Their sound, particle and custom keyframes still fire, so put footstep sounds on one sample only, or on a separate controller.
- The controllers are client-side. They're named
name/animation(for examplelocomotion/walk). Don't send commands to them; the blend space drives them every frame. - Add each blend space once. Calling
addTotwice throws.
Under the Hood
Blend spaces update through AzAnimator#addUpdateHook, which runs code every frame just before the controllers update. You can use it yourself to drive controller weights from your own logic:
@Overridepublic void registerControllers(AzAnimationControllerContainer<ExampleEntity> container) {var swim = AzAnimationController.builder(this, "swim").build();container.add(swim);addUpdateHook((entity, partialTicks) -> swim.setWeight(entity.isInWater() ? 1 : 0));}
The weight math is available in AzBlendWeights if you want to blend something else the same way: linear for 1D, gradientBand for 2D, and layered to turn shares into weights for layered controllers.
Troubleshooting
-
Only one animation ever plays.
- Check the input is changing (log
input()), and that the sample positions cover the range it moves through.
- Check the input is changing (log
-
The blend snaps between animations.
- Add
smoothing, or smooth the input yourself.
- Add
-
Feet slide when blending walk and run.
- Keep
syncPhaseon and make every cycle start on the same foot.
- Keep
-
Footstep sounds play too often.
- Sound keyframes fire on every sample. Keep them on one sample, or on a separate controller.
-
An attack doesn't show over the blend.
- Add the attack controller after the blend space, and mask it to the bones it should take over.