PulseLib Items
Items need a slightly different setup from entities and block entities. Minecraft creates one Item object for the item type, but the player can hold many ItemStacks of that type. If animation state lived directly on the item object, every stack would share the same animation.
PulseLib handles this by using AnimManagerKey and SingletonAnimationManager. The item still implements PItemAnimatable, but the actual manager is resolved per key.
Item JSON¶
Create a modern item definition file. This is not the animated GLB model; it tells Minecraft to use your registered special model renderer.
{
"model": {
"type": "minecraft:special",
"base": "examplemod:item/wand",
"model": {
"type": "examplemod:wand",
"model_location": "examplemod:glmodels/item/wand.glb",
"textures": [
"examplemod:item/wand/body"
]
}
}
}
The model.type value, here examplemod:wand, must be registered as a special renderer codec. The nested fields are decoded by PModelData.CODEC: model_location, optional model_type, optional model_format, and textures.
Item class¶
The important part here is getAnimationManager. Use the key passed into the method, not a single field shared by every stack.
public class WandItem extends Item implements PItemAnimatable<WandItem> {
private static final PRawAnimation IDLE = PRawAnimation.begin()
.thenLoop("idle")
.build();
public WandItem(Properties properties) {
super(properties);
}
@Override
public PAnimationManager<WandItem> getAnimationManager(AnimManagerKey key) {
return SingletonAnimationManager.getManager(key, this);
}
@Override
public void registerAnimationControllers(PAnimationManager.PAnimationRegistrar<WandItem> registrar) {
registrar.add("idle", () -> state -> {
state.controller().play(IDLE);
return ControllerState.PLAY;
});
}
@Override
public IClientItemExtensions registerClientExtension() {
return IClientItemExtensions.DEFAULT;
}
}
Renderer¶
The renderer points at the GLB model and selects a PulseLib render type. PItemRenderer implements Minecraft's SpecialModelRenderer<RS>, so it needs a render-state type and an unbaked codec.
public class WandRenderer extends PItemRenderer<WandItem, PItemRenderState.Impl<WandItem>> {
public WandRenderer(PModelData modelData) {
super(modelData, PRenderTypes.RenderTypeProvider::trianglesSolid);
}
@Override
protected PItemRenderState.Impl<WandItem> createRenderState() {
return new PItemRenderState.Impl<>();
}
public record Unbaked(PModelData data) implements SpecialModelRenderer.Unbaked<PItemRenderState.Impl<WandItem>> {
public static final MapCodec<Unbaked> MAP_CODEC = PModelData.CODEC.xmap(Unbaked::new, Unbaked::data);
@Override
public WandRenderer bake(BakingContext context) {
return new WandRenderer(this.data);
}
@Override
public MapCodec<Unbaked> type() {
return MAP_CODEC;
}
}
}
Register the unbaked renderer on the client mod event bus:
@SubscribeEvent
public static void registerSpecialModels(RegisterSpecialModelRendererEvent event) {
event.register(Identifier.fromNamespaceAndPath("examplemod", "wand"), WandRenderer.Unbaked.MAP_CODEC);
}
PulseLib automatically registers item client extensions for items that implement PItemAnimatable. Return IClientItemExtensions.DEFAULT unless the item also needs extra client extension behavior, such as armor integration.
Stack-specific state¶
AnimManagerKey.of(ItemStack) uses item id, stack count, and component data. If your item animation should differ by custom data, store it in item components so the key changes. If two stacks have identical key data, they can reuse the same cached manager.
Classes used: