Building
Animations
Vanilla Minecraft has no concept of an animated block model. A Resource Pack can change what a block looks like, not make it move. So animations here are two halves that meet in the middle: you keyframe a model in Animate mode, and our plugin plays those keyframes back on placed models, on your server.
On Java, animations need the plugin
A Java pack with an animated model is still a perfectly valid Resource Pack — the animation data lives in fields vanilla ignores — but the model sits still anywhere our plugin isn't running. Bedrock is different: it has native keyframe animation, so the converted pack carries the real thing. More on that below.
Animating in the editor#
- 1
Group what moves into bones
Animate mode works on cubes and on bones (named groups of cubes with a shared pivot, made in Edit mode). A hinge, a wheel, a wing — each wants to be a bone, because rotating a bone rotates its cubes around one pivot rather than each around its own. A generated model comes pre-grouped — named bones, pivots at the joints — so there this step is usually already done.
- 2
Keyframe it
Pick a target, scrub the timeline, change the rotation, position or scale, and a keyframe is set at that time. An animation has a name and a length in seconds; keyframes past the end are dropped when you shorten it.
- 3
Choose interpolation
Per keyframe:
- Linear — straight line between values. The default.
- Smooth — a Catmull-Rom curve through the neighbouring keys. Use for anything organic; a segment is smooth if either end is.
- Step — hold the value, then jump. Use for mechanical snaps and for anything that should look like it has no in-between frames.
Values hold flat before the first key and after the last one.
- 4
Pick playback triggers
An animation with no triggers never plays by itself in game — though a plugin can still play it from code. That's the next section.
Animate with AI
You can hand the keyframing over: Animate with AI gives the model's bones keyframed animations from an instruction, and generation itself can include looping and interaction animations when the prompt asks for them. Either way the result is ordinary keyframes on the timeline, editable like your own. An AI animation run merges onto your latest save, so edits you make while it's running survive.
Triggers#
Each animation can claim several triggers, and a model can have several animations. Triggers are what connect a timeline to something that actually happens in the world.
| Trigger | Fires when |
|---|---|
| On loop | Continuously, while the model is placed |
| On placement | Once, immediately after the model is placed |
| On right click | A player uses the model |
| On left click | A player hits the model |
| On range | A player enters a radius around it (default 5 blocks, 0.5–64) |
Rules worth knowing before you build something complicated:
- One animation wins per event. If two animations both claim right-click, only one plays — by default the first in model order. Which one is a choice you make when you give the model out: with two or more triggered animations, the give card grows a Plays dropdown, and the picked animation rides on the item itself. Place it and that copy uses your pick for every trigger the animation claims; anything it doesn't claim falls back to model order as usual. Two copies of the same model can be placed playing different animations.
- One-shots don't stack. While a triggered animation is playing, further triggers of it are ignored rather than restarting or layering it.
- What happens after a one-shot: the model returns to rest, or falls back to the first looping animation if it has one.
- Range is entry-based. Presence is re-checked periodically, so a group arriving together starts it once, and it can fire again after someone leaves and comes back — it doesn't re-trigger every tick while you stand there.
Left click makes a model unbreakable
A left-click trigger has a side effect by design: that placement can no longer be punched to pick it up. Otherwise the first hit would break the rig and drop the item instead of playing the animation. Give players another way to remove it if that matters.
Playing from code#
Triggers cover "when a player does something to this model". For anything else — a quest completing, a boss dying, a timer — a plugin on your server can play an animation by name.
Naming it is all it takes. Unlike a trigger, this doesn't care what the animation claims, so an animation with no triggers at all is playable this way and nothing else can reach it. That's a reasonable way to build: give the animations you drive from code no triggers, and they'll never fire by accident when somebody clicks.
Add softdepend: [ResourcePackAI] to your plugin.yml, and build against the
same jar your server runs (there's no maven repo for it — drop
the jar in libs/ and use
compileOnly files("libs/resourcepackai.jar")).
ModelAnimations models = Bukkit.getServicesManager().load(ModelAnimations.class);
if (models == null) return; // plugin isn't installed
for (ModelPlacement statue : models.placementsNear(altar, 8)) {
statue.play("Wave");
}You get a ModelPlacement from a location (placementsNear) or straight from
an entity (placementOf) — pass it the entity out of a
PlayerInteractAtEntityEvent and it resolves the rig that was clicked. From
there:
| Call | What it does |
|---|---|
animations() | Every animation name this model has, in editor order |
play(name) | Plays it. false if the name is unknown, the model has no moving parts, or that animation is already mid-play |
play(name, true) | Same, but restarts it if it's already running |
stop() | Back to the idle loop if there is one, otherwise the rest pose |
playing() | What's on screen now, including an idle loop it fell back to itself |
Two things worth knowing before you wire it up. Call it on the main thread
— it moves entities, so an async call throws rather than quietly corrupting
something. And a rig in an unloaded chunk isn't there: placementsNear
only finds what's loaded, which is also the only thing anyone could be looking
at.
Reacting to a player clicking a model#
isModel answers "is this entity part of a placed model" without building a
handle you're about to discard:
@EventHandler(priority = EventPriority.LOWEST)
public void onUse(PlayerInteractAtEntityEvent event) {
if (event.getHand() != EquipmentSlot.HAND) return;
if (!models.isModel(event.getRightClicked())) return;
ModelPlacement rig = models.placementOf(event.getRightClicked()).orElseThrow();
rig.play("Open");
}Listen at LOWEST, or your handler works intermittently
Our plugin handles that same event at NORMAL and cancels it when the model has a right-click trigger. At NORMAL or later you'd see a cancelled event for models that have one and a live event for models that don't — which reads as your code working on some models and not others. Running first means always seeing the click as the player sent it.
Left click is EntityDamageByEntityEvent, and that one is cancelled for
every rig, so punching can't break it. Same rule, no exceptions.
The hand check matters too: an interaction fires once per hand, so without it everything in your handler happens twice.
Surviving an older plugin version#
A null check isn't enough on its own. If the server runs a jar from before
this API existed, merely resolving ModelAnimations throws
NoClassDefFoundError — before your null check runs, and it takes your whole
onEnable with it. Keep every reference to our types in one class you only
touch after checking:
if (Bukkit.getPluginManager().getPlugin("ResourcePackAI") != null
&& plugin.getDescription().getVersion().compareTo("1.1.1") >= 0) {
this.rigs = new MyRigHook(this); // the only class naming our types
}The API arrived in plugin version 1.1.0 and isModel in 1.1.1, which is
what everything on this page needs. The jar also ships a
-sources.jar beside it, so put that in libs/ too and your IDE shows the
documentation for every call above instead of decompiled bytecode.
Bedrock players see it too
A code-driven animation is the same animation as a triggered one in every way that matters — including the packet that tells a Bedrock viewer's client to play the frames natively. Nothing extra to do.
How playback actually works#
Useful to know when something looks wrong in game:
- When a pack is synced to you, an animation rig manifest is sent alongside it. The plugin keeps those rigs keyed by model id and saves them to disk, so placed models keep animating across a server restart.
- A placed animated model isn't one entity. Each moving part of the rig gets its own display entity; the parts that never move are left as one static remainder and skipped by the animator entirely.
- On Java, playback is driven from the server every two ticks — ten updates a second, not sixty. It's smooth for the motion these models do, and deliberately cheap: a build full of animated props shouldn't cost you the server.
- The keyframe maths is the same code in the editor and the plugin, so what you preview is what plays.
On Bedrock#
Bedrock has native keyframe animation, and the exported Bedrock pack carries your keyframes as real Bedrock animations. So on a Geyser server the split is the other way round from Java: the plugin still decides when an animation plays — same triggers, same rules as above — but the client plays the frames. That's smoother than the Java side, which is tweening from the server a few times a second.
A Bedrock player sees a placed model only once that pack has been applied to them, since their client renders it out of their own copy. Someone who hasn't been pushed the pack sees nothing there.
Limits#
Things to expect:
- Animations are per-model. One model's trigger can't start another model's animation — though a plugin holding both can, by playing each from code.
- Placement rotation is remembered per placement, so the same model animates correctly whichever way you faced when you put it down.
- Nothing triggers without the plugin. Someone who just downloads your Java pack sees the model in its rest pose — the triggers are a server-side idea, and that's true on Bedrock too.