AzCommands 101

AzCommand is a highly flexible class for controlling animations in AzureLib by dispatching a sequence of animation-related actions. It simplifies animation management by allowing you to manipulate the animation system at three hierarchical levels: root, controller, and animation. This guide will walk you through the basics of using AzCommand and how to replace the traditional AnimationController approach with it.

The Three Configuration Layers:

  • Root: Applies to every controller at once.
  • Controller: Applies to one named controller. Playback properties (speed, transition length, offsets, repeat count, direction) live here and stay set until something changes them.
  • Animation: Applies to one animation (a stage of a sequence): its play behavior and, optionally, its direction. These only last while that animation plays.

Getting Started with AzCommand

1. Creating an AzCommand

Creating an AzCommand involves specifying the controller name, the animation name, and optionally the play behavior (e.g., loop, play once). Here's how:

Example:

AzCommand walkCommand = AzCommand.create("base", "WALK_ANIMATION");

In this example:

  • "base" is the controller name.
  • "WALK_ANIMATION" is the animation to play.
  • No play behavior is given, so the animation plays the way its animation file says: the Loop Mode set in Blockbench (see Setting Play Behaviors in Blockbench). Animations without a loop mode play once.

To choose the behavior in code instead, pass it as the third argument. A behavior set in code always wins over the animation file:

Example:

AzCommand idleCommand = AzCommand.create("base", "IDLE_ANIMATION", AzPlayBehaviors.LOOP);

Play Behaviors

BehaviorWhat it doesFinishes?
AzPlayBehaviors.PLAY_ONCEPlays once, then moves on to the next stage (or stops).Yes
AzPlayBehaviors.LOOPLoops forever.No
AzPlayBehaviors.HOLD_ON_LAST_FRAMEPlays once and holds the last frame.No
AzPlayBehaviors.PING_PONGPlays forward, then backward, forever, with seamless turnarounds.No
AzPlayBehaviors.REPEAT_X_TIMESPlays the number of times set by the repeat count, then moves on.Yes
AzPlayBehaviors.FREEZE_ON_FRAMEPlays until the freeze point, then holds that frame.No
AzPlayBehaviors.AS_AUTHOREDUses the Loop Mode from the animation file. This is what create(controller, animation) uses.Depends on the file
  • Repeat count is the total number of plays: 3 plays the animation three times. 1 or less plays it once.
  • Freeze point is set in ticks when set from code (freezeTickOffset), and in seconds in Blockbench.
  • If both code and the animation file set a repeat count or freeze point, the code's value wins.

Setting Play Behaviors in Blockbench

With the AzureLib Animator plugin (2.2.0+), open an animation's Properties (double-click it in the Animations panel, or right-click → Properties) and pick its Loop Mode: Once, Hold, Loop, Ping-Pong, Repeat or Freeze. Repeat adds a Repeat Count field, and Freeze adds a Freeze At time with a Use Playhead Time button. Blockbench previews all of them on the timeline.

These are saved into the animation file, so in most cases you can play animations with AzCommand.create(controller, animation) and never set the behavior in code:

"attack": { "loop": "repeat_x_times", "repeat_times": 3 },
"hover": { "loop": "ping_pong" },
"pose": { "loop": "freeze_on_frame", "freeze_at": 1.25 }

Setting Playback Properties

To play an animation and set every playback property in one call, use the full create overload:

Example:

AzCommand slamCommand = AzCommand.create(
"base", // controller
"SLAM_ANIMATION", // animation
AzPlayBehaviors.PLAY_ONCE, // play behavior
5F, // start tick offset: skip the first 5 ticks
1.5F, // animation speed: 50% faster
4F, // transition length: blend in over 4 ticks
0F, // freeze tick offset (FREEZE_ON_FRAME only)
1F, // repeat count (REPEAT_X_TIMES only)
false // play in reverse
);

AzCommand.createRoot(...) takes the same arguments without the controller name and applies them to every controller.

