Skip to main content
This guide shows you how to load a model, spawn it on an entity, and play an animation.

Prerequisites

  • BetterModel plugin installed on your server
  • A .bbmodel file placed in plugins/BetterModel/models/
  • Basic Java/Kotlin knowledge
For this tutorial, we’ll assume you have a model file named demon_knight.bbmodel with an animation called attack.

Step 1: Get a Model

1

Load the Model

Use BetterModel.model() to retrieve a model renderer by name. The name corresponds to the .bbmodel file without the extension.
import kr.toxicity.model.api.BetterModel;
import kr.toxicity.model.api.data.renderer.ModelRenderer;
import java.util.Optional;

Optional<ModelRenderer> renderer = BetterModel.model("demon_knight");
if (renderer.isEmpty()) {
    plugin.getLogger().warning("Model 'demon_knight' not found!");
    return;
}
Use BetterModel.modelOrNull() if you prefer null checks over Optional.
2

Understanding Model Types

BetterModel supports two types of models:
  • General models (BetterModel.model()) - Located in BetterModel/models/, used for entities
  • Player limb models (BetterModel.limb()) - Located in BetterModel/players/, used for player cosmetics
// For entity models
Optional<ModelRenderer> entityModel = BetterModel.model("demon_knight");

// For player models (cosmetics)
Optional<ModelRenderer> playerModel = BetterModel.limb("steve");

Step 2: Spawn the Model

You can spawn models on entities or at specific locations.

Attach to an Entity

Attach a model to a living entity (e.g., zombie, armor stand):
BukkitAdapter.java
import kr.toxicity.model.api.BetterModel;
import kr.toxicity.model.api.bukkit.platform.BukkitAdapter;
import kr.toxicity.model.api.tracker.EntityTracker;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Zombie;

public void spawnModelOnEntity(Entity entity) {
    EntityTracker tracker = BetterModel.model("demon_knight")
        .map(renderer -> renderer.create(BukkitAdapter.adapt(entity)))
        .orElse(null);
    
    if (tracker != null) {
        plugin.getLogger().info("Model attached to entity!");
    }
}
BukkitAdapter.adapt() converts Bukkit entities to BetterModel’s platform-agnostic entity wrapper.

Spawn at a Location (Dummy)

Create a standalone model at a specific location:
import kr.toxicity.model.api.tracker.DummyTracker;
import org.bukkit.Location;

public void spawnModelAtLocation(Location location) {
    DummyTracker tracker = BetterModel.model("demon_knight")
        .map(renderer -> renderer.create(BukkitAdapter.adapt(location)))
        .orElse(null);
    
    if (tracker != null) {
        plugin.getLogger().info("Model spawned at location!");
    }
}

Get or Create Pattern

Use getOrCreate() to reuse existing trackers or create a new one:
public void getOrCreateModel(Entity entity) {
    EntityTracker tracker = BetterModel.model("demon_knight")
        .map(renderer -> renderer.getOrCreate(BukkitAdapter.adapt(entity)))
        .orElse(null);
    
    // This will return the existing tracker if already attached
}

Step 3: Play an Animation

Once spawned, animate the model using the tracker:
import kr.toxicity.model.api.animation.AnimationModifier;

public void playAnimation(EntityTracker tracker) {
    // Play the "attack" animation with default settings
    boolean success = tracker.animate("attack");
    
    if (success) {
        plugin.getLogger().info("Animation started!");
    } else {
        plugin.getLogger().warning("Animation 'attack' not found!");
    }
}

Advanced Animation Control

Control animation speed, looping, and transitions:
public void playAdvancedAnimation(EntityTracker tracker) {
    AnimationModifier modifier = AnimationModifier.builder()
        .speed(1.5F)              // 1.5x speed
        .loop(true)                // Loop continuously
        .transition(5)             // 5-tick blend transition
        .removeAfter(false)        // Keep animation after completion
        .build();
    
    tracker.animate("attack", modifier);
}

Stop an Animation

public void stopAnimation(EntityTracker tracker) {
    tracker.stopAnimation("attack");
}

Step 4: Customize Display Properties

Update the model’s appearance using TrackerUpdateAction:
import kr.toxicity.model.api.tracker.TrackerUpdateAction;

