A library for creating mathematical animations for Minecraft entities using transformations (position, rotation, scale) rather than keyframes.
Note: There's a complete working example in the
./exampledirectory that may be more up-to-date than this documentation.
- Quick Start
- How It Works
- Common Classes Overview
- Basic Examples
- Advanced Usage
- Animation Synchronization
- Limitations
This guide assumes you already have a custom entity with a model and renderer (e.g., ExampleEntity, ExampleEntityModel, and ExampleEntityRenderer).
public class ExampleEntityRenderer extends LivingEntityRenderer<ExampleEntity, ExampleEntityModel<ExampleEntity>> {
private final EntityAnimationWrapper<ExampleEntity, ExampleEntityModel<ExampleEntity>> animationWrapper;
public ExampleEntityRenderer(EntityRendererFactory.Context context) {
super(context, new ExampleEntityModel<>(context.bakeLayer(ModModelLayers.EXAMPLE_ENTITY)), 0.5f);
// Register global animations that will be available to all entities of this type (on layer 0)
this.animationWrapper = new EntityAnimationWrapper<>(new HashMap<>() {{
put((short) 0, new IdleAnimation());
put((short) 1, new WalkAnimation());
}}, this.getModel());
}
@Override
public void render(ExampleEntity entity, float entityYaw, float partialTicks,
PoseStack poseStack, MultiBufferSource buffer, int packedLight) {
// Initialize/update animations BEFORE rendering
this.animationWrapper.initOrUpdate(entity, partialTicks);
super.render(entity, entityYaw, partialTicks, poseStack, buffer, packedLight);
}
}This tells the animation system which parts of your model can be animated.
public class ExampleEntityModel<T extends ExampleEntity> extends EntityModel<T>
implements IModelPartProvider<T> {
private final ModelPart root;
// ... other model parts
public ExampleEntityModel(ModelPart root) {
this.root = root;
// ... initialize other parts
}
@Override
public ModelInfo getRootAndChildren() {
// Automatically discovers all model parts from the root
return ModelInfo.getInfo(this.root)
.orElse(new ModelInfo(this.root, Collections.emptyList()));
}
// ... other model methods
}public class IdleAnimation implements IMathAnimation<ExampleEntity> {
@Override
public float getTransitionTime() {
return 0.4f; // Time in seconds to blend in/out of this animation
}
@Override
public void update(ExampleEntity entity, AnimationContext ctx) {
// Get the model parts you want to animate
ModelPartState body = ctx.getPart("body");
ModelPartState head = ctx.getPart("body/head");
if (body == null || head == null) return; // Safety check
// Create a gentle bobbing motion
float time = (float) ctx.getTimeSinceEntityStart();
float bob = (float) Math.sin(time * 2) * 0.05f;
body.translateY(bob);
head.setPitch((float) Math.sin(time) * 0.1f);
}
}Implement IAutoAnimation on your entity to automatically control which animation plays:
public class ExampleEntity extends LivingEntity implements IAutoAnimation {
private static final TrackedData<Integer> ANIMATION_STATE =
DataTracker.registerData(ExampleEntity.class, TrackedDataHandlerRegistry.INTEGER);
@Override
protected void initDataTracker(DataTracker.@NotNull Builder builder) {
super.initDataTracker(builder);
this.dataTracker.startTracking(ANIMATION_STATE, 0); // Start with idle animation
}
@Override
public short getCurrentAnimation(byte layerIndex) {
// Return which animation should play on layer 0
if (layerIndex == 0) {
return this.dataTracker.get(ANIMATION_STATE).shortValue();
}
return -1; // -1 means no animation
}
// Example: Change animation based on movement
@Override
public void tick() {
super.tick();
if (this.isMoving()) {
this.dataTracker.set(ANIMATION_STATE, 1); // Walk animation
} else {
this.dataTracker.set(ANIMATION_STATE, 0); // Idle animation
}
}
}That's it! Your entity should now animate smoothly between idle and walking states.
Understanding the system's architecture will help you use it effectively:
In Minecraft, each entity type (like zombies or cows) shares a single model and renderer across all instances. When an entity is rendered:
- The renderer calls the model's
setupAnim()method - The model updates its parts' transformations
- The renderer draws the model
This means we can only modify transformations (position, rotation, scale) of model parts, not the actual geometry.
The Mathimations system works by:
- EntityAnimationWrapper - Manages all entities of a type and their animators
- EntityAnimator - Handles animations for a single entity across multiple layers
- AnimationState - Tracks individual animations and their blend weights
- IMathAnimation - Your animation logic that modifies model parts
Animations are organized into layers (default: 8 layers). Each layer can have one active animation at a time, and layers are blended together:
- Layer 0: Usually base animations (idle, walk)
- Layer 1+: Additive animations (attacks, gestures)
When you play a new animation on a layer, it smoothly transitions out the old animation. The animator will iterate over each layer from the last to the first (example: 8 -> 1) blending the animations.
Provides access to model parts and timing information within your animation:
ctx.getPart("body") // Get a single part (returns null if not found)
ctx.getParts("body", "head") // Get multiple parts as an array
ctx.getAnimationTime() // Time since this animation started (seconds)
ctx.getTimeSinceEntityStart() // Time since entity rendered (+ age in ticks) (seconds)
ctx.getDelta() // Time since last frame (seconds)
ctx.getPartialTicks() // Sub-tick interpolation (0.0 to 1.0)Represents the transformation state of a model part:
// Rotation (in radians by default)
part.setPitch(angle) // Rotation around X axis
part.setYaw(angle) // Rotation around Y axis
part.setRoll(angle) // Rotation around Z axis
part.setRotation(x, y, z) // Set all rotations at once
// Rotation (in degrees)
part.setPitchDeg(45f)
part.setYawDeg(90f)
part.setRollDeg(180f)
part.setRotationDeg(45f, 90f, 0f)
// Incremental rotation
part.rotateX(0.1f)
part.rotateY(0.1f)
part.rotateZ(0.1f)
// Position (pivot point)
part.setPivot(x, y, z)
part.translate(dx, dy, dz) // Relative movement
// Scale
part.setScale(1.5f) // Uniform scale
part.setScale(1.0f, 2.0f, 1.0f) // Non-uniform scaleThe main interface for controlling animations:
// Register animations (usually in renderer constructor)
wrapper.registerGlobalAnimation(layerIndex, animationId, animation);
wrapper.registerAnimation(entity, layerIndex, animationId, animation);
// Play animations
wrapper.playAnimation(entity, layerIndex, animationId);
wrapper.playAnimationIf(entity, layerIndex, animationId, condition);
// Stop animations
wrapper.stopAnimation(entity, layerIndex, animationId, transitionTime);
wrapper.stopLayer(entity, layerIndex, transitionTime);TriGeoUtils - Helper functions for trigonometric animations:
// Map sine wave to a range over a period
float value = TriGeoUtils.sinValue(currentTime, min, max, period);
float value = TriGeoUtils.cosValue(currentTime, min, max, period);TimingUtils - Create pseudo-random animation windows:
// Returns 0-1 when inside a window, 0 otherwise
float progress = TimingUtils.animationWindow(
currentTime,
animationDuration, // How long the animation lasts
minInterval, // Minimum time between animations
maxInterval // Maximum time between animations
);public class SpinAnimation implements IMathAnimation<ExampleEntity> {
@Override
public float getTransitionTime() {
return 0.3f;
}
@Override
public void update(ExampleEntity entity, AnimationContext ctx) {
ModelPartState body = ctx.getPart("body");
if (body == null) return;
float time = (float) ctx.getAnimationTime();
body.setYaw(time * 2); // Spin around Y axis
}
}public class BreathingAnimation implements IMathAnimation<ExampleEntity> {
@Override
public float getTransitionTime() {
return 1.0f;
}
@Override
public void update(ExampleEntity entity, AnimationContext ctx) {
ModelPartState body = ctx.getPart("body");
if (body == null) return;
float time = (float) ctx.getTimeSinceEntityStart();
// Breathing cycle: 3 seconds per breath
float breathe = TriGeoUtils.sinValue(time, 0.95f, 1.05f, 3.0f);
body.setScale(1.0f, breathe, 1.0f);
}
}public class LookAroundAnimation implements IMathAnimation<ExampleEntity> {
@Override
public float getTransitionTime() {
return 0.5f;
}
@Override
public void update(ExampleEntity entity, AnimationContext ctx) {
ModelPartState head = ctx.getPart("head");
if (head == null) return;
float time = (float) ctx.getTimeSinceEntityStart();
// Look left and right slowly
float yaw = TriGeoUtils.sinValue(time, -0.5f, 0.5f, 4.0f);
// Slight up and down motion
float pitch = TriGeoUtils.cosValue(time, -0.2f, 0.2f, 3.0f);
head.setYaw(yaw);
head.setPitch(pitch);
}
}// In your renderer constructor
List<List<String>> legGroups = new ArrayList<>();
legGroups.add(List.of("front_left_leg", "back_left_leg")); // Left legs move together
legGroups.add(List.of("front_right_leg", "back_right_leg")); // Right legs move together
LegAnimation<ExampleEntity> walkAnim = new LegAnimation<>(
10.0f, // Leg length in pixels
45.0f, // Maximum leg angle in degrees
LegAnimation.LegAxis.X, // Which axis the legs rotate on
legGroups
);
this.animationWrapper.registerGlobalAnimation(0, (short) 1, walkAnim);Use layers to combine different types of animations:
// In renderer constructor
this.animationWrapper = new EntityAnimationWrapper<>(
(byte) 3, // Use 3 layers
this.getModel()
);
// Layer 0: Base movement animations
wrapper.registerGlobalAnimation(0, (short) 0, new IdleAnimation());
wrapper.registerGlobalAnimation(0, (short) 1, new WalkAnimation());
// Layer 1: Upper body actions
wrapper.registerGlobalAnimation(1, (short) 0, new AttackAnimation());
wrapper.registerGlobalAnimation(1, (short) 1, new WaveAnimation());
// Layer 2: Facial expressions
wrapper.registerGlobalAnimation(2, (short) 0, new BlinkAnimation());Then control each layer independently:
// Entity can walk while attacking and blinking
wrapper.playAnimation(entity, 0, (short) 1); // Walk on layer 0
wrapper.playAnimation(entity, 1, (short) 0); // Attack on layer 1
wrapper.playAnimation(entity, 2, (short) 0); // Blink on layer 2Play animations only when certain conditions are met:
wrapper.playAnimationIf(
entity,
0,
(short) 2,
() -> entity.getHealth() < entity.getMaxHealth() * 0.3f // Low health animation
);Control how quickly animations blend:
// Stop animation with custom transition
wrapper.stopAnimation(entity, 0, (short) 1, 2.0f); // 2 second fade out
// Or in your animation class
@Override
public float getTransitionTime() {
return 1.5f; // 1.5 second blend in/out
}For advanced control, get the animator for an entity:
EntityAnimator<ExampleEntity, ExampleEntityModel<ExampleEntity>> animator =
wrapper.getAnimator(entity);
if (animator != null) {
animator.playAnimation((byte) 0, (short) 1);
animator.stopLayer((byte) 1, 0.5f);
}Combine multiple transformations for rich animations, kind of like keyframes:
public class ComplexAttackAnimation implements IMathAnimation<ExampleEntity> {
@Override
public float getTransitionTime() {
return 0.2f;
}
@Override
public void update(ExampleEntity entity, AnimationContext ctx) {
float time = (float) ctx.getAnimationTime();
ModelPartState body = ctx.getPart("body");
ModelPartState rightArm = ctx.getPart("body/right_arm");
ModelPartState leftArm = ctx.getPart("body/left_arm");
if (body == null || rightArm == null || leftArm == null) return;
if (time < 0.3f) {
float t = time / 0.3f;
body.setYaw(-0.5f * t);
rightArm.setPitch(-1.5f * t);
}
else if (time < 0.5f) {
float t = (time - 0.3f) / 0.2f;
body.setYaw(-0.5f + 1.0f * t);
rightArm.setPitch(-1.5f + 3.0f * t);
}
else {
float t = Math.min((time - 0.5f) / 0.3f, 1.0f);
body.setYaw(0.5f * (1 - t));
rightArm.setPitch(1.5f * (1 - t));
}
leftArm.setPitch(-rightArm.getPitch() * 0.3f);
}
}Be aware of these constraints when using Mathimations:
-
No Keyframe Support - Although you can TECHNICALLY create keyframe-like animations using this system, it is not recommended. This system uses primarily mathematical functions. For complex pre-animated sequences, consider other animation systems.
-
Performance - Each animated entity runs calculations every frame. Performance testing is recommended for large numbers of entities.
-
Model Overwrites - Changes made in your
EntityModel.setupAnim()method will overwrite animation transformations. -
Shared Model Instance - All entities of the same type share one model instance. The system compensates for this, but it means you can't have persistent model state.
-
No Geometry Changes - You can only modify transformations (position, rotation, scale), not add/remove/reshape model parts.
-
Animation Mixing - While layers allow combining animations, they blend linearly. Complex blending behavior requires custom logic.
- Example Implementation: Check the
./exampledirectory for a complete working example - API Documentation: See
minidoc.txtfor detailed API information - Source Code: The library source is available for reference