# displaymotion docs All pages of the displaymotion docs, plus the roadmap and the compatibility page, in one file. Source: https://github.com/Jan1k1/displaymotion/tree/main/docs # Getting started This page builds a small plugin from zero. At the end, a sword spins and floats above you when you type a command. You need Paper 26.3, Java 25 and the PacketEvents plugin (2.14.0 or newer) on the server. PacketEvents is a separate plugin. The server owner installs it. You do not put it in your jar. ## What is displaymotion? Minecraft has three display entities: item, block and text displays. They show an item, a block or a text in the world. They can move, turn and change size. displaymotion makes this easy. You say what should move, and the library sends the right packets to the players. There are no real entities on the server, so it does not slow the server down. Players need no mod and no resource pack. ## 1. Add the library Make a Gradle project. This is the full `build.gradle.kts`: ```kotlin plugins { java id("com.gradleup.shadow") version "9.6.1" } java { toolchain { languageVersion = JavaLanguageVersion.of(25) } } repositories { mavenCentral() maven("https://repo.papermc.io/repository/maven-public/") maven("https://repo.codemc.io/repository/maven-releases/") maven("https://jitpack.io") } dependencies { compileOnly("io.papermc.paper:paper-api:26.3.build.180-stable") compileOnly("com.github.retrooper:packetevents-spigot:2.14.0") implementation("com.github.Jan1k1.displaymotion:displaymotion-paper:v0.2.0") } tasks.shadowJar { relocate("org.jan1k.displaymotion", "your.plugin.libs.displaymotion") } ``` What the parts do: - `compileOnly` means: use it to build, but do not put it in the jar. Paper and PacketEvents are on the server already. - `implementation` means: put it in the jar. This is displaymotion. - `relocate` moves the displaymotion classes into your own package. Then two plugins that both use displaymotion never clash. Change `your.plugin.libs.displaymotion` to a package that fits your plugin. - `v0.2.0` is the newest release. `main-SNAPSHOT` gives the newest commit instead. Do not shade PacketEvents. The server installs it. ## 2. plugin.yml Make the file `src/main/resources/plugin.yml`: ```yaml name: MyPlugin version: "1.0" main: your.plugin.MyPlugin api-version: "26.3" depend: [packetevents] commands: sword: description: Shows a spinning sword ``` `depend: [packetevents]` is important. It makes the server load PacketEvents first. Without it, `DisplayMotion.create` fails. ## 3. First display Make the file `src/main/java/your/plugin/MyPlugin.java`: ```java package your.plugin; import org.bukkit.Material; import org.bukkit.entity.Player; import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.java.JavaPlugin; import org.jan1k.displaymotion.Animation; import org.jan1k.displaymotion.Animations; import org.jan1k.displaymotion.paper.DisplayMotion; import org.jan1k.displaymotion.paper.MotionDisplay; public final class MyPlugin extends JavaPlugin { private static final Animation SHOW = Animations.popIn(20) .then(Animations.spin(80).with(Animations.bob(0.3f, 80))); private DisplayMotion motion; @Override public void onEnable() { motion = DisplayMotion.create(this); getCommand("sword").setExecutor((sender, command, label, args) -> { if (!(sender instanceof Player player)) { sender.sendMessage("Players only."); return true; } showSword(player); return true; }); } private void showSword(Player player) { MotionDisplay sword = motion.item(player.getLocation().add(0, 1.5, 0), ItemStack.of(Material.DIAMOND_SWORD)); sword.play(SHOW); } } ``` What happens in the code: - `DisplayMotion.create(this)` starts the library. It is your entry point for everything. - `motion.item(location, item)` makes an item display at the location. It returns a `MotionDisplay`. This is a handle to the display. - `SHOW` is an animation. `popIn(20)` grows the item from nothing in 20 ticks (1 second). Then it spins, one turn every 80 ticks, and moves up and down at the same time. `then` means "after that". `with` means "at the same time". - `sword.play(SHOW)` starts the animation. That is all. The library removes every display when your plugin disables. ## 4. Build and run 1. Run `./gradlew shadowJar`. The jar is in `build/libs/`. 2. Copy the jar to the `plugins` folder of a Paper 26.3 server. Put the PacketEvents jar there too. 3. Start the server and join. 4. Type `/sword`. You should see a diamond sword grow out of nothing, about one and a half blocks above your feet. Then it spins and floats up and down. It stays until you remove it or the plugin stops. To remove it, call `sword.remove()`. If you walk more than 48 blocks away, it hides, and it comes back when you return. If something is wrong: | What you see | Fix | | --- | --- | | `PacketEvents is not running` in the log | Install PacketEvents on the server. Check that `depend: [packetevents]` is in `plugin.yml`. | | `Unknown command` | The command is not in `plugin.yml`, or the plugin did not load. Read the log. | | The sword does not show | You are more than 48 blocks away, or your client is older than 1.19.4. | ## Rules to remember - Call `DisplayMotion.create` once, on the main thread, in `onEnable`. - Keep animations in `static final` fields. Each animation object is baked once. - Call `play` right after spawning, in the same tick. Then the first frame is already the animation. - A display never moves by itself. Use `teleport` to move it, and animations to move parts. ## Where to go next - [Animations](https://displaymotion.peakstone.app/docs/animations/): make your own movements. - [Models](https://displaymotion.peakstone.app/docs/models/): build things from many blocks, items and texts. - [Effects](https://displaymotion.peakstone.app/docs/effects/): kill effects in one line. - [Importing](https://displaymotion.peakstone.app/docs/importing/): draw a model in Blockbench or BDEngine and load it. - [Cookbook](https://displaymotion.peakstone.app/docs/cookbook/): short recipes to copy. # Animations An `Animation` describes how a display changes over time: where it moves, how it turns and how big it is. You build one, then play it on a display. Time in Minecraft is counted in **ticks**. 20 ticks are one second. So `popIn(20)` takes one second, and `popIn(10)` takes half a second. An animation moves a display away from where you placed it. `Pose.IDENTITY` means "stay where you are". ## Presets Presets are ready animations for the most common things. Start here. | Call | Result | Use it for | | --- | --- | --- | | `Animations.popIn(ticks)` | Scale 0 to 1, fast at first. | Showing something. | | `Animations.popOut(ticks)` | Scale 1 to 0, fast at the end. | Hiding or removing something. | | `Animations.spin(ticksPerTurn)` | Turn around Y forever. At least 6 ticks per turn. | Items that float and turn. | | `Animations.turn(pitch, yaw, roll, ticks)` | Turn by these degrees once. | Opening a lid, turning a wheel. | | `Animations.bob(height, ticks)` | Up and down forever. | Floating. | | `Animations.slide(x, y, z, ticks, ease)` | Move by an offset once. | Moving a part. | | `Animations.hold(ticks)` | Stay still. | A pause inside `then`. | ## Combine You make long animations by joining short ones. | Call | Result | | --- | --- | | `a.then(b)` | `a`, then `b`. `b` starts where `a` ended. | | `a.with(b)` | Both at once. Offsets and angles add, scales multiply. | | `a.loop()` | Repeat forever. | ```java Animation idle = Animations.spin(80).with(Animations.bob(0.3f, 80)); Animation show = Animations.popIn(20).then(idle); Animation hop = Animations.slide(0f, 1f, 0f, 6, Ease.OUT) .then(Animations.slide(0f, -1f, 0f, 6, Ease.IN)) .loop(); ``` Read the code above like this: - `idle` turns and floats at the same time, for ever. - `show` grows the display in, then goes on with `idle`. - `hop` jumps up one block, falls back down, and does this again and again. `spin` and `bob` already loop. `slide`, `turn`, `popIn` and `popOut` play once. Add `.loop()` if you want them to repeat. ## Easing Easing is how a movement feels. Without it, everything moves at one speed and looks like a robot. | Ease | Feel | Example | | --- | --- | --- | | `LINEAR` | Same speed all the time. | A wheel that turns. | | `OUT` | Fast at the start, slow at the end. | Something that pops in. | | `IN` | Slow at the start, fast at the end. | Something that falls. | | `IN_OUT` | Slow, fast, slow. | Floating up and down. | ## Your own keyframes A keyframe says: at this tick, the display must have this pose. The library fills the space between keyframes for you. ```java Animation wobble = Animation.keyframes() .at(0, Pose.IDENTITY) .at(5, Pose.IDENTITY.withRotation(0f, 0f, 15f), Ease.OUT) .at(10, Pose.IDENTITY.withRotation(0f, 0f, -15f), Ease.IN_OUT) .at(15, Pose.IDENTITY, Ease.IN) .build() .loop(); ``` - Ticks must go up. - The ease of a keyframe is how the blend *into* it feels. So the ease at tick 5 above is how it feels to go from tick 0 to tick 5. - Before the first keyframe the display holds the first pose. After the last keyframe it holds the last pose. ## Pose A pose is three things: an offset, a rotation and a scale. ```java Pose.IDENTITY.withOffset(x, y, z).withRotation(pitch, yaw, roll).withScale(s) ``` - **Offset** moves it, in blocks. - **Rotation** turns it, in degrees. The order is yaw, then pitch, then roll. - **Scale** changes the size. `withScale(2f)` is twice as big. `withScale(1f, 2f, 1f)` is twice as tall. A negative scale mirrors it. A pose never changes. Every `with...` call returns a new pose. ## Playing ```java display.play(show); // whole display, replaces what plays now display.play(Animations.popOut(10), display::remove); // runs remove on the main thread at the end display.play("hub", Animations.turn(0f, 0f, 360f, 60).loop()); // one part of a model display.stop(); ``` - `play(animation)` plays on the whole display. If something plays already, it is replaced. - `play(animation, onEnd)` also runs code when the animation ends. This is how you remove a display after it shrinks. - `play("part", animation)` plays on one part of a [model](https://displaymotion.peakstone.app/docs/models/). The children of that part follow it. - `stop()` freezes the display where it is. - `isPlaying()` tells you if an animation plays on the whole display. ## Events An event is a name at one tick of an animation. When the display reaches that tick, your code runs. Use it to play a sound or spawn particles at the exact moment something happens. ```java static final Animation PRESS = Animation.keyframes() .at(0, Pose.IDENTITY) .at(2, Pose.IDENTITY.withOffset(0f, -0.08f, 0f)) .at(6, Pose.IDENTITY) .build() .event(2, "click"); // tick 2, when the button is all the way down button.onEvent(event -> { if (event.name().equals("click")) { location.getWorld().playSound(location, Sound.BLOCK_STONE_BUTTON_CLICK_ON, 1f, 1f); } }); button.play("cap", PRESS); ``` - `event(tick, name)` works on any animation, also on presets: `Animations.spin(40).event(20, "half")`. - The tick counts from the start of that animation. After `then`, the ticks of the second part move back by the length of the first part, so the event stays at the same moment. - In a loop the event comes again every round. - `onEvent` runs on the main thread, so the Bukkit API is safe there. `event.part()` is the part the animation plays on, or `null` for the whole display. - One handler per display. A new `onEvent` replaces the old one. - Events land on the frame that shows their tick. A frame is 2 ticks, so an event can come up to 1 tick early or late. ## Timing - A frame goes out every 2 ticks. The player's game blends between frames. Lengths are rounded to whole frames. - An animation can be at most 6000 ticks (intro plus one loop). - A new `play` blends from the current pose to the first frame over 2 ticks. So there is no jump. - An endless animation never calls `onEnd`. Anything after it in `then` never plays. - `spin` needs at least 6 ticks per turn. With less, the player's game cannot tell which way it turns. - Keep animations in `static final` fields. Each animation object is baked once, so one object can be used for many displays. # Models A model is many displays that move as one. Use a model when one block or item is not enough: a lamp, a windmill, a pet with a body and a head. Parts can be blocks, items or texts. Every part has a **name**. You use the name later to move or change that part. If you draw your model in an editor, read [Importing](https://displaymotion.peakstone.app/docs/importing/) instead. This page is for building models in code. ## Your first model ```java MotionDisplay lamp = motion.model(location) .block("post", Material.DARK_OAK_FENCE.createBlockData(), Pose.IDENTITY.withOffset(0f, 1f, 0f).withScale(1f, 2f, 1f)) .item("light", ItemStack.of(Material.LANTERN), Pose.IDENTITY.withOffset(0f, 1.3f, 0f), "post") .text("sign", Component.text("Shop"), Pose.IDENTITY.withOffset(0f, 0.5f, 0.2f).withScale(0.5f), "post") .spawn() .play(Animations.popIn(20)); ``` How to read it: - `motion.model(location)` starts a model. The location is the point where it stands. - `.block("post", ...)` adds a block part named `post`. Its offset is `(0, 1, 0)` and its scale is `(1, 2, 1)`. So it is a post that is one block wide and two blocks tall, and it stands on the location. - `.item("light", ...)` adds a lantern. The last text `"post"` is the **parent**. The lantern moves with the post. - `.text("sign", ...)` adds a text part, also under the post. - `.spawn()` makes the model. It returns a `MotionDisplay`. - `.play(...)` plays an animation on the whole model. For a block part, the **offset is where its center goes** and the **scale is its size in blocks**. A block with scale `(1, 2, 1)` is a post two blocks tall. To make it stand on the floor, put its center one block up. ## Rules - A parent must be added before its children. - Names must be different. Two parts with the same name give an error. - Offset and rotation of a part pass to its children. - Scale of a part is its own size. Children do not get it. - Animations pass everything to the children, scale too. - Blocks turn and scale around their center. Items around their model center. Text grows up from its pose. - The model faces the yaw and pitch of the spawn location. - At most 256 parts. ## Parents A parent is a part that other parts follow. When the parent moves or turns, the children go with it. This is how you make arms, doors and wheels. ```java windmill.play(Animations.popIn(25)); // the whole model windmill.play("axle", Animations.turn(0f, 0f, -360f, 120).loop()); // the axle and everything on it ``` Tip: when many parts turn around one point, put them under one parent and turn the parent. This is easier than turning every part. ## Groups A group is an invisible pivot. Nothing is shown for it. It only holds children, and you can turn or move it. Use a group when the point you want to turn around is not a block. A door turns around its hinge. A lid turns around its back edge. ```java motion.model(location) .group("hinge", Pose.IDENTITY.withOffset(0f, 0.5f, -0.4f)) .block("lid", Material.DARK_OAK_PLANKS.createBlockData(), Pose.IDENTITY.withOffset(0f, 0f, 0.4f).withScale(1f, 0.2f, 0.8f), "hinge") .spawn() .play("hinge", Animations.turn(-70f, 0f, 0f, 10)); // the lid opens around the hinge ``` The hinge is the pivot. The lid sits 0.4 blocks away from the hinge. When the hinge turns, the lid swings up like a real lid. ## Matrix parts If you already have a 4x4 matrix (from `/summon`, another tool, or your own math), give it row by row: ```java float[] matrix = {0.5f, 0, 0, -0.25f, 0, 0.5f, 0, 0, 0, 0, 0.5f, -0.25f, 0, 0, 0, 1}; model.block("cube", Material.STONE.createBlockData(), matrix, null); ``` Matrix parts work exactly like vanilla: blocks turn around their corner and the whole matrix, scale too, passes to the children. There are matrix versions of `block`, `item`, `text` and `group`. The importers use them. ## Looks per part These calls change how one part looks. Leave the part name out to change every part. ```java lamp.glow("light", Color.ORANGE); lamp.brightness("sign", new Display.Brightness(15, 15)); lamp.billboard("sign", Display.Billboard.CENTER); lamp.text("sign", Component.text("Open")); lamp.background("sign", Color.fromARGB(0, 0, 0, 0)); // no dark box behind the text ``` - `glow` draws a colored outline. Use `null` to turn it off. Text parts do not glow. - `brightness` sets a fixed light level (block light, sky light). The part is bright even in a cave. - `billboard` makes the part turn to the player. `CENTER` turns in every direction. - `text` changes the text of a text part while it is shown. - `background` changes the box behind a text. Alpha 0 means no box. ## Clicks A display has no hitbox, so players click through it. Give it one with `hitbox`, then listen with `onClick`: ```java MotionDisplay chest = model.spawn(motion, location) .hitbox(1f, 1f); // 1 block wide, 1 block high chest.onClick(click -> { if (click.rightClick()) { model.play(chest, "open"); } }); ``` - The box is a vanilla Interaction entity, sent with packets like the display. Players need no mod. - `hitbox(width, height)` puts the bottom center of the box at the display location. Its name is `main`. - For more boxes, give each one a name and an offset: `hitbox("lid", 0.8f, 0.3f, 0, 0.6, 0)`. The offset turns with the display. - `onClick` runs on the main thread, so you can use the Bukkit API there. `click.player()` is who clicked, `click.hitbox()` is the name of the box, `click.leftClick()` is true for a left click. - The box stays upright and does not follow animations. It moves with `teleport`. - Only players who see the display can click it. Clicks from too far away are ignored. ## Find the part names `display.parts()` gives the names of all parts, in the order you added them. ## Example `example/src/main/java/org/jan1k/displaymotion/example/ExamplePlugin.java` builds a 53 part windmill. Run it with `/dm windmill`. To build models in an editor instead, see [Importing](https://displaymotion.peakstone.app/docs/importing/). # Effects Effects are short, ready-made animations. You call one line, the effect plays, and it removes itself when it is done. You do not need to build or clean up anything. They are made for moments like a kill, a death, a win or a drop. ```java motion.effects().shatter(location, Material.REDSTONE_BLOCK.createBlockData()); motion.effects().tombstone(location, Component.text("Steve")); motion.effects().swordRain(location, ItemStack.of(Material.IRON_SWORD)); motion.effects().soul(location); ``` The location is the ground point where the effect happens. For a mob that died, use the location of the mob. | Effect | What you see | Time | | --- | --- | --- | | `shatter(location, block)` | The block breaks into 10 small pieces. They fly out, spin, fall and shrink. | about 2 seconds | | `tombstone(location, name)` | A gravestone with "RIP" and the name rises out of the ground, stays 3 seconds, and sinks back. | about 4.5 seconds | | `swordRain(location, sword)` | 6 swords fall from the sky one after another, stick in the ground, then vanish. | about 3.5 seconds | | `soul(location)` | A glowing skull rises, spins and fades away. | about 2.5 seconds | What you give to each effect: - `shatter` takes the block to break, for example `Material.REDSTONE_BLOCK.createBlockData()`. Pick the block of the thing that died, or a color you like. - `tombstone` takes a `Component` for the name. `Component.text("Steve")` works. For a mob, use `event.getEntity().name()`. - `swordRain` takes any `ItemStack`. It does not have to be a sword. - `soul` takes nothing else. Each call returns the `MotionDisplay`, so you can still `hide(player)` or `remove()` it before it ends. ## As a kill effect ```java @EventHandler public void onKill(EntityDeathEvent event) { Player killer = event.getEntity().getKiller(); if (killer == null) { return; } Location at = event.getEntity().getLocation(); // face the killer so the tombstone text is readable at.setDirection(killer.getLocation().toVector().subtract(at.toVector())); at.setPitch(0f); motion.effects().tombstone(at, event.getEntity().name()); } ``` Remember to register the listener with `getServer().getPluginManager().registerEvents(this, this)`. The front of an effect is the direction of the location's yaw. The text of the tombstone is on the front. Turn the location to the player to make the text readable. For `shatter`, `swordRain` and `soul` the direction does not matter. ## Who sees it Every player within 48 blocks of the location sees the effect. If you want only one player to see it, call `hide(player)` for the others, or use [`visibleByDefault(false)` and `show(player)`](https://displaymotion.peakstone.app/docs/cookbook/). ## Try them In the example plugin: `/dm effect shatter|tombstone|swords|soul`, or `/dm killeffect tombstone` and kill a mob. ## Make your own An effect is only a model with an animation and `display::remove` at the end. The source of [`Effects.java`](https://github.com/Jan1k1/displaymotion/blob/main/paper/src/main/java/org/jan1k/displaymotion/packetevents/Effects.java) is a good place to learn. See [Models](https://displaymotion.peakstone.app/docs/models/) and [Animations](https://displaymotion.peakstone.app/docs/animations/). # Importing models You can build a model in code, but a robot with 100 parts is a lot of numbers. It is easier to draw it in an editor and load the file. displaymotion reads files from two free editors: | Editor | File | What comes in | | --- | --- | --- | | [Blockbench](https://www.blockbench.net) | `.bbmodel` | Cubes, groups, rotations and animations. Every cube becomes the vanilla block that is closest to its color. | | [BDEngine](https://block-display.com) | `.bdengine` | Blocks, items, player heads, texts, groups and animations. They look like they do in the editor. | Which one should you use? - Pick **BDEngine** if you want real vanilla blocks and items, with their real textures. - Pick **Blockbench** if you want to draw your own shapes and use its animation timeline. The result is made of solid color blocks. ## Quick start 1. Make a model, or download one. 2. Put the file in your plugin folder. 3. Load it once with `ImportedModel.load`. 4. Spawn it with `spawn`. 5. Play an animation from the file with `play`. The next parts show every step. ## 1. Get a model ### Blockbench 1. Open Blockbench. It runs in the browser and as a desktop app. 2. Make a new project of the type **Free Model**. Any type that has animations works. 3. Build the model from cubes. Put cubes that move together in a **group** and give the group a name, for example `head` or `arm_left`. 4. Paint the cubes. Use flat, clear colors. See [Colors](#colors-blockbench) below. 5. Open the **Animate** tab. Make an animation, for example `walk`, and add keyframes on the groups. 6. Save with **File, Save Project**. The file ends with `.bbmodel`. The textures are saved inside the file. ### BDEngine 1. Open [block-display.com](https://block-display.com) and make a model from blocks, items, heads and texts. Or download one from the gallery. 2. Make animations in the editor and give each one a name, for example `open`. 3. Save the project. The file ends with `.bdengine`. Models in the BDEngine gallery have a license. Many are BDL BY-NC: free for non-commercial use, and you must credit the author. Read it before you use one on a paid server. ## 2. Put the file in your plugin The plugin reads the file from its data folder: `plugins/YourPlugin/models/`. **Option A: ship the file inside your jar.** Put it in `src/main/resources/models/robot.bbmodel`. Then copy it out when the plugin starts: ```java saveResource("models/robot.bbmodel", false); ``` - `false` means: do not overwrite. The server owner can edit the file and the plugin keeps it. - `true` means: always write the file from the jar again. **Option B: copy the file by hand.** Make the folder `plugins/YourPlugin/models/` and drop the file in it. Then the server owner can add their own models without a new jar. ## 3. Load and spawn `ImportedModel.load` reads the file. It picks the format from the file name. It can throw `IOException`, so catch it. Load a model once, for example in `onEnable`. Then spawn it as many times as you want. Loading is safe on any thread. ```java import org.bukkit.Location; import org.bukkit.command.Command; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; import org.bukkit.plugin.java.JavaPlugin; import org.jan1k.displaymotion.Animations; import org.jan1k.displaymotion.paper.DisplayMotion; import org.jan1k.displaymotion.paper.ImportedModel; import org.jan1k.displaymotion.paper.MotionDisplay; import java.io.IOException; import java.nio.file.Path; public final class MyPlugin extends JavaPlugin { private DisplayMotion motion; private ImportedModel robot; @Override public void onEnable() { motion = DisplayMotion.create(this); saveResource("models/robot.bbmodel", false); Path file = getDataFolder().toPath().resolve("models/robot.bbmodel"); try { robot = ImportedModel.load(file); } catch (IOException e) { getLogger().severe("Could not read " + file + ": " + e.getMessage()); getServer().getPluginManager().disablePlugin(this); return; } getLogger().info("Animations: " + robot.animations()); getLogger().info("Parts: " + robot.parts()); } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (!(sender instanceof Player player)) { sender.sendMessage("Players only."); return true; } Location at = player.getLocation(); MotionDisplay display = robot.spawn(motion, at); robot.play(display, "walk"); display.play(Animations.popIn(15)); return true; } } ``` For the command to work, add it to `plugin.yml`: ```yaml commands: robot: description: Spawns the robot ``` Look at the log when the server starts. It prints the animation names and the part names. You need them in the next steps. What the three calls do: - `robot.spawn(motion, at)` makes the model at the location. It returns a `MotionDisplay`. - `robot.play(display, "walk")` starts the animation named `walk` from the file. - `display.play(Animations.popIn(15))` grows the whole model in. This is optional. It works together with the animation from the file. You can also spawn bigger or smaller: `robot.spawn(motion, at, 0.5f)` is half size. To remove the model, call `display.remove()`. To change the animation, call `robot.play(display, "wave")` again. ## How animations work - The name you give to `play` is the name of the animation in the editor. Capital letters matter. If the name is wrong, you get an error that lists all names. - `robot.animations()` gives you the list of names. - A new `play` replaces the old animation. Parts that the new animation does not move go back to their rest pose. - Blockbench: only **groups** move. A cube that is not inside a group never moves, so put every moving part in a group. - Blockbench: an animation that is set to **loop** loops here too. Any other animation plays once and stays on its last pose. - BDEngine: animations always loop. - Blockbench keyframes are in seconds. They are turned into ticks (20 ticks are one second). BDEngine keyframes are already in ticks. - One animation can be at most 6000 ticks (5 minutes). You can still animate one part with your own code. It replaces the animation from the file for that part: ```java display.play("head", Animations.turn(0f, 360f, 0f, 40).loop()); ``` ## Part names Every part of the model has a name. You need it for `display.play("head", ...)`, `display.glow("head", ...)` and other calls on one part. Print `robot.parts()` to see them all. - **Blockbench:** the name of the group or cube, as you wrote it in the editor. If two parts have the same name, the second one gets `_2`, the third `_3`, and so on. - **BDEngine:** the name in lowercase, with spaces turned into `_`. Blocks and items are named by their id, for example `minecraft:oak_planks`. A text is named by its text. Repeated names get `_2` too. BDEngine models also have one hidden part called `root`. ```java display.glow("head", Color.AQUA); display.brightness("lamp", new Display.Brightness(15, 15)); ``` Blockbench removes some cubes while it loads, so the model needs fewer entities. A cube that is hidden inside another cube is dropped. Cubes of the same flat color that touch each other are merged into one. Groups are never removed. ## Colors (Blockbench) A vanilla block display shows a block. It cannot show a custom texture. So displaymotion turns every cube into one block: 1. It takes the pixels of the texture that cover the faces of the cube. See-through pixels are ignored. 2. It makes one average color from them. 3. It picks the closest block from a list of about 100 full blocks. The list has concrete, wool and terracotta in all 16 colors, stone types, planks, bricks, and blocks like gold, iron, diamond, lapis and snow. This means: - One cube is one block. The detail inside a cube is lost. If you want detail, use more and smaller cubes. - Use clear, flat colors. A cube with a plain red, blue or white texture gives a good block. A muddy color can give a block you did not expect. - A cube with no texture, or with a texture that is not saved inside the file, becomes a gray block. - If you do not like the block, change the color a little in Blockbench and load again. ## Size, position and turning - **Blockbench:** 16 pixels are one block. - **BDEngine:** the size is the same as in the editor. - The point (0, 0, 0) of the editor is placed on the location you spawn at. If your model stands on the floor at height 0, its feet touch the location. - The yaw and pitch of the location turn the whole model. The example plugin turns the model to look at the player with `at.setYaw(player.getLocation().getYaw() + 180f)`. - The scale in `spawn(motion, at, scale)` changes the size and also the distance parts move in animations. ## What works and what does not **Blockbench** - Works: cubes, nested groups, cube rotation on any axis, inflate, animations with rotation, position and scale keyframes. Blending can be linear, smooth or step. Smooth also covers bezier, the bezier handles are ignored. - Skipped: meshes and every element that is not a cube. Sounds, particles and other keyframe types. - Molang in keyframes, like `math.sin(q.anim_time * 90)`, is not supported yet. Those values are read as 0. - Only groups can be animated, not single cubes. - Textures have to be saved inside the file. Blockbench does this by default. **BDEngine** - Works: blocks with block states, items with their display mode, player heads with skins, texts, brightness, groups and animations. Both the new project files and the older text files work. - Text comes in as plain text. Colors, bold, background and alignment are not read. - Other item data, like enchantments or custom model data, is not read. - A block or item that the server does not know (for example from a newer version) becomes stone. ## Limits - At most 256 parts per model. A BDEngine model uses one of them for `root`. - An animation can be at most 6000 ticks. - Every part is one entity that the player's game draws. A model with 200 parts costs more fps than one with 20. Fewer cubes is better. ## Problems and fixes | What you see | Why | Fix | | --- | --- | --- | | `only .bbmodel and .bdengine files work` | The file has another ending. | Save the project again. Use `.bbmodel` or `.bdengine`. | | `not a bdengine file` | The file is not a BDEngine project. | Save it as a project in BDEngine, not as a command. | | `no animation named 'x', there are [...]` | The name is wrong. Capital letters matter. | Use a name from the list in the error, or from `animations()`. | | `a model can have at most 256 parts` | The model has too many parts. | Use fewer and bigger cubes. | | `saveResource` says it cannot find the file | The file is not in your jar. | Put it in `src/main/resources/models/` and build again. | | The animation plays but nothing moves | Blockbench: the cubes are not in a group. Or the keyframes use Molang. | Put the cubes in a group. Use plain numbers in keyframes. | | Colors look wrong or gray | The texture is not inside the file, or the color is muddy. | Save the textures inside the file. Use clear flat colors. | | The model floats or sinks | The model is not at height 0 in the editor. | Move it in the editor, or spawn it a bit higher or lower. | | The model looks the wrong way | The yaw of the location. | Turn the location, for example `at.setYaw(at.getYaw() + 180f)`. | | The model is too big or small | The editor size is not the size you want. | Use the scale: `spawn(motion, at, 0.5f)`. | | You see nothing | You are more than 48 blocks away, or the player has a client older than 1.19.4. | Go closer and check the client version. | ## Examples The example plugin ships three models: - `models/robot.bbmodel` with the animations `idle`, `walk`, `wave` and `dance` - `models/windmill.bbmodel` with `spin` and `idle` - `models/chest.bdengine` with `open` Run `/dm model robot.bbmodel walk` in game. Drop more files into `plugins/displaymotion-example/models/` and run `/dm model [animation]`. Tab complete lists the files and the animations. # Cookbook Short recipes. Each one is code you can copy. They use `motion` for your `DisplayMotion` and `location` for a `Location`. See [Getting started](https://displaymotion.peakstone.app/docs/getting-started/) for how to make them. ## Floating item that spins Good for a reward, a drop or a shop item. It grows in, then spins and floats. ```java static final Animation FLOAT = Animations.popIn(20).then(Animations.spin(80).with(Animations.bob(0.3f, 80))); motion.item(location, ItemStack.of(Material.NETHER_STAR)).play(FLOAT); ``` ## Remove with an animation A display that just disappears looks cheap. Shrink it first. The code after the comma runs when the animation ends. ```java display.play(Animations.popOut(10), display::remove); ``` ## Follow a player like a pet A task moves the display every 2 ticks. The `4` makes the client glide over 4 ticks, so the pet trails a bit behind the player. ```java MotionDisplay pet = motion.item(spot(player), ItemStack.of(Material.DIAMOND_SWORD)).play(FLOAT); Bukkit.getScheduler().runTaskTimer(plugin, task -> { if (pet.isRemoved() || !player.isOnline()) { task.cancel(); return; } pet.teleport(spot(player), 4); // move every 2 ticks, glide over 4, so it trails behind }, 2L, 2L); ``` `spot` is a small helper that picks a place behind the player: ```java static Location spot(Player player) { Location spot = player.getLocation(); Vector back = spot.getDirection().setY(0); if (back.lengthSquared() > 1e-6) { spot.subtract(back.normalize().multiply(1.2)); } spot.add(0, 1.6, 0); spot.setPitch(0f); return spot; } ``` ## Only one player sees it Use this for quest markers or private hints. Nobody sees the display until you call `show`. ```java motion.item(location, ItemStack.of(Material.EMERALD)) .visibleByDefault(false) .show(player); ``` ## Hide from one player ```java display.hide(player); display.show(player); // back again ``` ## Label that faces the player A billboard text turns to look at every player, so it is always readable. ```java motion.text(location, Component.text("Spawn", NamedTextColor.GOLD)) .billboard(Display.Billboard.CENTER) .play(Animations.popIn(15)); ``` ## Change a text later ```java label.text(Component.text("Open now", NamedTextColor.AQUA)); // label is a text display you spawned before ``` ## Glowing outline ```java display.glow(Color.AQUA); // null turns it off ``` ## Always bright, even in a cave By default a display is lit by the world, so it is dark in a cave. A fixed light level fixes this. ```java display.brightness(new Display.Brightness(15, 15)); ``` ## Mirror a part A negative scale flips a part on that axis. ```java Pose.IDENTITY.withScale(-1f, 1f, 1f) ``` ## A hop ```java static final Animation HOP = Animations.slide(0f, 1f, 0f, 6, Ease.OUT) .then(Animations.slide(0f, -1f, 0f, 6, Ease.IN)); ``` `then` starts each part where the last ended, so the second slide brings it back down. ## A button you can click Right click presses it and counts. Left click breaks it. ```java static final Animation PRESS = Animation.keyframes() .at(0, Pose.IDENTITY) .at(2, Pose.IDENTITY.withOffset(0f, -0.08f, 0f)) .at(6, Pose.IDENTITY) .build(); int[] clicks = {0}; MotionDisplay button = motion.model(location) .block("base", Material.POLISHED_BLACKSTONE.createBlockData(), Pose.IDENTITY.withOffset(0f, 0.1f, 0f).withScale(0.9f, 0.2f, 0.9f)) .block("cap", Material.RED_CONCRETE.createBlockData(), Pose.IDENTITY.withOffset(0f, 0.27f, 0f).withScale(0.6f, 0.14f, 0.6f)) .text("label", Component.text("Click me"), Pose.IDENTITY.withOffset(0f, 0.8f, 0f)) .spawn() .billboard("label", Display.Billboard.CENTER) .hitbox(1f, 0.5f); button.onClick(click -> { if (click.leftClick()) { motion.effects().shatter(location, Material.RED_CONCRETE.createBlockData()); button.remove(); return; } clicks[0]++; button.play("cap", PRESS).text("label", Component.text("Clicks: " + clicks[0])); }); ``` Try it in game with `/dm button` from the example plugin. ## Spawn an imported model Load the file once. Spawn it every time you need it. See [Importing](https://displaymotion.peakstone.app/docs/importing/) for the full steps. ```java ImportedModel robot = ImportedModel.load(getDataFolder().toPath().resolve("models/robot.bbmodel")); MotionDisplay display = robot.spawn(motion, location, 0.5f); robot.play(display, "walk"); ``` ## Remove everything when the plugin stops You do not need to do anything. The library removes all displays when your plugin disables. To do it by hand, call `motion.close()`. # How it works You do not need this page to use the library. Read it if you want to know why it is fast, or why something has a limit. ## Threads - **Main thread:** every 10 ticks it copies the online players (position, world, connection). Events for quit, teleport, respawn and world change only queue a note. Nothing else runs there. - **Motion thread:** one per plugin, every 50 ms. It runs your commands, decides who sees what, steps animations every 2 ticks and builds the packets. - **Network threads:** each player gets one task and one bundle per tick. - `MotionDisplay` methods only queue a command. They are safe from any thread. ## Why it is cheap - An animation is baked once into a matrix per frame. A step reads the table. - Only parts that moved are computed again. Children follow their parent. - A display that nobody sees is not computed. - A packet is encoded once and shared by every viewer. Only the changed transform fields are sent. - A slow connection skips frames and gets the full state when it catches up. - Parts that stay above the spawn point get a culling box, so the client skips them when they are off screen. - Big animations are baked on another thread. ## Fake entities Every part is a display entity that exists only on the client. The server never ticks it, saves it or collides with it. The library spawns it when a player comes in range, and removes it when the player leaves the range (with 4 blocks of margin), quits, teleports far, respawns or changes world. ## Limits - Java Edition only. Bedrock players (Geyser) do not see display entities. - Clients from 1.19.4 up work through ViaVersion. Older clients do not have display entities. - Text over 32000 characters of JSON is refused, ViaVersion kicks players on huge text. - Glide time for `teleport` is 0 to 59 ticks, the client limit. - The client blends in straight lines. Curves are sent as a frame every 2 ticks. ## Client bugs we work around - **MC-295859:** a teleport and a transform in the same tick get out of sync. The library never moves an entity to fake a transform. - **MC-311974 (26.3):** entity movement can stutter. Animations change the transform only, never the position. - **MC-260874:** a long blend started early shows a "ghost". Frames always use the same 2 tick length. - **MC-259926:** late joiners do not know the blend in progress. They get the current frame at spawn. Roadmap # What works, what is next No dates. The order can change. Tell us what you need on [GitHub issues](https://github.com/Jan1k1/displaymotion/issues). 12Done 6Next 14Later ## Done v0.2.0. See the [docs](https://displaymotion.peakstone.app/docs/). - ModelsBuild models from many parts. Parts have parents and groups and move as one. - AnimationsEasing, presets like popIn and spin, and your own keyframes. - EffectsShatter, tombstone, sword rain and soul. One line each. - ImportLoad Blockbench and BDEngine models, with their animations. - LooksGlow, brightness, billboard and live text. - Smooth movesTeleport a display with a smooth glide. - Per player visibilityShow a display to all players or only to some. - Client cullingParts get a culling box, so the client skips them when they are off screen. - Own threadThe motion runs on its own thread, not on the main thread. - Clickable hitboxesLeft and right clicks on a display, with a box that players can hit. - Events in animationsRun your code, a sound or particles at an exact tick of an animation. - Docs and setupDocs, llms.txt and a JitPack build you can add to Gradle. ## Next We build these first. - Riding and sittingDisplays ride players and mobs, for hats, wings and pets. Players sit on models that have hitboxes. - More easingBack, elastic and bounce. - CrossfadeBlend smoothly from one animation into the next. - Follow an entityA display follows a player or mob smoothly. - Leaderboard exampleA ready example of a leaderboard made from displays. - Level of detailPlayers far away get fewer parts and fewer frames. ## Later After the Next list. - Mannequin NPCsPlayer shaped models with any skin. - Text animationsTyping, counters and fades. - Animated looksFade in and out, a glow that pulses, and blocks that change in the middle of an animation. - Per player text and glowText in the language of each player. Highlight a model for one player only. - Live controllersFor example, a head that looks at the nearest player. - Curved pathsSmooth curves for cutscenes and projectiles. - MolangSupport Molang in Blockbench keyframes. - Hot reloadChange a model file and see it in game without a restart. - Solid hitboxes and break apartHitboxes that block players, and any model can break into pieces. - More ready usesDamage numbers, cutscenes, 3D menus, and moving builds like doors and elevators. - In game editorEdit models in game. It works on a vanilla client, with no mod. - IntegrationsPlaceholderAPI, Skript, Denizen, MythicMobs and crate plugins. - Benchmark pageA page with real, measured numbers. - Compatibility pageOne page that shows what works with what. A first version is [here](https://displaymotion.peakstone.app/compatibility/). ## Research We are not sure yet if these are possible, or how. - Moving player modelsPlayer models with moving arms and legs and real skins, without a resource pack. - Bedrock playersShow displays to Bedrock players through a Geyser extension. - Usage statsAnonymous stats with bStats. Optional, clearly documented, and you can turn them off. ## Ideas and feedback Ideas and feedback are welcome on [GitHub issues](https://github.com/Jan1k1/displaymotion/issues). A star on GitHub helps more people find displaymotion. Compatibility # What you need What the server needs, and which players can see the displays. ServerPaper 26.3 Folia is not supported yet. JavaJava 25 On the server. PacketEvents2.14.0+ The server owner installs it as a plugin. Do not shade it into your plugin. Players1.19.4+ Java Edition. Older than the server needs ViaVersion. ## Who sees displays - Yes**Java, same version as the server**: Nothing extra to do. - Yes, with Via**Java 1.19.4 or newer, older than the server**: Install ViaVersion and ViaBackwards on the server. No extra setup. Tested with a 1.21.11 client on a 26.3 server. - No**Java older than 1.19.4**: Display entities came in 1.19.4. Older clients do not have them. - Not yet**Bedrock through Geyser**: Geyser does not show display entities yet. A Geyser extension is on the [roadmap](https://displaymotion.peakstone.app/roadmap/) as research. Very large text can make ViaVersion kick a player. displaymotion refuses text over 32000 characters of JSON for this reason. See [How it works](https://displaymotion.peakstone.app/docs/how-it-works/). ## In your plugin Your plugin needs `depend: [packetevents]` in `plugin.yml`. See [Getting started](https://displaymotion.peakstone.app/docs/getting-started/). ## Licenses | Module | License | | --- | --- | | `core` | MIT | | `packetevents` | GPL-3.0-only | | Example plugin | GPL-3.0-only | PacketEvents is GPL-3.0, so the module that uses it is GPL-3.0 too. `core` stays MIT, so you can write your own sender. This is not legal advice. ## Something does not work? Tell us on [GitHub issues](https://github.com/Jan1k1/displaymotion/issues). Write the server version, the client version, and which plugins you use.