public void customizeModel(EntityTracker tracker) {
    // Apply red tint
    tracker.update(TrackerUpdateAction.tint(0xFF0000));
    
    // Set brightness (block light: 15, sky light: 15)
    tracker.update(TrackerUpdateAction.brightness(15, 15));
    
    // Enable enchanted glint on all bones
    tracker.update(TrackerUpdateAction.enchant(true));
    
    // Composite multiple updates
    tracker.update(TrackerUpdateAction.composite(
        TrackerUpdateAction.tint(0x00FF00),
        TrackerUpdateAction.glow(true),
        TrackerUpdateAction.glowColor(0xFFFF00)
    ));
}

Apply Updates Before Spawn

You can apply updates before the model is sent to players:
public void spawnWithCustomization(Entity entity) {
    EntityTracker tracker = BetterModel.model("demon_knight")
        .map(renderer -> renderer.create(
            BukkitAdapter.adapt(entity),
            TrackerModifier.DEFAULT,
            t -> {
                // Pre-spawn customization
                t.update(TrackerUpdateAction.tint(0x0026FF));
                t.update(TrackerUpdateAction.brightness(15, 15));
            }
        ))
        .orElse(null);
}

Complete Example

Here’s a full working example that ties everything together:
CompleteExample.java
import kr.toxicity.model.api.BetterModel;
import kr.toxicity.model.api.animation.AnimationModifier;
import kr.toxicity.model.api.bukkit.platform.BukkitAdapter;
import kr.toxicity.model.api.tracker.EntityTracker;
import kr.toxicity.model.api.tracker.TrackerModifier;
import kr.toxicity.model.api.tracker.TrackerUpdateAction;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Entity;
import org.bukkit.entity.Player;
import org.bukkit.plugin.java.JavaPlugin;

public class ModelCommand implements CommandExecutor {
    
    private final JavaPlugin plugin;
    
    public ModelCommand(JavaPlugin plugin) {
        this.plugin = plugin;
    }
    
    @Override
    public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
        if (!(sender instanceof Player player)) {
            sender.sendMessage("Only players can use this command!");
            return true;
        }
        
        Entity target = player.getTargetEntity(5);
        if (target == null) {
            player.sendMessage("Look at an entity!");
            return true;
        }
        
        // Spawn model with customization
        EntityTracker tracker = BetterModel.model("demon_knight")
            .map(renderer -> renderer.create(
                BukkitAdapter.adapt(target),
                TrackerModifier.DEFAULT,
                t -> {
                    // Apply blue tint and full brightness
                    t.update(TrackerUpdateAction.composite(
                        TrackerUpdateAction.tint(0x0026FF),
                        TrackerUpdateAction.brightness(15, 15)
                    ));
                }
            ))
            .orElse(null);
        
        if (tracker == null) {
            player.sendMessage("Failed to spawn model!");
            return true;
        }
        
        // Play attack animation with custom settings
        AnimationModifier modifier = AnimationModifier.builder()
            .speed(1.2F)
            .transition(3)
            .build();
        
        boolean animated = tracker.animate("attack", modifier);
        
        if (animated) {
            player.sendMessage("Model spawned and animated!");
        } else {
            player.sendMessage("Model spawned but animation not found!");
        }
        
        return true;
    }
}

Common Patterns

Check if Entity Already Has Model

import kr.toxicity.model.api.tracker.EntityTrackerRegistry;

public boolean hasModel(Entity entity) {
    return BetterModel.registry(BukkitAdapter.adapt(entity)).isPresent();
}

Remove Model from Entity

public void removeModel(EntityTracker tracker) {
    tracker.close(); // Removes all display entities and cleans up
}

Listen for Animation Events

import kr.toxicity.model.api.animation.AnimationModifier;

public void playWithCallback(EntityTracker tracker) {
    tracker.animate("attack", AnimationModifier.DEFAULT, () -> {
        // Called when animation completes
        plugin.getLogger().info("Animation finished!");
    });
}

Troubleshooting

  • Verify the .bbmodel file is in plugins/BetterModel/models/
  • Check the filename matches (case-sensitive)
  • Look for errors in plugins/BetterModel/logs/ or server console
  • Ensure the model was loaded on server start
  • Confirm the animation name matches exactly (case-sensitive)
  • Check the animation exists in the .bbmodel file
  • Verify the tracker is not closed or removed
  • Ensure players are within render distance
  • Check that the entity is in a loaded chunk
  • Verify sight tracing settings in TrackerModifier
  • Ensure the model has valid textures
  • Try adjusting view range with TrackerUpdateAction.viewRange()

Next Steps

API Reference

Explore all available methods and classes

Animation System

Deep dive into animation control

Player Models

Learn about 12-limb player animations

Examples

See real-world usage examples

Build docs developers (and LLMs) love