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 throughplayState(ModelState).IStateMachineHandler(id"state_machine") — adds priority-aware overloads such asplayAnimation(int priority, String animation, ...)andstopAnimation(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.