Custom Animation Handler

An AnimationHandler is the per-ActiveModel object that decides how animations are played, blended and stopped. Model Engine ships two implementations, but you can supply your own to completely replace how a model selects and drives its animations. See Play / Stop Animation for how handlers are used at runtime.

The two built-in styles

AnimationHandler is the base interface. Model Engine provides two specializations, and your custom handler will usually extend one of them rather than the bare base:

  • IPriorityHandler (id "priority") — the default. Plays animations by priority and drives movement states through playState(ModelState).
  • IStateMachineHandler (id "state_machine") — adds priority-aware overloads such as playAnimation(int priority, String animation, ...) and stopAnimation(int priority, String animation).

Each interface fixes its own getId() ("priority" / "state_machine"). A fully custom handler should return its own id from getId().

Creating a handler class

Implement AnimationHandler (or extend one of the interfaces above). The interface already provides default save / load and isWalking implementations, so you only need to supply the core methods.

public class CustomAnimationHandler implements AnimationHandler {

    private final ActiveModel activeModel;

    public CustomAnimationHandler(ActiveModel activeModel) {
        this.activeModel = activeModel;
    }

    @Override
    public ActiveModel getActiveModel() {
        return activeModel;
    }

    @Override
    public String getId() {
        // Used as the registry key when the model is saved/loaded
        return "custom";
    }

    @Override
    public void prepare() {
        // Called every tick before bones update; advance your animation state here
    }

    @Override
    public void updateBone(ModelBone bone) {
        // Apply the current animation transform onto the given bone
    }

    @Override
    public void tickGlobal() {
        // Called every tick for global keyframes (sounds, particles, etc.)
    }

    @Override
    public boolean hasFinishedAllAnimations() {
        // Return true when nothing is playing
    }

    @Override
    public @Nullable IAnimationProperty playAnimation(String animation, double lerpIn, double lerpOut, double speed, boolean force) {
        // Start an animation; return its IAnimationProperty, or null if it could not play
    }

    @Override
    public boolean playAnimation(IAnimationProperty property, boolean force) {
        // Start an already-built property
    }

    @Override
    public boolean isPlayingAnimation(String animation) { ... }

    @Override
    public void stopAnimation(String animation) {
        // Stop gracefully (play the lerp-out)
    }

    @Override
    public void forceStopAnimation(String animation) {
        // Stop immediately, skipping lerp-out
    }

    @Override
    public void forceStopAllAnimations() { ... }

    @Override
    public @Nullable IAnimationProperty getAnimation(String animation) { ... }

    @Override
    public Map<String, IAnimationProperty> getAnimations() {
        // Unordered, immutable map of all live properties
    }

    @Override
    public void setDefaultProperty(DefaultProperty defaultProperty) { ... }

    @Override
    public DefaultProperty getDefaultProperty(ModelState state) { ... }

}

DefaultProperty

AnimationHandler.DefaultProperty holds the per-ModelState defaults (animation id, lerpIn, lerpOut, speed, and a merge flag). The default save / load implementations on AnimationHandler persist these for every ModelState, so storing them via setDefaultProperty / getDefaultProperty is enough to get save/load for free. DefaultProperty.build turns one into an IAnimationProperty against a model's blueprint.

Using the handler

A handler is bound to an ActiveModel at creation time. createActiveModel accepts a Function<ActiveModel, AnimationHandler> supplier as its last argument:

ActiveModel model = ModelEngineAPI.createActiveModel(
        "my_model",
        null,                       // renderer supplier (null = default)
        CustomAnimationHandler::new // handler supplier
);

Passing null for the supplier falls back to the built-in handler. Once created, the handler is reachable through model.getAnimationHandler().

Registering the handler

Registration is what lets a saved model rebuild your handler on load. ActiveModel.save() writes the handler's getId(), and on load AnimationHandlerRegistry.createHandler(model, data) looks that id up and invokes the registered factory. Register a BiFunction<ActiveModel, SavedData, AnimationHandler> under the same id your getId() returns:

// On plugin start-up
ModelEngineAPI.getAnimationHandlerRegistry().register("custom", (model, data) -> {
    CustomAnimationHandler handler = new CustomAnimationHandler(model);
    handler.load(data);
    return handler;
});

If you only ever attach the handler manually via a supplier and never rely on saved-model reloading, registration is optional — but registering keeps the handler working across save/load like the built-in ones.

Updated Aug 19, 2026