To change only some properties, or change them without playing anything, use AzCommand.controllerBuilder() (one controller) or AzCommand.rootBuilder() (all controllers):

Example:

AzCommand sprintCommand = AzCommand.controllerBuilder()
.setSpeed("base", 2F)
.play("base", "RUN_ANIMATION")
.build();
Controller builder methodRoot builder methodSets
setSpeed(controller, speed)setSpeed(speed)Playback speed multiplier (1 = normal).
setTransitionSpeed(controller, ticks)setTransitionSpeed(ticks)Transition length in ticks when switching animations.
setStartTickOffset(controller, ticks)setStartTickOffset(ticks)Ticks skipped at the start of each animation.
setFreezeTickOffset(controller, ticks)setFreezeTickOffset(ticks)Freeze point for FREEZE_ON_FRAME.
setRepeatAmount(controller, count)setRepeatAmount(count)Total plays for REPEAT_X_TIMES.
setReverseAnimation(controller, reverse)setReverseAnimation(reverse)Play direction (see Playing Animations in Reverse).
setEasingType(controller, easing)setEasingType(easing)Easing used for transitions.
setWeight / fadeWeight—Layer weight (see Changing a Controller's Weight).
play(controller, animation) / playSequence(...)playSequence(...)Plays an animation or sequence.
cancel(controller)cancelAll()Stops what's playing.

Playing Animations in Reverse

Any animation can play backwards, from its last frame to its first. Keyframes, Molang query.anim_time and keyframe events (sounds, particles, custom instructions) all run in reverse, and transitions blend into the animation's last frame instead of its first.

Reverse a single animation as part of a sequence:

AzSequence closeDoor = AzSequence.create().playReversed("DOOR_OPEN_ANIMATION");

Or reverse a whole controller:

AzCommand.controllerBuilder()
.setReverseAnimation("door", true)
.build()
.sendForBlockEntity(myDoor);
  • If the controller is in the middle of an animation, it turns around from its current pose instead of jumping. A half-open door closes from where it is.
  • A direction set on the animation (playReversed, or withShouldReverse(true) in a stage) wins over the controller's direction.
  • For an animation that plays forward and then back forever, use AzPlayBehaviors.PING_PONG.

2. Playing Sequences

An AzSequence plays several animations one after another on one controller:

Example:

static final AzSequence SLAM = AzSequence.builder()
.play("WINDUP_ANIMATION") // plays once, then continues
.play("STRIKE_ANIMATION")
.loop("IDLE_ANIMATION") // loops forever
.build();
SLAM.toCommand("base").sendForEntity(myEntity);
MethodStage
play(name)Plays once and continues.
loop(name)Loops forever.
hold(name)Holds the last frame.
pingPong(name)Plays forward and backward forever.
playReversed(name)Plays once backwards and continues.
authored(name)Uses the animation file's Loop Mode.
then(name, behavior)Any behavior.
then(name, behavior, props -> ...)Any behavior with stage properties, e.g. props -> props.withShouldReverse(true).
  • Stages that never finish (loop, hold, ping-pong, freeze) must be last. Adding a stage after one throws an IllegalStateException. authored(...) can't be checked this way, so only add stages after it if the file's Loop Mode finishes.
  • Use toCommand(controller) for one controller, or toRootCommand() for all of them.
  • Sending a sequence that is already playing does nothing, so it's safe to send it every tick.

Sequence Events

event(name, tick) schedules a named event a number of game ticks after the sequence starts. Events run on the side that ticks an AzSequencePlayer, usually the server, which makes them suitable for gameplay such as dealing damage on the right frame:

Example:

static final AzSequence SLAM = AzSequence.builder()
.play("WINDUP_ANIMATION")
.event("damage", 12)
.play("STRIKE_ANIMATION")
.build();
private final AzSequencePlayer attackPlayer = AzSequencePlayer.forEntity(this, "base", (sequence, event) -> {
if (event.is("damage")) {
dealSlamDamage();
}
});
public void startSlam() { // server side, e.g. from Goal#start
attackPlayer.play(SLAM);
}
@Override
public void tick() {
super.tick();
if (!level().isClientSide()) {
attackPlayer.tick();
}
}
  • Event ticks count from the start of the whole sequence, not from the stage they follow.
  • They aren't scaled by animation speed, and each stage starts after the controller's transition length. Account for both when lining an event up with a keyframe.
  • If another animation takes over the controller (hurt, death), call attackPlayer.cancel() so pending events don't fire.

3. Composing Multiple Commands

If you need to merge several commands into one, you can use the compose method. This is helpful for combining animations across multiple controllers:

AzCommand combinedCommand = AzCommand.compose(walkCommand, idleCommand);

All actions from the two commands are merged into a single unified command.

4. Changing a Controller's Weight

When several controllers are layered on the same bones (see the Layering Controllers section of the Animation Controller Builder page), you can change how strongly a controller is applied from the server. Build the command with AzCommand.controllerBuilder():

// Fade the "aim" layer in over 5 ticks.
AzCommand aimIn = AzCommand.controllerBuilder()
.fadeWeight("aim", 1F, 5F)
.build();
// Set it back to 0 instantly.
AzCommand aimOff = AzCommand.controllerBuilder()
.setWeight("aim", 0F)
.build();
aimIn.sendForEntity(myEntity);
  • setWeight(controllerName, weight) changes the weight instantly; fadeWeight(controllerName, weight, ticks) moves it there over the given number of ticks.
  • Weights range from 0 (no effect) to 1 (full).
  • A weight stays in place until it is changed again; playing a new animation on the controller doesn't reset it.
  • The same builder can also play animations, so a layer can be faded in and started in one command:
AzCommand startAiming = AzCommand.controllerBuilder()
.fadeWeight("aim", 1F, 5F)
.play("aim", "AIM_ANIMATION")
.build();

5. Random Animation Pools

A pool plays a random animation from a weighted list, and picks the next one each time the current animation finishes. This is the way to get idle or walk variety (idle, then idle2, then idle again) without timers.

Because the next pick happens when the animation actually ends, nothing about animation length is hardcoded. If a resource pack retimes or replaces an animation, the pool follows it automatically.

Creating a Pool

Build a pool with AzWeightedPoolBehavior.builder, giving it a unique name and one entry per animation:

Example:

public static final AzWeightedPoolBehavior IDLE_POOL = AzWeightedPoolBehavior.builder("mymod:example_idle")
.add("IDLE_ANIMATION", 6)
.add("IDLE_STRETCH_ANIMATION", 3)
.addNoRepeat("IDLE_SNIFF_ANIMATION", 1)
.build();

In this example:

  • "mymod:example_idle": The pool's name. It must be unique across all mods, so prefix it with your mod id.
  • 6, 3, 1: Relative weights. These three give 60%, 30% and 10%. Weights don't need to add up to anything in particular.
  • addNoRepeat: This animation is never picked twice in a row. See Preventing Repeats.

Playing a Pool

Turn the pool into a command with the controller it should play on, then send it like any other command:

Example:

private static final AzCommand IDLE_COMMAND = IDLE_POOL.sequence().toCommand("base");
IDLE_COMMAND.sendForEntity(myEntity);
  • sequence() always starts the pool on its first entry, or on the entry chosen with startWith(...).
  • It's safe to send the same pool command every tick. Sending the pool that's already playing does nothing, so it won't restart the chain.
Starting on a Random Entry

sequence() always starts on the same animation, which means that animation shows up more often than its weight suggests when the pool only runs briefly. Use randomSequence() to start on a weighted-random entry instead:

Example:

private AzCommand currentIdle;
// When the entity enters its idle state:
currentIdle = IDLE_POOL.randomSequence().toCommand("base");
// Every tick while idle:
currentIdle.sendForEntity(this);
Using a Pool After Other Animations

A pool can be the last stage of a sequence. The earlier stages play once, then the pool takes over:

Example:

AzSequence.create()
.play("WAKE_UP_ANIMATION")
.then("IDLE_ANIMATION", IDLE_POOL)
.toCommand("base")
.sendForEntity(myEntity);
  • The pool should be the last stage. If other stages come after it, the pool plays its first animation and then moves on to them, without picking.

Preventing Repeats

Base loops are usually designed to repeat seamlessly, but detail animations such as a sniff or a head turn look obvious when they play twice in a row. You can block repeats per animation:

Example:

AzWeightedPoolBehavior.builder("mymod:example_walk")
.add("WALK_ANIMATION", 1) // may repeat
.addNoRepeat("WALK_SNIFF_ANIMATION", 1) // never twice in a row
.add("WALK_LOOK_ANIMATION", 1, false) // same as addNoRepeat
.build();
  • avoidImmediateRepeat() on the builder marks every entry as no-repeat at once.
  • Blocking repeats slightly lowers how often that animation appears overall, because it's sometimes excluded. For example, with IDLE at weight 3 and a no-repeat SNIFF at weight 1, the sniff appears 20% of the time instead of 25%. Raise its weight a little (1.5 here) if you need the original share.

Things to Know

  • Picks happen on each client. Two players watching the same entity may see different variants. That's usually fine for idles and walks. If every player must see the same animation, choose it on the server and send a normal command instead.
  • Missing animations are skipped. If a resource pack removes an entry, the pool tries the other entries. If none of them can be found, the current animation loops. A warning is logged for each missing animation.
  • Blending uses the controller's transition length, exactly like switching between any two animations. See the Animation Controller Builder page.
  • Switching between pools restarts them. If you switch between an idle pool and a walk pool based on movement, make sure the movement check is stable. A check that flickers for a single tick (for example, between path nodes) will keep restarting the pools and you'll only ever see the first animation. Requiring the new state to hold for a few ticks before switching fixes this.

Full Example

public class ExampleEntity extends PathfinderMob {
public static final AzWeightedPoolBehavior IDLE_POOL = AzWeightedPoolBehavior.builder("mymod:example_idle")
.add("IDLE_ANIMATION", 3)
.addNoRepeat("IDLE_SNIFF_ANIMATION", 1)
.build();
public static final AzWeightedPoolBehavior WALK_POOL = AzWeightedPoolBehavior.builder("mymod:example_walk")
.add("WALK_ANIMATION", 1)
.addNoRepeat("WALK_SNIFF_ANIMATION", 1)
.addNoRepeat("WALK_LOOK_ANIMATION", 1)
.build();
private static final int STATE_SWITCH_TICKS = 5;
private boolean walking;
private int pendingStateTicks;
private AzCommand currentCommand;
public ExampleEntity(EntityType<? extends PathfinderMob> entityType, Level level) {
super(entityType, level);
}
@Override
public void tick() {
super.tick();
// Horizontal movement only, and the new state must hold for a few ticks before it counts.
var dx = getX() - xo;
var dz = getZ() - zo;
var movingNow = dx * dx + dz * dz > 1.0E-4;
if (movingNow == walking) {
pendingStateTicks = 0;
} else if (++pendingStateTicks >= STATE_SWITCH_TICKS) {
walking = movingNow;
pendingStateTicks = 0;
currentCommand = null; // pick a new start for the new state
}
if (!level().isClientSide()) {
return; // Only dispatch on the client, so the server doesn't send a packet every tick
}
if (currentCommand == null) {
var pool = walking ? WALK_POOL : IDLE_POOL;
currentCommand = pool.randomSequence().toCommand("base");
}
currentCommand.sendForEntity(this);
}
}

Sending AzCommands

Once you've created an AzCommand, you can dispatch it to an entity, block entity, or item stack using the provided methods.

Send to an Entity

To trigger an animation on an entity, use the sendForEntity method:

walkCommand.sendForEntity(myEntity);
  • The command will determine the proper dispatch based on the client or server side.

Send to a Block Entity

To send a command to a BlockEntity:

idleCommand.sendForBlockEntity(myBlockEntity);

This sends the command to all clients tracking the relevant chunk.

Send to an Item/Armor

To animate an item, use the sendForItem method:

combinedCommand.sendForItem(myEntity, myItemStack);

This requires the item stack to have a registered UUID, which is covered in the Item and Armor guides.

Best Practices

  1. Reuse Commands:
    • Define reusable commands for common animations to simplify your rendering logic.
  2. Use pools instead of timers for variety:
    • To randomly vary idle or walk animations, use a Random Animation Pool rather than counting ticks. Pools follow the real animation length, so they keep working when a resource pack changes an animation.
  3. Set play behaviors in Blockbench:
    • Choosing the Loop Mode in Blockbench keeps it next to the animation and lets you preview it. Then AzCommand.create(controller, animation) is all your code needs. Pass a behavior in code only when it must differ from the file.
  4. Remember that controller properties stick:
    • Speed, transition length, offsets, repeat count and direction stay on the controller after a command sets them. If one animation needs different settings, set them back afterwards, or keep that animation on its own controller.

Converting from AnimationController to AzCommand

If you previously managed animations using AnimationController, you can streamline and simplify your code using AzCommand. This approach eliminates the need for directly managing AnimationController states by leveraging AzCommand to dispatch animations dynamically.


Old Approach (Using AnimationController)

@Override
public void registerControllers(AnimatableManager.ControllerRegistrar controllers) {
controllers.add(new AnimationController<>(this, "Walk", 5, state -> {
if (state.isMoving()) {
return state.setAndContinue(WALK_ANIMATION);
}
return state.setAndContinue(IDLE_ANIMATION);
}));
}

In this example:

  • The AnimationController dynamically switches between a walking animation and an idle animation based on whether the entity is moving.

New Approach (Using AzCommand and MoveAnalysis)

With AzCommand, you can dynamically dispatch animation commands based on the entity's state. Instead of continuously managing AnimationController state updates, you simply send the appropriate AzCommand when needed.

To enhance the new AzCommand approach and replace the isMoving logic from the old AnimationController example, you can use the MoveAnalysis utility class. This class provides advanced functionalities, such as detecting horizontal movement and checking whether the entity is on the ground. Here's how you can implement this:

public class ExampleEntity extends Monster {
private final AzCommand idleCommand = AzCommand.create("base", "IDLE_ANIMATION", AzPlayBehaviors.LOOP);
private final AzCommand walkCommand = AzCommand.create("base", "WALK_ANIMATION", AzPlayBehaviors.LOOP);
private final MoveAnalysis moveAnalysis;
public ExampleEntity(EntityType<? extends Monster> entityType, Level level) {
super(entityType, level);
this.moveAnalysis = new MoveAnalysis(this);
}
@Override
public void tick() {
super.tick(); // Update base entity behavior
moveAnalysis.update(); // Analyze the entity's movement state
if (this.level().isClientSide) { // Only execute animation logic on the client
boolean isMovingOnGround = moveAnalysis.isMovingHorizontally() && onGround();
if (isMovingOnGround) {
walkCommand.sendForEntity(this); // Send the walk animation if moving
} else {
idleCommand.sendForEntity(this); // Otherwise, send the idle animation
}
}
}
}
  • One controller: walk and idle replace each other, so they play on the same controller ("base"). Sending the command that's already playing does nothing, so sending every tick is fine.
  • Loop modes: if both animations are set to Loop in Blockbench, AzCommand.create("base", "WALK_ANIMATION") is enough.
  • MoveAnalysis: Provides a reliable method to check if the entity is moving horizontally.
  • Benefits:
    • Improves code readability and reusability by moving movement logic into MoveAnalysis.
    • Cleanly separates movement detection from animation dispatch.