Animation System
Animate Minecraft blocks, items and entities with 3D Blockbench models and .erianim.json files.
Opening lootbox, spinning machine, entity with idle animation — the same pipeline for everything.
Introduction
The 3D Block Animation module allows you to create Minecraft blocks that are rendered with 3D Blockbench models and animated using .erianim.json files.
Unlike standard Minecraft blocks which are simple cubes with textures on each face, an animated block uses a TileEntity (an entity stored in the world that holds data) and a TESR (TileEntity Special Renderer — a rendering system that draws the 3D model instead of the cube).
Key concepts for beginners
- TileEntity — A Java object attached to a block in the world. It stores data (which model to display, which animation to play, etc.) and survives server restarts.
- TESR (TileEntity Special Renderer) — The code that draws the 3D model every frame. It reads data from the TileEntity and applies animation transformations.
- Blockbench — A free software (blockbench.net) for creating 3D models in the Minecraft format. You design your model with cubes, texture it, then export it as JSON.
- .erianim.json file — An EriAPI JSON file that describes animations: which groups of cubes move, how they move, at what speed, with which effects.
The creation pipeline
Here are the steps of the process, from creation to in-game animation:
- Modeling — Create the 3D model in Blockbench. Organize cubes into groups (each group can be animated independently).
- Export — Export the model in "Java Block/Item" format (.json).
- Animation — Create the
.erianim.jsonfile that describes the animations (manually or with the EriAnim editor). - Integration — Register the block in Java code with
EriAnimBlock.create(). - Runtime — The TESR reads the model and the animation, and draws the animated block on screen.
When you use EriAnimBlock.create(), EriAPI automatically registers the TileEntity,
the TESR, the blockstate, and the item rendering in hand. You don't need to touch any OpenGL code or create additional classes.
Quick Start
Here is how to create a complete animated block in 5 steps. At the end, you will have a block that plays an animation when right-clicked.
Step 1 — Create the model in Blockbench
Open Blockbench and create a new project of type "Java Block/Item". Design your model by organizing cubes into named groups.
For example, for a lootbox: a base group for the pedestal, a lid group for the cover,
a lock group for the padlock. Each group can be animated separately.
Export via File > Export > Export Java Block/Item Model. The generated file is a .json
that you will place in your mod.
Step 2 — Create the .erianim.json file
Create a JSON file that describes the animations of your block. Here is a minimal example with an "open" animation that rotates the lootbox lid:
{
"format_version": 1,
"model": "mymod:block/my_lootbox",
"animations": {
"open": {
"duration": 15,
"endBehavior": "freeze",
"tracks": {
"lid": {
"rotation": [
{ "tick": 0, "value": [0, 0, 0], "easing": "linear" },
{ "tick": 15, "value": [-110, 0, 0], "easing": "ease_out_back" }
]
},
"lock": {
"translation": [
{ "tick": 0, "value": [0, 0, 0], "easing": "linear" },
{ "tick": 8, "value": [0, -3, 0], "easing": "ease_out" }
],
"visible": [
{ "tick": 8, "value": false }
]
}
},
"events": [
{ "tick": 0, "type": "sound", "value": "minecraft:block.chest.open", "volume": 1.0, "pitch": 1.2 },
{ "tick": 15, "type": "callback", "value": "open_complete" }
]
}
}
}
Step 3 — Place the files in the correct folders
src/main/resources/assets/mymod/
models/block/my_lootbox.json ← Blockbench model (export)
animations/block/my_lootbox.erianim.json ← Animation file
textures/blocks/my_lootbox/ ← Model PNG textures
base.png
lid.png
lock.png
blockstates/my_lootbox.json ← Blockstate (auto-generated)
Step 4 — Register the block in Java
import fr.eri.eriapi.anim.AnimatedBlockTileEntity;
import fr.eri.eriapi.anim.EriAnimBlock;
import net.minecraft.block.material.Material;
public class MyBlocks {
public static void init() {
EriAnimBlock.create("mymod", "my_lootbox")
.material(Material.IRON)
.hardness(5.0f)
.resistance(10.0f)
.model("mymod:block/my_lootbox")
.animation("mymod:block/my_lootbox")
.renderDistance(64)
.onRightClick(ctx -> {
// Get the block's TileEntity
AnimatedBlockTileEntity te = (AnimatedBlockTileEntity)
ctx.world.getTileEntity(ctx.pos);
if (te != null) {
te.playAnimation("open");
}
})
.register();
}
}
Step 5 — Test in-game
Place the block in the world and right-click it. The lid opens with a chest sound, the padlock falls and disappears,
and at the end of the animation the "open_complete" callback is triggered.
Don't forget to call ContentRegistry.register(this) in your @Mod class before declaring your blocks.
This is required for EriAPI to properly register blocks, TileEntities, and TESRs with Forge.
The TESR (AnimatedBlockTESR) is automatically registered by ContentRegistry for all blocks created via EriAnimBlock. No manual call to ClientRegistry.bindTileEntitySpecialRenderer() is needed.
EriAnimBlock — The Builder (recommended)
EriAnimBlock is the simplest way to create an animated block. It is a builder
(a fluent API where you chain methods) that automatically configures everything needed:
the TileEntity, the TESR, the blockstate, and the item rendering in hand.
Builder for animated blocks with 3D Blockbench models. Automatically configures TileEntity + TESR + blockstate + item renderer.
-
Staticstatic EriAnimBlock create(String modId, String registryName)Creates a new builder. The
modIdis your mod's identifier (e.g.,"mymod"). TheregistryNameis the block's internal name (e.g.,"lootbox"). -
SetterEriAnimBlock material(Material mat)Sets the block's material (e.g.,
Material.IRON,Material.WOOD,Material.ROCK). Determines step sound, whether the block is flammable, etc. -
SetterEriAnimBlock hardness(float hardness)Sets the block's hardness (time to break). Examples: stone = 1.5, iron = 5.0, obsidian = 50.0.
-
SetterEriAnimBlock resistance(float resistance)Sets the explosion resistance. Higher values make the block more resistant to TNT and creepers.
-
SetterEriAnimBlock harvestTool(String tool, int level)Sets the required tool and minimum harvest level to break the block. Tools:
"pickaxe","axe","shovel". Levels: 0 = wood, 1 = stone, 2 = iron, 3 = diamond. -
SetterEriAnimBlock soundType(SoundType sound)Sets the block's sound type (walking on it, breaking it, placing it). Examples:
SoundType.METAL,SoundType.WOOD,SoundType.STONE. -
SetterEriAnimBlock lightLevel(float light)Sets the light level emitted by the block (0.0 to 1.0). A torch = 0.9375, glowstone = 1.0.
-
SetterEriAnimBlock creativeTab(CreativeTabs tab)Sets the creative menu tab where the block appears.
-
SetterEriAnimBlock drops(Item item, int min, int max)Sets what the block drops when broken.
minandmaxdefine the random quantity range. -
SetterEriAnimBlock silkTouchable(boolean silk)If
true, the block can be collected with Silk Touch. -
SetterEriAnimBlock model(String modelId)Required. Sets the Blockbench model to use. Format:
"modid:path/model"(without the.jsonextension). The file must be atassets/modid/models/path/model.json.
Example:"mymod:block/lootbox"→assets/mymod/models/block/lootbox.json -
SetterEriAnimBlock animation(String animFileId)Required. Sets the
.erianim.jsonanimation file. Format:"modid:path/anim"(without the.erianim.jsonextension). The file must be atassets/modid/animations/path/anim.erianim.json.
Example:"mymod:animations/lootbox"→assets/mymod/animations/animations/lootbox.erianim.json -
SetterEriAnimBlock renderDistance(double distance)Sets the maximum distance (in blocks) at which the 3D model is rendered. Default:
64blocks. Beyond that, the block is invisible. -
SetterEriAnimBlock defaultAnimation(String animName)Sets an animation that plays automatically when the block is loaded (with LOOP EndBehavior). Ideal for idle/breathing. Leave empty for no automatic animation.
-
SetterEriAnimBlock tileEntity(Class<?> tileClass)Replaces the automatic TileEntity with a custom class. The class must extend
AnimatedBlockTileEntityand have a no-argument constructor. Use this when you need custom callbacks (see Callbacks section). -
CallbackEriAnimBlock onRightClick(Consumer<BlockActionContext> handler)Code executed when a player right-clicks the block. The
BlockActionContextcontainsworld,pos,player,hand,facing. -
CallbackEriAnimBlock onBreak(Consumer<BlockActionContext> handler)Code executed when the block is broken by a player.
-
CallbackEriAnimBlock onPlace(Consumer<BlockActionContext> handler)Code executed when the block is placed by a player.
-
SetterEriAnimBlock javaAnimation(String name, EriAnimJava anim) v1.6.5Registers a Java animation directly on the builder — no need to subclass
AnimatedBlockTileEntityjust to calladdJavaAnimation(). The instance is automatically registered on the TileEntity (server + client) at creation time. Coexists with.animation()for.erianimfiles. Can be called multiple times. -
FinalEriAnimBlock register()Registers the block. Automatically configures: TileEntity, TESR, render type
ENTITYBLOCK_ANIMATED, item renderer, andopaque/fullCube = false. If no custom TileEntity is specified, usesAnimatedBlockTileEntityGeneric.
EriAnimBlock.create("mymod", "refining_machine")
.material(Material.IRON)
.hardness(5.0f)
.resistance(10.0f)
.harvestTool("pickaxe", 2)
.soundType(SoundType.METAL)
.lightLevel(0.5f)
.model("mymod:block/refining_machine")
// Java anims declared directly on the builder (v1.6.5)
.javaAnimation("idle", new IdleAnim())
.javaAnimation("work", new WorkAnim())
.defaultAnimation("idle") // auto-play on spawn
.renderDistance(48)
.onRightClick(ctx -> {
AnimatedBlockTileEntity te = (AnimatedBlockTileEntity)
ctx.world.getTileEntity(ctx.pos);
if (te != null) te.playAnimation("work");
})
.onBreak(ctx -> {
// Drop items when the block is broken
dropItems(ctx.world, ctx.pos);
})
.register();
.tileEntity(MyTE.class)? The
.javaAnimation() builder covers 99% of cases. You only need a custom
TileEntity class if you want to override animation callbacks
(onAnimationComplete, onAnimationEvent,
onAnimationCallback) or add custom fields / NBT to the TE.
Java Animation API v1.6.4
Since v1.6.4, EriAPI lets you write animations directly in Java, bypassing the
.erianim file pipeline. This is useful for procedural animations (sin/cos
oscillations, reactions to entity state, parametric movements, etc.) that would be tedious or impossible to
express in JSON.
A Java animation is a class extending EriAnimJava with two extension points:
defineKeyframes(AnimContext ctx)— static keyframe declarations (equivalent to.erianimJSON). Called once and cached.compute(AnimContext ctx, AnimationPose pose)— procedural modifications applied on top of the interpolated pose. Called every frame.
Java animations coexist with .erianim animations on the same TileEntity. The same
te.playAnimation("name") API automatically dispatches to the right path based on the registered name.
EriAnimJava — abstract class
Create a class that extends EriAnimJava and implements at minimum duration():
import fr.eri.eriapi.anim.*;
import fr.eri.eriapi.gui.anim.Easing;
public class WalkAnim extends EriAnimJava {
@Override
public void defineKeyframes(AnimContext ctx) {
// Equivalent to .erianim JSON, but in code
ctx.at(0)
.group("leg_left").rotateTo(-30, 0, 0).easing(Easing.EASE_IN_OUT)
.group("leg_right").rotateTo(30, 0, 0).easing(Easing.EASE_IN_OUT);
ctx.at(15)
.group("leg_left").rotateTo(30, 0, 0)
.group("leg_right").rotateTo(-30, 0, 0);
ctx.at(30)
.group("leg_left").rotateTo(-30, 0, 0)
.group("leg_right").rotateTo(30, 0, 0);
}
@Override
public void compute(AnimContext ctx, AnimationPose pose) {
// Procedural: head bobbing on sin(t)
float t = ctx.animTick + ctx.partial;
pose.group("head").addRotateY((float) Math.sin(t * 0.2f) * 5f);
}
@Override public float duration() { return 30f; }
@Override public EndBehavior endBehavior() { return EndBehavior.LOOP; }
}
Overridable methods
| Method | Return | Description |
|---|---|---|
defineKeyframes(AnimContext ctx) | void | Declarative. Optional. Cached (rebuilt on invalidateDef()). |
compute(AnimContext ctx, AnimationPose pose) | void | Procedural. Optional. Called every frame. |
duration() | float | Required. Total duration in ticks. |
endBehavior() | EndBehavior | End behavior (default FREEZE). See EndBehaviors. |
invalidateDef() | void | Forces a rebuild of defineKeyframes() on the next frame. |
Registration and playback
// In the TileEntity constructor (or a shared server+client init)
public MyTileEntity() {
addJavaAnimation("walk", new WalkAnim());
addJavaAnimation("idle", new IdleAnim());
}
// Playback (server-side, like .erianim)
te.playAnimation("walk");
te.pauseAnimation();
te.resumeAnimation();
te.resetAnimationInstant();
AnimContext
Context object passed to defineKeyframes() and compute(). Public mutable fields:
| Field | Type | Description |
|---|---|---|
animTick | float | Current animation tick (with partial part). Available in compute(). |
partial | float | Raw partial ticks [0.0, 1.0) for fine interpolation. |
worldTick | long | World tick at render time. |
target | Object | The TileEntity or entity. Cast via the helpers below. |
Methods
| Method | Return | Description |
|---|---|---|
at(int tick) | TickBuilder | Starts a keyframe at this tick. Used in defineKeyframes(). |
asEntity() | EntityLiving | Cast of target, or null if not an entity. |
asTileEntity() | TileEntity | Cast of target, or null if not a TE. |
DSL at(tick).group(name).property(...)
The fluent defineKeyframes() API lets you chain keyframes and groups at a same tick:
ctx.at(0)
.group("arm_left").rotateTo(0, 0, 0)
.group("arm_right").rotateTo(0, 0, 0)
.group("body").translateTo(0, 0, 0).easing(Easing.EASE_OUT)
.at(20)
.group("arm_left").rotateTo(0, 45, 0)
.group("arm_right").rotateTo(0, -45, 0).scaleTo(1.1f, 1.1f, 1.1f)
.group("body").translateY(0.2f);
GroupKeyframeBuilder — methods
| Method | Description |
|---|---|
rotateTo(x, y, z) / rotateX/Y/Z(v) | Sets the target rotation (in degrees). |
translateTo(x, y, z) / translateX/Y/Z(v) | Sets the target translation. |
scaleTo(x, y, z) / scale(s) | Sets the scale (uniform or per-axis). |
visible(boolean) | Sets visibility at this tick (instantaneous). |
easing(Easing) | Easing used to reach this keyframe (default LINEAR). |
group(name) | Switches to another group at the same tick. |
at(tick) | Moves to a new tick. |
GroupPoseBuilder — modify the pose in compute()
In compute(), use pose.group("name").property(...) to modify the interpolated pose.
Two families of methods:
add*(additive) — adds to the value produced by keyframes. Typical for stacking procedural effects on top of a declarative animation.set*/ no prefix (replace) — overwrites the value produced by keyframes.
Example — procedural head oscillation
@Override
public void compute(AnimContext ctx, AnimationPose pose) {
float t = ctx.animTick + ctx.partial;
// Head: sway left/right
pose.group("head")
.addRotateY((float) Math.sin(t * 0.15f) * 8f)
.addRotateZ((float) Math.cos(t * 0.10f) * 3f);
// Body: small vertical breathing
pose.group("body").addTranslateY((float) Math.sin(t * 0.05f) * 0.05f);
// React to entity state (if target is an entity)
if (ctx.asEntity() != null) {
float yaw = ctx.asEntity().rotationYaw;
pose.group("head").addRotateY(yaw * 0.1f);
}
}
GroupPoseBuilder methods
| Category | Methods |
|---|---|
| Rotation (set) | rotateX(v), rotateY(v), rotateZ(v), rotateTo(x,y,z) |
| Rotation (additive) | addRotateX(v), addRotateY(v), addRotateZ(v) |
| Translation (set) | translateX(v), translateY(v), translateZ(v), translateTo(x,y,z) |
| Translation (additive) | addTranslateX(v), addTranslateY(v), addTranslateZ(v) |
| Scale | scaleTo(x,y,z), scale(s) |
| Visibility | visible(boolean) |
| Chain | group(String) — switch to another group |
add* by default, layered on top of defineKeyframes().
Use set* only when you want to completely override the keyframe result (e.g. last-mile
adjustments based on a runtime parameter).
Overlay — play an animation on top of another
The overlay slot lets you play a second Java animation on top of the main one. The overlay wins on every group it touches; groups it does not touch keep the main animation's pose.
The overlay is client-local — it is not synced via packets. Use it for reactive visual effects (flash, weapon recoil, temporary glow) triggered on the client only.
// Start the overlay
te.playOverlay("flash", new FlashOverlayAnim());
// Stop it
te.stopOverlay();
// Check
boolean active = te.hasOverlay();
String name = te.getOverlayAnimName();
Overlay API
| Method | Return | Description |
|---|---|---|
playOverlay(String, EriAnimJava) | void | Plays a Java anim as overlay. Loops automatically. |
stopOverlay() | void | Stops the overlay. |
hasOverlay() | boolean | true if an overlay is active. |
getOverlayAnimName() | String | Overlay name, or null. |
addJavaAnimation(name, anim) | void | Registers a Java anim in the main registry. |
isJavaAnimation(name) | boolean | true if name is in the Java registry. |
getJavaAnimation(name) | EriAnimJava | Retrieves a registered anim. |
EndBehavior values FREEZE, WAIT_SERVER,
LOOP, LOOP_COUNT, LOOP_PAUSE and instant reset are supported for Java anims.
ROLLBACK is treated as an instant reset (no exposed rollbackDuration).
Alternative method: EriBlock + Custom TileEntity
If you need complex server-side logic in your animated block (inventory management, conditional
interactions, animation chaining...), you can create a subclass of AnimatedBlockTileEntity
and pass it to the builder via .tileEntity().
import fr.eri.eriapi.anim.AnimatedBlockTileEntity;
import fr.eri.eriapi.anim.AnimationEvent;
public class TileLootbox extends AnimatedBlockTileEntity {
private boolean isOpen = false;
// No-argument constructor is REQUIRED
public TileLootbox() {
super("mymod:block/lootbox", "mymod:block/lootbox");
// ^--- modelId ^--- animFileId
}
@Override
protected void onAnimationCallback(String callbackName) {
if ("open_complete".equals(callbackName)) {
isOpen = true;
// Give rewards to the player, etc.
}
}
@Override
protected void onAnimationComplete(String animName) {
if ("close".equals(animName)) {
isOpen = false;
}
}
@Override
protected void onAnimationEvent(AnimationEvent event) {
if (event.getType() == AnimationEvent.EventType.PARTICLE) {
// Spawn custom particles
spawnParticles(event.getCount(), event.getSpread());
}
}
public boolean isOpen() { return isOpen; }
}
EriAnimBlock.create("mymod", "lootbox")
.material(Material.WOOD)
.hardness(3.0f)
.model("mymod:block/lootbox")
.animation("mymod:block/lootbox")
.tileEntity(TileLootbox.class) // ← Our custom TE
.onRightClick(ctx -> {
TileLootbox te = (TileLootbox) ctx.world.getTileEntity(ctx.pos);
if (te != null && !te.isOpen()) {
te.playAnimation("open");
}
})
.register();
Animated block — all approaches v1.6.5
Since v1.6.5, EriAnimBlock supports two kinds of animations:
.animation() for .erianim JSON files (original approach) and
.javaAnimation() for Java-coded animations. Both coexist and can be mixed
on the same block.
// Requires assets/mymod/animations/my_machine.erianim.json
EriAnimBlock.create("mymod", "my_machine")
.material(Material.IRON)
.hardness(5.0f)
.model("mymod:block/my_machine")
.animation("mymod:animations/my_machine") // ← .erianim file
.defaultAnimation("idle")
.onRightClick(ctx -> {
AnimatedBlockTileEntity te = (AnimatedBlockTileEntity)
ctx.world.getTileEntity(ctx.pos);
if (te != null) te.playAnimation("work");
})
.register();
EriAnimBlock.create("mymod", "my_machine")
.material(Material.IRON)
.hardness(5.0f)
.model("mymod:block/my_machine")
// Java anims directly on the builder — no custom TE needed
.javaAnimation("idle", new IdleAnim())
.javaAnimation("work", new WorkAnim())
.defaultAnimation("idle") // auto-play on spawn
.onRightClick(ctx -> {
AnimatedBlockTileEntity te = (AnimatedBlockTileEntity)
ctx.world.getTileEntity(ctx.pos);
if (te != null) te.playAnimation("work");
})
.register();
EriAnimBlock.create("mymod", "my_machine")
.model("mymod:block/my_machine")
.animation("mymod:animations/my_machine") // .erianim animations
.javaAnimation("glow", new GlowPulseAnim()) // Java animation on top
.register();
.tileEntity(MyTE.class)? The
.javaAnimation() builder is enough to declare/play animations. You only need
a custom TE class to override animation callbacks
(onAnimationComplete, onAnimationEvent,
onAnimationCallback) or to add custom fields / NBT. In that case, just call
addJavaAnimation() in your TE constructor.
.animation()? The .animation() call
is still required if the block uses .erianim animations in
addition to Java animations. It can be omitted when every animation is
defined in Java.
AnimatedBlockTileEntity
This is the core of the animation system. Every animated block in the world has an AnimatedBlockTileEntity
that stores the animation state (which animation is playing, at which point, paused or not, etc.).
All control methods (playAnimation, pauseAnimation, etc.) must be called
server-side. The TileEntity automatically sends network packets to clients to synchronize the animation.
TileEntity for blocks rendered with Blockbench models and animated via .erianim files. Implements ITickable but returns immediately when the state is IDLE/FROZEN/PAUSED/WAITING_SERVER to avoid any overhead.
Constructors
-
ConstructorAnimatedBlockTileEntity()Empty constructor. The modelId and animFileId will be configured by the builder or manually via setters.
-
ConstructorAnimatedBlockTileEntity(String modelId)Constructor with the Blockbench model identifier. E.g.,
"mymod:block/lootbox". -
ConstructorAnimatedBlockTileEntity(String modelId, String animFileId)Full constructor.
modelId= 3D model,animFileId= animation file.
Animation control (server only)
-
Servervoid playAnimation(String animName)Plays the animation by name. Uses the default
EndBehaviordefined in the.erianim.jsonfile. Automatically sends a packet to all clients that can see the block.
Side: Server only. Does nothing if called client-side. -
Servervoid playAnimation(String animName, EndBehavior endBehaviorOverride)Plays the animation with a different
EndBehaviorthan the one in the.erianim.jsonfile. Passnullto use the file's default behavior.
Side: Server only. -
Servervoid pauseAnimation()Pauses the animation at the current tick. The block freezes at its current position. Only has an effect if the state is
PLAYING.
Side: Server only. -
Servervoid resumeAnimation()Resumes a paused animation. The block continues where it left off. Only has an effect if the state is
PAUSED.
Side: Server only. -
Servervoid resetAnimation()Resets the animation. If the current animation has a
EndBehaviorof typeROLLBACKand arollbackDuration > 0, the block replays the animation in reverse (smooth return to the initial position). Otherwise, returns instantly to the starting position.
Side: Server only. -
Servervoid resetAnimation(boolean instant)Resets the animation. If
instantistrue, always returns immediately to the starting position (ignores rollback). Iffalse, uses rollback if theEndBehaviorisROLLBACK.
Side: Server only. -
Servervoid stopAnimation()Immediately stops the animation. No rollback, no
onAnimationCompletecallback. The block transitions directly to theIDLEstate. Use to abruptly cut an animation.
Side: Server only.
State and progress
-
Getterboolean isAnimating()Returns
trueif the block is currently playing an animation (PLAYINGorROLLING_BACK). Returnsfalsefor all other states (IDLE, FROZEN, PAUSED, WAITING_SERVER). -
GetterAnimState getAnimState()Returns the current animation state. See the AnimStates section for the list of possible states.
-
GetterString getCurrentAnimation()Returns the name of the current animation, or an empty string
""if no animation is active. -
Getterfloat getAnimationProgress()Returns the animation progress between
0.0(start) and1.0(end). Returns0.0if no animation is active. Returns1.0if the animation is inFROZENorWAITING_SERVERstate.
Side: Client and server. -
GetterString getCurrentAnimName()Returns the internal name of the current animation. Identical to
getCurrentAnimation().
Dynamic textures
-
Servervoid setTexture(String target, String value)Replaces a model texture with another one in real-time.
targetis the original texture key (defined in the Blockbench model, e.g.,"side_core").valueis the replacement texture key (e.g.,"side_core_active"). The replacement texture must exist in the model's texture map. The change is automatically synchronized to clients.
Key resolution: Thetargetparameter is resolved by both the JSON key and the value from the Blockbench model. For example, if the model contains"beam": "rarity_beam", then bothsetTexture("beam", ...)andsetTexture("rarity_beam", ...)work.
Side: Server only. -
Servervoid clearTexture(String target)Removes a specific texture override. The model reverts to the original texture for this key.
Side: Server only. -
Servervoid clearAllTextures()Removes all texture overrides. The model reverts to all its original textures.
Side: Server only. -
GetterMap<String, String> getTextureOverrides()Returns the map of current texture overrides (target → value). Read-only.
Configuration
-
Settervoid setAnimFileId(String animFileId)Changes the animation file used by this TileEntity. Useful for loading different animations on the same model at runtime.
-
GetterString getAnimFileId()Returns the current animation file identifier.
-
Settervoid setModelId(String modelId)Changes the 3D model used by this TileEntity.
-
GetterString getModelId()Returns the current 3D model identifier.
-
Settervoid setMaxRenderDistance(double distance)Changes the maximum render distance (in blocks).
TileEntity Callbacks
If you create a subclass of AnimatedBlockTileEntity, you can override 3 callback methods
to react to animation events. These callbacks are called server-side only.
-
Servervoid onAnimationEvent(AnimationEvent event)Called every time an event from the
.erianim.jsonfile is triggered (sound, particle, callback, hide/show group). Theeventparameter contains the type (event.getType()), the value (event.getValue()), and type-specific properties (volume, pitch, count, spread). -
Servervoid onAnimationComplete(String animName)Called when an animation finishes and its
EndBehavioris resolved. For example, called after a freeze, after a rollback completes, or when aLOOP_COUNTreaches its last iteration. -
Servervoid onAnimationCallback(String callbackName)Called when a
"callback"type event fires in the.erianim.jsonfile. ThecallbackNamecorresponds to the"value"field of the event. This is the primary way to synchronize server logic with the animation.
public class TileMachine extends AnimatedBlockTileEntity {
public TileMachine() {
super("mymod:block/machine", "mymod:block/machine");
}
@Override
protected void onAnimationCallback(String callbackName) {
switch (callbackName) {
case "produce_item":
// The animation reached the point where the item is created
produceOutput();
break;
case "consume_fuel":
// The animation reached the point where fuel is consumed
consumeFuel();
break;
}
}
@Override
protected void onAnimationComplete(String animName) {
if ("process".equals(animName)) {
// The processing animation is complete
// Restart if there is still fuel
if (hasFuel()) {
playAnimation("process");
}
}
}
@Override
protected void onAnimationEvent(AnimationEvent event) {
if (event.getType() == AnimationEvent.EventType.PARTICLE) {
// Spawn smoke particles server-side
// Particles will be visible to all nearby players
if (world instanceof WorldServer) {
((WorldServer) world).spawnParticle(
EnumParticleTypes.SMOKE_NORMAL,
pos.getX() + 0.5, pos.getY() + 1.0, pos.getZ() + 0.5,
event.getCount(), event.getSpread(), 0.1, event.getSpread(),
0.01);
}
}
}
}
Triggering an animation
This section groups the most common practical cases for triggering an animation: on right-click, every tick, in response to a Forge event, from a command, or based on entity behavior (idle, walk, attack). All examples are server-side — EriAPI automatically syncs the animation state to clients.
Block — Right-click (open a lootbox)
Use the onRightClick callback from the EriAnimBlock
builder. Always check isAnimating() to avoid restarting an
animation that is already running. playAnimation() automatically
sends the network packets to clients.
// EriAnimBlock with onRightClick callback
EriAnimBlock.create("mymod", "lootbox")
.animation("mymod:block/lootbox")
.onRightClick(ctx -> {
AnimatedBlockTileEntity te = (AnimatedBlockTileEntity)
ctx.world.getTileEntity(ctx.pos);
if (te != null && !te.isAnimating()) {
te.playAnimation("open"); // Triggers the "open" animation
}
})
.register();
Block — Continuous tick (spinning machine)
For a machine that should animate in a loop while active, subclass
AnimatedBlockTileEntity and override update().
The animation is restarted every time it ends.
resetAnimation() returns the model to its neutral pose.
// AnimatedBlockTileEntity subclass with ITickable
public class TileMachine extends AnimatedBlockTileEntity {
private boolean running = false;
@Override
public void update() {
super.update();
if (!world.isRemote && running && !isAnimating()) {
// Restart the animation in a loop
playAnimation("spin");
}
}
public void setRunning(boolean running) {
this.running = running;
if (!running) resetAnimation();
}
}
Block — React to external event (explosion)
You can trigger an animation from any Forge event via EriEvents.
Here, all animated blocks within a 5-block radius of an explosion play the
"shake" animation.
// Via EriEvents - shake animated blocks near an explosion
EriEvents.on(ExplosionEvent.Detonate.class)
.handle(e -> {
BlockPos center = e.getExplosion().getPosition().toBlockPos();
// Search our blocks within a 5-block radius
for (BlockPos pos : BlockPos.getAllInBoxMutable(
center.add(-5,-5,-5), center.add(5,5,5))) {
TileEntity te = e.world.getTileEntity(pos);
if (te instanceof AnimatedBlockTileEntity) {
((AnimatedBlockTileEntity) te).playAnimation("shake");
}
}
});
Block — Trigger from a command
Useful for debugging or for admin commands. The EriCommand
builder already provides the PosArg argument with
auto-completion.
// Inside an EriCommand
EriCommand.create("machine")
.sub("start")
.arg(PosArg.of("pos"))
.execute(ctx -> {
BlockPos pos = ctx.getPos("pos");
TileEntity te = ctx.getSender().world.getTileEntity(pos);
if (te instanceof AnimatedBlockTileEntity) {
((AnimatedBlockTileEntity) te).playAnimation("run");
ctx.reply("Animation started.");
}
})
.register();
Entity — Idle animation (always active)
The "idle" animation runs continuously. Other animations
(walk, attack) temporarily interrupt it. Remember to set
endBehavior: "loop" in the .erianim.json.
These examples go in your entity Java class extending
EntityLiving (or EntityCreature, EntityMob, etc.)
and implementing IAnimatedEntity. EriEntity is a
configuration builder — it does not provide behavioral callbacks in the current version.
// In your entity Java class (extends EntityLiving implements IAnimatedEntity)
public class EntityMonster extends EntityLiving implements IAnimatedEntity {
@Override
public void onUpdate() {
super.onUpdate();
if (!world.isRemote) {
// Start idle if no animation is playing
if (dataManager.get(ANIM_NAME).isEmpty()) {
playAnimation("idle");
}
}
}
}
Entity — Walk animation (movement-based)
Derive the animation state from the entity's speed. The walk ↔ idle transition happens automatically once a speed threshold is crossed.
// In your entity Java class (extends EntityLiving implements IAnimatedEntity)
public class EntityMonster extends EntityLiving implements IAnimatedEntity {
@Override
public void onUpdate() {
super.onUpdate();
if (!world.isRemote) {
double speed = Math.sqrt(motionX * motionX + motionZ * motionZ);
String current = dataManager.get(ANIM_NAME);
if (speed > 0.01 && !"walk".equals(current)) {
playAnimation("walk"); // Start walking
} else if (speed <= 0.01 && "walk".equals(current)) {
playAnimation("idle"); // Back to idle
}
}
}
}
Entity — Attack animation (on hit)
attackEntityAsMob is called server-side when the entity hits.
Trigger the animation after confirming that the hit landed.
// In your entity Java class (extends EntityMob implements IAnimatedEntity)
public class EntityMonster extends EntityMob implements IAnimatedEntity {
@Override
public boolean attackEntityAsMob(Entity target) {
boolean result = super.attackEntityAsMob(target);
if (result && !world.isRemote) {
playAnimation("attack"); // Play the attack animation
}
return result;
}
}
Animated Item — Animation playback v1.3.2
AnimatedItemController plays .erianim.json
animations on animated items in real time. Playback state is shared
per modelId: all items rendered with the same model
share the same animation.
// Declare the item with an animated model
EriItem.create("mymod", "sword")
.animatedModel("mymod:item/sword")
.onRightClick(ctx -> {
if (ctx.world.isRemote) {
AnimatedItemController.play("mymod:item/sword", "use");
}
})
.register();
// From anywhere on the client side
AnimatedItemController.play("mymod:item/sword", "mymod:item/sword_anims", "idle"); // explicit animFileId
AnimatedItemController.play("mymod:item/sword", "swing"); // animFileId = modelId
AnimatedItemController.stop("mymod:item/sword");
boolean playing = AnimatedItemController.isPlaying("mymod:item/sword");
animFileId follows the same format as blocks:
"modid:path" resolves to
assets/modid/animations/path.erianim.json.
.erianim.json File Format
The .erianim.json file is a JSON file that describes all animations for a block.
A single file can contain multiple animations (idle, open, close, process...).
Root structure
{
"format_version": 1,
"model": "mymod:block/my_machine",
"animations": {
"idle_breathe": { ... },
"open": { ... },
"close": { ... },
"process": { ... }
}
}
| Field | Type | Description |
|---|---|---|
format_version |
int | Format version. Currently 1. |
model |
string | Associated Blockbench model identifier (e.g., "mymod:block/machine"). Informational. |
animations |
object | An object where each key is the animation name and the value is its definition. |
Animation structure
{
"duration": 40,
"endBehavior": "freeze",
"rollbackDuration": 10,
"blendIn": 5,
"blendOut": 0,
"loopCount": 3,
"loopPause": 20,
"tracks": { ... },
"events": [ ... ]
}
| Field | Type | Default | Description |
|---|---|---|---|
duration |
int | 20 | Total duration in ticks (20 ticks = 1 second). |
endBehavior |
string | "freeze" | What to do when the animation ends. See EndBehaviors. |
rollbackDuration |
int | 10 | Return animation duration in ticks (for endBehavior: "rollback"). |
blendIn |
int | 0 | Blend-in duration (in ticks) from the previous pose. 0 = no blending. |
blendOut |
int | 0 | Blend-out duration (in ticks). 0 = no blending. |
loopCount |
int | 0 | Number of loops for endBehavior: "loop_count". |
loopPause |
int | 0 | Pause in ticks between each loop for endBehavior: "loop_pause". |
tracks |
object | {} | Animation tracks for each model group. Key = group name. |
elementTracks v1.5.1 |
object | {} | Animation tracks targeting individual elements inside a group. Key = "groupName:elementIndex" (e.g. "body:2"). Same structure as a group track. See Element-level animation. |
events |
array | [] | List of events triggered at specific ticks during the animation. |
Track Types
Each group of your Blockbench model can have one or more tracks (animation channels). Each track contains a list of keyframes — points in time where you define a value. The system automatically interpolates between keyframes using the chosen easing function.
Here are the 7 available track types:
Defines the absolute rotation of the group in degrees (0-359) on each axis. This is the fixed orientation of the group at a given instant. Values are interpolated between keyframes.
"rotation": [
{ "tick": 0, "value": [0, 0, 0], "easing": "linear" },
{ "tick": 20, "value": [-90, 0, 0], "easing": "ease_out_back" },
{ "tick": 40, "value": [-90, 45, 0], "easing": "ease_in_out" }
]
Cumulative rotation in degrees since the beginning of the animation. Unlike rotation,
values are not limited to 0-359: you can go beyond 360 to make multiple turns.
For example, [0, 720, 0] = 2 full turns on the Y axis.
Adds on top of the pose rotation (rotation).
"rotate": [
{ "tick": 0, "value": [0, 0, 0], "easing": "linear" },
{ "tick": 60, "value": [0, 720, 0], "easing": "ease_in_out" }
]
Moves the group in Blockbench pixels (1 Blockbench pixel = 1/16 of a Minecraft block).
[0, 16, 0] = move 1 block upward.
[0, -4, 0] = move 0.25 blocks downward.
"translation": [
{ "tick": 0, "value": [0, 0, 0], "easing": "linear" },
{ "tick": 10, "value": [0, -3, 0], "easing": "ease_out" },
{ "tick": 20, "value": [0, 0, 0], "easing": "ease_in" }
]
Changes the size of the group on each axis. [1, 1, 1] = normal size.
[2, 2, 2] = double size. [0, 0, 0] = invisible (crushed to zero).
"scale": [
{ "tick": 0, "value": [1, 1, 1], "easing": "linear" },
{ "tick": 10, "value": [1.5, 1.5, 1.5], "easing": "ease_out_back" },
{ "tick": 20, "value": [1, 1, 1], "easing": "ease_in" }
]
Shows or hides a group at a specific tick. No interpolation — it is an on/off switch.
The value field is a boolean: true = visible, false = invisible.
"visible": [
{ "tick": 0, "value": true },
{ "tick": 8, "value": false }
]
Rotates the group continuously at a given speed (in degrees per tick).
Unlike rotation which defines a fixed angle, spin defines a rotation speed
that accumulates frame after frame. Ideal for gears, fans, etc.
You can change the spin speed during the animation. The easing controls the transition between the old and new speed.
"spin": [
{ "tick": 0, "axis": "y", "speed": 0, "easing": "linear" },
{ "tick": 10, "axis": "y", "speed": 18, "easing": "ease_in" },
{ "tick": 30, "axis": "y", "speed": 18, "easing": "linear" },
{ "tick": 40, "axis": "y", "speed": 0, "easing": "ease_out" }
]
| Field | Type | Description |
|---|---|---|
tick |
int | Tick when this speed takes effect. |
axis |
string | Rotation axis: "x", "y", or "z". Default: "y". |
speed |
float | Rotation speed in degrees per tick. 18 = 1 full turn in 20 ticks (1 second). 0 = stop. |
easing |
string | Easing function for the transition to this speed. Default: "linear". |
Changes the texture of a face or element of the model at a specific tick. No interpolation — the change is instant. Replacement textures must be declared in the Blockbench model's texture map.
"texture": [
{ "tick": 0, "target": "screen", "value": "screen_off" },
{ "tick": 10, "target": "screen", "value": "screen_on" }
]
| Field | Type | Description |
|---|---|---|
tick |
int | Tick when the texture change applies. |
target |
string | Texture key to replace (name in the model's texture map, e.g., "screen"). |
value |
string | Replacement texture key (e.g., "screen_on"). |
Standard keyframe format
The rotation, rotate, translation, and scale tracks all use
the same keyframe format:
| Field | Type | Description |
|---|---|---|
tick |
int | Position in time (in ticks). 0 = start of the animation. |
value |
float[3] | Value [x, y, z]. Degrees for rotation/rotate, BB pixels for translation, factor for scale. |
easing |
string | Interpolation function. Default: "linear". See Easings. |
Element-level animation v1.5.1
By default, an animation track applies to the entire Blockbench group: every cube attached to
the group rotates / translates / scales together around the group's pivot. Since v1.5.1, you can also animate
individual elements inside a group. Each element (Blockbench cube) owns a
rotation.origin (pivot) defined in the model and becomes independently animatable.
When to use elementTracks?
- A button, a trigger, a mechanical part inside a group that must move independently of other cubes in the same group.
- Multiple elements of the same group that must rotate around different pivots (e.g. turbine blades sharing a "turbine" group).
- Cases where you don't want (or can't) restructure the Blockbench model into sub-groups.
If each moving part already deserves its own Blockbench group, just use tracks — it's more
readable and cheaper at runtime.
JSON format
The elementTracks field sits next to tracks in the animation definition. Keys follow
the "groupName:elementIndex" format, where elementIndex is the
zero-based position of the element in its Blockbench group's elements array. The value has the same
structure as a group track (rotation, rotate, translation, scale, visible, spin — see
Track Types).
{
"formatVersion": 1,
"modelId": "mymod:block/turret",
"animations": {
"idle": {
"duration": 40,
"endBehavior": "loop",
"tracks": {
"base": {
"rotation": [
{ "tick": 0, "value": [0, 0, 0] },
{ "tick": 40, "value": [0, 360, 0] }
]
}
},
"elementTracks": {
"body:2": {
"translation": [
{ "tick": 0, "value": [0, 0, 0] },
{ "tick": 20, "value": [0, 2, 0] },
{ "tick": 40, "value": [0, 0, 0] }
]
}
}
}
}
}
elementTracks is absent, behavior is identical to previous versions: only group tracks apply and no extra OpenGL matrix is pushed per element.
Java API
The entire animation stack (AnimationDef, AnimationPose, AnimationController,
AnimatedBlockTESR, AnimatedEntityRenderer) supports element poses. You can read an
element pose with AnimationPose.getElement(String groupName, int elementIndex).
// Check for element tracks before iterating
if (animDef.hasElementTracks()) {
for (Map.Entry<String, GroupTrack> entry : animDef.getElementTracks().entrySet()) {
// entry.getKey() = "body:2"
// entry.getValue() = GroupTrack
}
}
// Direct access via group + index
GroupTrack t = animDef.getElementTrack("body", 2);
// Canonical key (static helper)
String key = AnimationDef.elementTrackKey("body", 2); // -> "body:2"
AnimationPose pose = controller.getCurrentPose(partialTicks);
// Group pose
AnimationPose.GroupPose bodyPose = pose.getGroup("body");
// Element pose (null if no track exists for that element)
AnimationPose.GroupPose elemPose = pose.getElement("body", 2);
if (elemPose != null) {
if (!elemPose.visible) return; // element is hidden
float rotY = elemPose.rotation[1];
// ...
}
Rendering — transform order
In AnimatedBlockTESR and AnimatedEntityRenderer, for each animated element, the OpenGL
matrix is pushed around the element's rotationOrigin in this order:
translate(rotationOrigin)- Element's static JSON rotation (if
rotationis set in the Blockbench model). - Animated translation (
elemPose.translation / 16, converted from Blockbench pixels). - Animated rotations (
rotationZ/Y/X thencumulativeRotationY/X/Z — the spin part). - Animated scale (
elemPose.scale). translate(-rotationOrigin).- Render the element's faces.
The element's static JSON rotation is preserved — the animation applies on top, in the local pivot space. If the element has neither an animated pose nor a static rotation, no extra matrix is pushed (optimization: the zero-cost path of previous versions).
Events in .erianim.json
Events are actions triggered at specific ticks during the animation. They are listed in the "events" array
of each animation. Each event has a tick and a type.
The 6 event types
| Type | Description | Additional fields |
|---|---|---|
sound |
Plays a Minecraft sound at the block's position. The sound is audible to all nearby players. | value (sound ResourceLocation), volume (default: 1.0), pitch (default: 1.0) |
particle |
Triggers the onAnimationEvent() callback in the TileEntity. You handle particle spawning in your Java code. |
value (particle name), count (default: 1), spread (default: 0.5) |
callback |
Calls onAnimationCallback(value) in the TileEntity. This is the primary way to synchronize server logic with the animation. |
value (callback name) |
hide_group |
Hides a model group. The event is dispatched server-side for information. The renderer uses the visible track client-side. |
value (group name) |
show_group |
Shows a model group. Same behavior as hide_group but in reverse. |
value (group name) |
cut |
Immediately stops the animation and resets the block to the IDLE state. Equivalent to calling resetAnimationInstant(). |
value (ignored) |
"events": [
{
"tick": 0,
"type": "sound",
"value": "minecraft:block.anvil.use",
"volume": 0.8,
"pitch": 1.5
},
{
"tick": 10,
"type": "particle",
"value": "smoke",
"count": 5,
"spread": 0.3
},
{
"tick": 20,
"type": "callback",
"value": "halfway_done"
},
{
"tick": 5,
"type": "hide_group",
"value": "lock"
},
{
"tick": 30,
"type": "show_group",
"value": "reward"
},
{
"tick": 40,
"type": "cut"
}
]
EndBehaviors
The EndBehavior determines what happens when an animation reaches its last tick.
It is defined in the .erianim.json file ("endBehavior" field) or can be
overridden in Java via te.playAnimation("open", EndBehavior.LOOP).
| Value | JSON | Description | Use case |
|---|---|---|---|
FREEZE |
"freeze" |
Stays frozen on the last frame indefinitely. | Open door, open chest, pulled lever. |
ROLLBACK |
"rollback" |
Replays the animation in reverse over rollbackDuration ticks to return to frame 0. |
Button that springs back up, machine that gradually powers down. |
ROLLBACK_INSTANT |
"rollback_instant" |
Returns instantly to frame 0 without a return animation. | Flash animation, quick visual impact. |
WAIT_SERVER |
"wait_server" |
Stays frozen on the last frame and waits for a server call (te.resetAnimation()) to return. |
Machine processing, chest awaiting confirmation. The server decides when to reset. |
LOOP |
"loop" |
Infinite loop. The animation automatically restarts from the beginning at each end. | Spinning gear, pulsing flame, looping idle. |
LOOP_COUNT |
"loop_count" |
Loops N times (configured by loopCount) then freezes on the last frame. |
Charge animation (3 blinks), repeated light signal. |
LOOP_PAUSE |
"loop_pause" |
Infinite loop with a pause of loopPause ticks between each cycle. |
Machine that processes in batches (cycle, pause, cycle, pause...). |
AnimatedBlockTileEntity te = (AnimatedBlockTileEntity) world.getTileEntity(pos);
// Use the default EndBehavior from the .erianim.json file
te.playAnimation("process");
// Force a different EndBehavior
te.playAnimation("process", EndBehavior.LOOP);
te.playAnimation("open", EndBehavior.WAIT_SERVER);
te.playAnimation("flash", EndBehavior.ROLLBACK_INSTANT);
// Pass null = use the file's default
te.playAnimation("idle", null);
Easings
Easing functions control how a value transitions from one keyframe to another.
Instead of a linear (constant) movement, you can have acceleration, deceleration, bounce, etc.
The easing is defined in each keyframe by the "easing" field.
Each function takes a normalized time t between 0 and 1 and returns an interpolated value.
Some functions (ELASTIC, BOUNCE, BACK, SPRING) can return values outside 0-1, which produces overshoot effects.
| JSON Name | Java Enum | Description | Typical usage |
|---|---|---|---|
"linear" |
LINEAR |
Constant movement, no acceleration. f(t) = t |
Mechanical rotations, machine movements. |
"ease_in" |
EASE_IN |
Starts slow, accelerates at the end. f(t) = t^2 |
Object starting to move, vehicle starting up. |
"ease_out" |
EASE_OUT |
Starts fast, slows down at the end. f(t) = t(2-t) |
Door closing, object settling down. |
"ease_in_out" |
EASE_IN_OUT |
Slow at start and end, fast in the middle. | Natural movements, lids, levers. |
"ease_in_cubic" |
EASE_IN_CUBIC |
Like ease_in but more pronounced. f(t) = t^3 |
Falls, strong accelerations. |
"ease_out_cubic" |
EASE_OUT_CUBIC |
Like ease_out but more pronounced. Faster braking. | Heavy object stopping, dampened bounce. |
"bounce" |
BOUNCE |
Bounce on arrival, like a ball hitting the ground. | Object falling and bouncing, button clicking. |
"elastic" |
ELASTIC |
Elastic oscillation on arrival, like a spring. Overshoots the target then returns. | Pop-up, element appearing with punch. |
"ease_in_back" |
EASE_IN_BACK |
Pulls back slightly before moving forward (overshoot at entry). | Strike preparation, wind-up before a jump. |
"ease_out_back" |
EASE_OUT_BACK |
Overshoots the target then returns (overshoot at exit). Very satisfying "pop" effect. | Chest lid opening with punch, item growing. |
"ease_in_expo" |
EASE_IN_EXPO |
Exponential start — very slow then explosion. f(t) = 2^(10(t-1)) |
Energy charge, power build-up. |
"ease_out_expo" |
EASE_OUT_EXPO |
Exponential deceleration — brakes much harder than cubic. f(t) = 1 - 2^(-10t) |
Emergency braking, abrupt stop. |
"spring" |
SPRING |
Physical spring simulation — dampened oscillations. f(t) = 1 - cos(t*4.5*PI) * e^(-6t) |
Antennas, flexible elements, hit feedback. |
In the .erianim.json file, the easing is written in lowercase with underscores or hyphens.
Both notations are accepted: "ease_out_back" and "ease-out-back" are equivalent.
Uppercase is also accepted. If the name is not recognized, LINEAR is used by default.
AnimStates
The TileEntity's animation state is accessible via te.getAnimState().
It allows you to know exactly what the block is doing at a given moment.
| State | Description | Server tick? |
|---|---|---|
IDLE |
No animation in progress. The block is at rest. The TESR displays the model's default pose. | No |
PLAYING |
The animation is currently playing. Events are triggered and tracks are interpolated. | Yes |
PAUSED |
The animation is paused at a specific tick. The block is frozen at the position where it was paused. | No |
FROZEN |
The animation is finished and frozen on the last frame (endBehavior = FREEZE or completed LOOP_COUNT). |
No |
ROLLING_BACK |
The animation is playing in reverse, returning to frame 0 (endBehavior = ROLLBACK). |
Yes |
WAITING_SERVER |
The animation is finished and waiting for a server signal (te.resetAnimation()) to return to IDLE. |
No |
AnimatedBlockTileEntity te = (AnimatedBlockTileEntity) world.getTileEntity(pos);
// Check if an animation is in progress
if (te.isAnimating()) {
// PLAYING or ROLLING_BACK
}
// Check a specific state
if (te.getAnimState() == AnimState.WAITING_SERVER) {
// The animation is finished, the server must act
te.resetAnimation();
}
// Check the progress
float progress = te.getAnimationProgress(); // 0.0 to 1.0
if (progress > 0.5f) {
// The animation has passed the halfway point
}
Animated Items v1.3.1
Since version 1.3.1, you can use the .animatedModel() method on an
EriItem to render the item with a 3D Blockbench model instead of the
standard 2D texture. The item will be displayed in 3D in the player's hand and in the inventory.
Rendering is delegated to an internal TileEntityItemStackRenderer
(AnimatedBlockItemRenderer) that uses the same rendering pipeline as
AnimatedBlockTESR — groups, elements, faces, UV, textures.
Usage
Simply call .animatedModel("modid:item/name") in the builder chain.
The Blockbench JSON model must be located at assets/{modid}/models/item/{name}.json.
EriItem.create("mymod", "fancy_sword")
.maxStackSize(1)
.rarity(EnumRarity.EPIC)
.animatedModel("mymod:item/fancy_sword") // 3D Blockbench model
.register();
The item's textures are defined directly in the Blockbench model JSON file, not in Java code. The renderer automatically reads the textures referenced by the model.
The Blockbench JSON model must be placed at assets/{modid}/models/item/{name}.json.
The ID passed to animatedModel() uses the format "modid:item/name".
Animated Entities v1.3.0
The animated entity system allows you to create custom Minecraft entities rendered with
3D Blockbench models and .erianim.json animations. It relies on three
components: the EriEntity builder, the AnimatedEntityRenderer, and
the IAnimatedEntity interface.
EriEntity — Fluent builder
EriEntity is the builder for defining a custom entity. It stores the configuration
(model, textures, animations, hitbox, seats) in an EntityDefinition registered in
the ContentRegistry.
Unlike EriItem and EriBlock, EriEntity
does not handle automatic Forge registration. The mod must register
EntityEntry entries itself in RegistryEvent.Register<EntityEntry>
using ContentRegistry.getEntityDefs() to retrieve the definitions.
Builder methods
| Method | Description |
|---|---|
create(String modId, String registryName) | Creates a new entity builder. Static method. |
model(String modelId) | Sets the Blockbench model (e.g. "mymod:entity/monster"). |
texture(String key, String path) | Maps a model texture key to a ResourceLocation path. |
animation(String name, String animId) | Maps an animation name (e.g. "idle", "walk") to an .erianim ID. |
javaAnimation(String name, EriAnimJava anim) v1.6.5 | Registers a Java animation directly on the builder. Stored in EntityDefinition.javaAnimations, retrievable via ContentRegistry.getEntityDef(modId, name).getJavaAnimation("idle"). Coexists with .animation(). |
hitbox(float width, float height) | Sets the hitbox dimensions. |
seat(float x, float y, float z) | Adds a simple passenger seat (Blockbench pixel coordinates). |
seat(float x, float y, float z, String attachedGroup) | Adds a seat attached to a model group. |
seat(float x, float y, float z, float yawOffset, String attachedGroup, boolean lockCamera) | Full seat: yaw rotation, attached group, camera lock. |
spawnEgg(int primary, int secondary) | Enables a spawn egg (colors in 0xRRGGBB). |
creativeTab(String tabName) | Creative tab for the spawn egg. |
register() | Registers the definition in the ContentRegistry. |
IAnimatedEntity — Entity interface
The Java entity must implement the IAnimatedEntity interface to provide animation
poses to the renderer. Two methods to implement:
| Method | Description |
|---|---|
AnimationPose getCurrentPose(float partialTicks) | Returns the current interpolated pose, or null if no animation is active (IDLE). |
AnimState getAnimState() | Returns the current animation state of the entity. |
AnimatedEntityRenderer — Renderer
The entity renderer that displays Blockbench models with animations. The rendering pipeline
is identical to AnimatedBlockTESR. Register via
RenderingRegistry.registerEntityRenderingHandler().
| Static method | Description |
|---|---|
factory(String modelId) | Creates an IRenderFactory for the given model (default shadow: 0.5). |
factory(String modelId, float shadowSize) | Factory with custom shadow size. |
EntitySeat — Seat system
Each entity can have one or more passenger seats. Coordinates are in Blockbench pixels (same coordinate space as the model).
| Field | Type | Description |
|---|---|---|
x, y, z | float | Seat position in Blockbench pixels (local to entity). |
yawOffset | float | Passenger yaw rotation in degrees (default: 0). |
attachedGroup | String | Model group name the seat is attached to (may be null). The seat follows the group's animations. |
lockCamera | boolean | If true, the seated player's camera is locked (default: false). |
Full example
// 1. Define the entity via EriEntity builder (in preInit)
EriEntity.create("mymod", "monster")
.model("mymod:entity/monster")
.texture("body", "mymod:textures/entity/monster/body")
.animation("idle", "mymod:entity/monster_idle")
.animation("walk", "mymod:entity/monster_walk")
.hitbox(0.8f, 1.8f)
.seat(0.0f, 24.0f, 0.0f)
.register();
// 2. Entity class — animation state managed via DataParameters
public class EntityMonster extends EntityLiving implements IAnimatedEntity {
// DataParameters for server -> client sync
private static final DataParameter<String> ANIM_NAME =
EntityDataManager.createKey(EntityMonster.class, DataSerializers.STRING);
private static final DataParameter<Integer> ANIM_TICK =
EntityDataManager.createKey(EntityMonster.class, DataSerializers.VARINT);
private final AnimationController controller = new AnimationController();
public EntityMonster(World world) {
super(world);
setSize(0.8f, 1.8f);
}
@Override
protected void entityInit() {
super.entityInit();
dataManager.register(ANIM_NAME, "");
dataManager.register(ANIM_TICK, 0);
}
// Call server-side to start an animation
public void playAnimation(String name) {
dataManager.set(ANIM_NAME, name);
dataManager.set(ANIM_TICK, 0);
}
public void stopAnimation() {
dataManager.set(ANIM_NAME, "");
}
@Override
public void onUpdate() {
super.onUpdate();
if (!world.isRemote) {
String name = dataManager.get(ANIM_NAME);
if (!name.isEmpty()) {
dataManager.set(ANIM_TICK, dataManager.get(ANIM_TICK) + 1);
}
}
}
// --- IAnimatedEntity ---
@Override
public AnimationPose getCurrentPose(float partialTicks) {
String animName = dataManager.get(ANIM_NAME);
if (animName.isEmpty()) return null;
// Load animation file from cache
AnimationFile animFile = EriAnimCache.get("mymod:entity/monster_" + animName);
if (animFile == null) return null;
AnimationDef animDef = animFile.getAnimations().values().iterator().next();
if (animDef == null) return null;
float tickFloat = dataManager.get(ANIM_TICK) + partialTicks;
return controller.computePose(
animDef,
AnimState.PLAYING,
tickFloat,
animDef.getDuration(),
10, // rollbackDuration (unused here)
null, // blendPose
1.0f // blendProgress
);
}
@Override
public AnimState getAnimState() {
return dataManager.get(ANIM_NAME).isEmpty() ? AnimState.IDLE : AnimState.PLAYING;
}
}
// 3. Register the renderer (ClientProxy.preInit)
RenderingRegistry.registerEntityRenderingHandler(
EntityMonster.class,
AnimatedEntityRenderer.factory("mymod:entity/monster")
);
// 4. Register with Forge (RegistryEvent.Register<EntityEntry>)
for (EntityDefinition def : ContentRegistry.getEntityDefs()) {
// Register EntityEntry with EntityRegistry.registerModEntity(...)
// Attach AnimatedEntityRenderer via RenderingRegistry (client only)
}
Entity animations — all approaches v1.6.5
Since v1.6.5, EriEntity supports two kinds of animations on the builder:
.animation(name, animId) for .erianim JSON files (original approach)
and .javaAnimation(name, anim) for Java-coded animations. Both coexist and
can be mixed on the same entity.
// Requires .erianim.json files in assets/
EriEntity.create("mymod", "monster")
.model("mymod:entity/monster")
.texture("body", "mymod:textures/entity/monster")
.hitbox(0.8f, 1.8f)
.animation("idle", "mymod:entity/monster_idle") // ← .erianim file
.animation("walk", "mymod:entity/monster_walk") // ← .erianim file
.register();
// Java anims declared directly on the builder (since v1.6.5)
EriEntity.create("mymod", "monster")
.model("mymod:entity/monster")
.texture("body", "mymod:textures/entity/monster")
.hitbox(0.8f, 1.8f)
.javaAnimation("idle", new IdleAnim())
.javaAnimation("walk", new WalkAnim())
.register();
// Mix: .erianim for long/complex animations + Java for procedural effects
EriEntity.create("mymod", "monster")
.model("mymod:entity/monster")
.animation("walk", "mymod:entity/monster_walk") // .erianim
.javaAnimation("headBob", new HeadBobAnim()) // Java
.register();
import fr.eri.eriapi.anim.AnimationController;
import fr.eri.eriapi.anim.AnimationPose;
import fr.eri.eriapi.anim.AnimState;
import fr.eri.eriapi.anim.EndBehavior;
import fr.eri.eriapi.anim.EriAnimJava;
import fr.eri.eriapi.anim.IAnimatedEntity;
import fr.eri.eriapi.content.ContentRegistry;
import fr.eri.eriapi.content.EntityDefinition;
public class EntityMonster extends EntityLiving implements IAnimatedEntity {
private final AnimationController controller = new AnimationController();
private EriAnimJava currentAnim;
private float animTick = 0;
public EntityMonster(World world) {
super(world);
setSize(0.8f, 1.8f);
// Anims retrieved from the EntityDefinition created by EriEntity
EntityDefinition def = ContentRegistry.getEntityDef("mymod", "monster");
this.currentAnim = def != null ? def.getJavaAnimation("idle") : null;
}
@Override
public void onUpdate() {
super.onUpdate();
if (currentAnim != null) {
animTick += 1.0f;
float dur = currentAnim.duration();
if (currentAnim.endBehavior() == EndBehavior.LOOP && animTick >= dur) {
animTick -= dur; // loop
}
}
}
/** Switch animation by name (resolved from the EntityDefinition). */
public void setAnimation(String name) {
EntityDefinition def = ContentRegistry.getEntityDef("mymod", "monster");
EriAnimJava anim = def != null ? def.getJavaAnimation(name) : null;
if (anim != null) {
this.currentAnim = anim;
this.animTick = 0;
}
}
// --- IAnimatedEntity ---
@Override
public AnimationPose getCurrentPose(float partialTicks) {
if (currentAnim == null) return null;
long worldTick = world != null ? world.getTotalWorldTime() : 0;
return controller.computeJavaPose(
currentAnim, // current EriAnimJava
AnimState.PLAYING,
animTick + partialTicks, // interpolated tickFloat
partialTicks,
worldTick,
this, // target accessible via ctx.asEntity()
null, // blendPose
1.0f // blendProgress
);
}
@Override
public AnimState getAnimState() {
return currentAnim != null ? AnimState.PLAYING : AnimState.IDLE;
}
}
.javaAnimation() instead of .animation()?
.animation() maps a name to a .erianim file (resource pack, disk
loading). .javaAnimation() stores an EriAnimJava instance directly
in the EntityDefinition, retrievable via
ContentRegistry.getEntityDef(modId, name).getJavaAnimation("idle") —
no need to instantiate anims inside the entity class.
currentAnim and
animTick are not synchronized server→client. For an entity visible to
other players, you must expose an animation identifier (for example an int)
through a DataParameter and rebuild currentAnim client-side
accordingly. See the DataParameters pattern in the full example above.
FREEZE,
WAIT_SERVER, LOOP, LOOP_COUNT and
LOOP_PAUSE. ROLLBACK is treated as an instant reset. The example
above handles looping manually inside onUpdate() so it can use a local tick
without any network sync.
Unlike animated blocks which have a server-side AnimatedBlockTileEntity, entities
manage their animation state manually via Forge DataParameters. Only the animation
name and current tick are synchronized server→client. Pose calculation
(AnimationController) is purely client-side, executed each frame.
Use ContentRegistry.getEntityDefs() in your
RegistryEvent.Register<EntityEntry> handler to retrieve all entity
definitions registered via EriEntity. Each EntityDefinition contains
the modId, registryName, modelId, textures, animations, hitbox and seats.
Forge Events
The animation module publishes 4 Forge events on the MinecraftForge.EVENT_BUS.
They are all cancelable (@Cancelable) and fired server-side only.
You can listen to them with the EriAPI event system or with @SubscribeEvent.
Fired before an animation starts. If canceled, the animation is not played.
-
GetterWorld getWorld()The world where the block is located.
-
GetterBlockPos getPos()The block's position.
-
GetterAnimatedBlockTileEntity getTileEntity()The animated block's TileEntity.
-
GetterString getAnimName()The name of the animation that is about to play.
-
GetterEndBehavior getEndBehavior()The effective EndBehavior (override or file default).
Fired when an animation reaches its last tick, before the EndBehavior is applied. If canceled, the animation is frozen on the last frame (FROZEN).
-
GetterWorld getWorld()The world where the block is located.
-
GetterBlockPos getPos()The block's position.
-
GetterAnimatedBlockTileEntity getTileEntity()The animated block's TileEntity.
-
GetterString getAnimName()The name of the animation that is ending.
-
GetterEndBehavior getEndBehavior()The EndBehavior that is about to be applied.
Fired before an animation is reset. If canceled, the reset is not applied.
-
GetterWorld getWorld()The world where the block is located.
-
GetterBlockPos getPos()The block's position.
-
GetterAnimatedBlockTileEntity getTileEntity()The animated block's TileEntity.
-
GetterString getPreviousAnim()The name of the animation that was in progress before the reset.
-
Getterboolean isInstant()
trueif the reset is instant,falseif a rollback is used.
Fired when a looping animation is about to restart (LOOP, LOOP_COUNT, LOOP_PAUSE). If canceled, the animation stops looping and freezes (FROZEN).
-
GetterWorld getWorld()The world where the block is located.
-
GetterBlockPos getPos()The block's position.
-
GetterAnimatedBlockTileEntity getTileEntity()The animated block's TileEntity.
-
GetterString getAnimName()The name of the looping animation.
-
Getterint getCurrentLoop()Current iteration number (starts at 0). The first time the animation ends = 0.
-
Getterint getMaxLoops()Maximum number of loops for
LOOP_COUNT, or-1for infinite loops (LOOPandLOOP_PAUSE).
Listener examples
import fr.eri.eriapi.anim.event.*;
import fr.eri.eriapi.event.EriEvents;
// Prevent certain animations from playing
EriEvents.on(AnimationStartEvent.class, e -> {
if ("forbidden_anim".equals(e.getAnimName())) {
e.setCanceled(true); // The animation will not play
}
});
// React when an animation finishes
EriEvents.on(AnimationEndEvent.class, e -> {
if ("charge".equals(e.getAnimName())) {
// Give a buff to the nearest player
giveBuffToNearestPlayer(e.getWorld(), e.getPos());
}
});
// Stop a loop after a condition
EriEvents.on(AnimationLoopEvent.class, e -> {
if ("production".equals(e.getAnimName())) {
TileMachine te = (TileMachine) e.getTileEntity();
if (!te.hasFuel()) {
e.setCanceled(true); // Stops the loop, freezes the block
}
}
});
// Prevent a reset
EriEvents.on(AnimationResetEvent.class, e -> {
if (isBlockLocked(e.getPos())) {
e.setCanceled(true); // The reset is not applied
}
});
Dynamic Textures
You can change the textures of an animated block in real-time, in two ways:
- Via the .erianim.json file — with the
texturetrack, textures change automatically during the animation. - Via the Java API — with
te.setTexture(), you change textures from server code at any time.
Texture priority
When the TESR looks for which texture to display, it checks in this order:
- Animation override (texture track from .erianim.json) — highest priority.
- TileEntity override (
te.setTexture()) — medium priority. - Original model texture — lowest priority (default).
AnimatedBlockTileEntity te = (AnimatedBlockTileEntity) world.getTileEntity(pos);
// Change a specific texture
te.setTexture("screen", "screen_active");
// The model's "screen" texture will be replaced by "screen_active"
// The change is automatically synchronized to all clients
// Restore the original texture
te.clearTexture("screen");
// Restore ALL original textures
te.clearAllTextures();
// Check current overrides
Map<String, String> overrides = te.getTextureOverrides();
// overrides = {"screen" -> "screen_active", ...}
public class TileReactor extends AnimatedBlockTileEntity {
public TileReactor() {
super("mymod:block/reactor", "mymod:block/reactor");
}
@Override
protected void onAnimationCallback(String callbackName) {
switch (callbackName) {
case "activate_core":
// The animation reached the point where the core should light up
setTexture("core", "core_glowing");
setTexture("panel", "panel_active");
break;
case "deactivate_core":
clearAllTextures();
break;
}
}
}
Debug Commands
EriAPI automatically registers the /erianim command for testing and debugging animations in-game.
All subcommands require OP level 2, except reload which requires OP level 3.
| Command | Description |
|---|---|
/erianim play <x> <y> <z> <anim> [endBehavior] |
Plays an animation on the animated block at the given position. The EndBehavior is optional (uses the .erianim file's default). |
/erianim reset <x> <y> <z> [true|false] |
Resets the animation. true = instant, false (default) = uses rollback if applicable. |
/erianim stop <x> <y> <z> |
Immediately stops the animation (no rollback, no callback). |
/erianim list <x> <y> <z> |
Lists all available animations in the block's .erianim file, with their duration, EndBehavior, number of tracks and events. |
/erianim state <x> <y> <z> |
Displays the block's full state: model, anim file, state, current animation, progress, texture overrides. |
/erianim perf |
Enables performance tracking and displays stats: number of blocks rendered, average render time, cache sizes. Run a second time to view data. |
/erianim reload |
(OP 3) Clears model and animation caches. Files will be reloaded on the next render. Ideal for testing modifications without restarting the server. |
File Structure
Here is where to place each file in your mod for the animation system to work correctly.
src/main/resources/assets/mymod/
models/
block/
my_machine.json ← Blockbench model (export "Java Block/Item")
animations/
block/
my_machine.erianim.json ← EriAPI animation file
textures/
blocks/
my_machine/ ← Model texture folder
base.png ← Main texture
panel.png ← Panel texture
screen_off.png ← Screen off
screen_on.png ← Screen on (for texture swap)
blockstates/
my_machine.json ← Blockstate (auto-generated by EriAnimBlock)
The modelId "mymod:block/my_machine" resolves to
assets/mymod/models/block/my_machine.json.
The animFileId "mymod:block/my_machine" resolves to
assets/mymod/animations/block/my_machine.erianim.json.
Textures are resolved via the texture map in the Blockbench model ("textures" section of the JSON).
Performance
The animation system is designed to support 500 to 1000 concurrent players. Here are the built-in optimizations and best practices to maintain good performance.
Built-in optimizations
- Fast exit in update() — The
ITickable.update()method returns immediately for IDLE, FROZEN, PAUSED, and WAITING_SERVER states. Only blocks in PLAYING or ROLLING_BACK state consume server CPU time. - IDLE render cache — When the block is IDLE and nothing has changed, the TESR skips pose calculation and reuses the last render. A
idleRenderDirtyflag controls re-evaluation. - Model and animation caches — Blockbench models and .erianim files are parsed once then cached (
AnimModelCacheandEriAnimCache). Reload is available via/erianim reload. - Sorted events — Animation events are sorted by tick, allowing O(n) dispatch with early exit when the tick exceeds the current tick.
Best practices
- Reduce renderDistance — For decorative or non-critical blocks, use a renderDistance of 32 or 48 instead of 64.
- Avoid unnecessary infinite loops — A block in
LOOPconsumes server CPU every tick to dispatch events. If you have no events, prefer an IDLE animation without events using a spin track. - Use WAIT_SERVER for long processes — Instead of looping, play an animation once then wait for the server signal. This avoids continuous ticking.
- Fewer groups = fewer calculations — Each animated group requires transformation calculations. Merge groups that move together in Blockbench.
- Monitor with /erianim perf — Use the in-game command to check render time and identify problematic blocks.
EriAnim Editor
The EriAnim editor is a desktop tool (Electron application) that allows you to create
and visually edit .erianim.json files without writing JSON by hand.
Features
- Visual timeline — Displays tracks and keyframes on an interactive timeline. Drag and drop to move keyframes.
- 3D Preview — Preview the animation in real-time with the Blockbench model loaded in the editor.
- Keyframe editor — Form to edit values, easing, and tick of each keyframe.
- Direct export — Generates the
.erianim.jsonfile ready to be integrated into your mod.
The EriAnim editor is an internal tool of EriniumGroup. If you are working on a mod that uses EriAPI,
contact the team to get access. In the meantime, you can always create .erianim.json files
by hand following the documentation on this page.