Skip to content

Latest commit

 

History

History
507 lines (372 loc) · 15.5 KB

File metadata and controls

507 lines (372 loc) · 15.5 KB

Mathimations Documentation

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 ./example directory that may be more up-to-date than this documentation.

Table of Contents


Quick Start

This guide assumes you already have a custom entity with a model and renderer (e.g., ExampleEntity, ExampleEntityModel, and ExampleEntityRenderer).

Step 1: Add EntityAnimationWrapper to Your Renderer

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);
    }
}

Step 2: Implement IModelPartProvider on Your Model

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
}

Step 3: Create Your Animation Class

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);
    }
}

Step 4: Trigger the Animation

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.


How It Works

Understanding the system's architecture will help you use it effectively:

Entity Rendering in Minecraft

In Minecraft, each entity type (like zombies or cows) shares a single model and renderer across all instances. When an entity is rendered:

  1. The renderer calls the model's setupAnim() method
  2. The model updates its parts' transformations
  3. The renderer draws the model

This means we can only modify transformations (position, rotation, scale) of model parts, not the actual geometry.

Animation System Architecture

The Mathimations system works by:

  1. EntityAnimationWrapper - Manages all entities of a type and their animators
  2. EntityAnimator - Handles animations for a single entity across multiple layers
  3. AnimationState - Tracks individual animations and their blend weights
  4. IMathAnimation - Your animation logic that modifies model parts

Animation Layers

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.


Common Classes Overview

AnimationContext

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)

ModelPartState

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 scale

EntityAnimationWrapper

The 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);

Utility Classes

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
);

Basic Examples

Simple Rotation Animation

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
    }
}

Breathing Animation

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);
    }
}

Head Look Animation

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);
    }
}

Using the Prefab Leg Animation

// 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);

Advanced Usage

Multiple Animation Layers

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 2

Conditional Animations

Play animations only when certain conditions are met:

wrapper.playAnimationIf(
    entity, 
    0, 
    (short) 2, 
    () -> entity.getHealth() < entity.getMaxHealth() * 0.3f  // Low health animation
);

Custom Transition Times

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
}

Accessing the Animator Directly

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);
}

Creating Complex Animations

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);
    }
}

Limitations

Be aware of these constraints when using Mathimations:

  1. 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.

  2. Performance - Each animated entity runs calculations every frame. Performance testing is recommended for large numbers of entities.

  3. Model Overwrites - Changes made in your EntityModel.setupAnim() method will overwrite animation transformations.

  4. 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.

  5. No Geometry Changes - You can only modify transformations (position, rotation, scale), not add/remove/reshape model parts.

  6. Animation Mixing - While layers allow combining animations, they blend linearly. Complex blending behavior requires custom logic.


Additional Resources

  • Example Implementation: Check the ./example directory for a complete working example
  • API Documentation: See minidoc.txt for detailed API information
  • Source Code: The library source is available for reference