Skip to content

Animatables and Controllers

An animatable is any object that can provide animation state to a PulseLib renderer. The renderer does not decide whether an entity is walking, whether a block is open, or whether an item should be idling. That decision belongs to controllers registered by the animatable.

Animation state is built around:

PAnimatable

An animatable has two responsibilities. It returns a manager for the current object or stack, and it registers controller factories. The factories matter because items and other singleton-style objects may need separate controller instances for separate keys.

public class ExampleEntity extends PathfinderMob implements PAnimatable<ExampleEntity> {
    private final PAnimationManager<ExampleEntity> manager = PLibHelper.createManager(this);

    @Override
    public PAnimationManager<ExampleEntity> getAnimationManager(AnimManagerKey key) {
        return this.manager;
    }

    @Override
    public void registerAnimationControllers(PAnimationManager.PAnimationRegistrar<ExampleEntity> registrar) {
        registrar.add("movement", () -> state -> {
            ExampleEntity entity = state.animatable();
            state.controller().play(entity.walkAnimation.isMoving() ? WALK : IDLE);
            return ControllerState.PLAY;
        });
    }
}

Manager choice

Entities and block entities are real world instances, so they can usually keep one manager field. Items are different: there is one Item object for many ItemStacks, so PulseLib uses keys to separate stack animation state.

PLibHelper.createManager chooses instance managers for entities and block entities, and singleton managers for other animatables.

For items, use SingletonAnimationManager.getManager with the key passed by the renderer:

@Override
public PAnimationManager<ExampleItem> getAnimationManager(AnimManagerKey key) {
    return SingletonAnimationManager.getManager(key, this);
}

AnimManagerKey can derive keys from ItemStack, Entity, BlockEntity, or an arbitrary object.

PRawAnimation

PRawAnimation is not the animation data itself. The real keyframes come from the model file. PRawAnimation is the playback recipe: play this model animation once, wait a bit, then loop another model animation.

Stage names must match animation names inside the loaded model.

private static final PRawAnimation SEQUENCE = PRawAnimation.begin()
        .thenPlay("deploy")
        .thenWait(10)
        .thenLoop("idle")
        .build();

Stage helpers:

  • thenPlay(name) uses PAnimationType.PLAY_ONCE.
  • thenLoop(name) uses PAnimationType.CYCLE.
  • thenHold(name) uses PAnimationType.HOLD_LAST_FRAME and pauses on the last frame.
  • thenWait(ticks) inserts a wait stage.
  • withSpeed(speed) changes the last stage speed.
  • withInterpolation(type) changes the last stage interpolation.

Example with speed and interpolation:

private static final PRawAnimation FAST_OPEN = PRawAnimation.begin()
        .thenPlay("open")
        .withSpeed(2.0f)
        .withInterpolation(PInterpolationType.BEZIER)
        .thenHold("open")
        .build();

Built-in interpolation types are defined in PInterpolationType: LINEAR, CATMULLROM, BEZIER, STEP.

Controller state handler

A controller owns one active PRawAnimation at a time. Think of the state handler as the current animation rule. It is called while ticking, checks the animatable's current state, and returns ControllerState.

registrar.add("attack", () -> state -> {
    if (!state.animatable().swinging) {
        state.controller().stop();
        return ControllerState.STOP;
    }

    state.controller().play(ATTACK);
    return ControllerState.PLAY;
});

Useful controller methods:

  • play(PRawAnimation animation)
  • pause()
  • resume()
  • stop()
  • getState()
  • isPlaying()
  • isPaused()
  • isStopped()
  • getTime()
  • getCurrentStage()

Multiple controllers are mixed on the same bones in renderer order. Register broad base pose controllers first and specific overrides later. A common pattern is one movement controller for idle/walk/run and another controller for attacks or short actions.