A Tracker is the runtime controller for a specific model instance. It manages the lifecycle, rendering, animation playback, and player interaction for a single spawned model.
Think of ModelRenderer as the blueprint and Tracker as the living instance created from that blueprint.
ModelRenderer renderer = BetterModel.platform() .modelManager() .getRenderer("warrior") .orElseThrow();// Create tracker attached to entityEntityTracker tracker = renderer.create(BaseEntity.of(entity));// Tracker will automatically follow entity movement// and clean itself up when no players are nearby
Key Features:
Automatically follows entity position and rotation
Registered in EntityTrackerRegistry for the entity
Persists across server restarts (for GENERAL type models)
Trackers automatically start a scheduled update task when players are nearby:
Tick Interval: 25ms (40 ticks per second)
Auto-start: When first player comes in range
Auto-pause: When no players are in range
Frame counter: Tracks elapsed ticks since start
// Check if tracker is actively updatingif (tracker.isScheduled()) { System.out.println("Tracker is running");}// Get current player countint viewers = tracker.playerCount();
The tracker tick rate (TRACKER_TICK_INTERVAL = 25ms) is faster than Minecraft’s tick rate (50ms) to provide smooth animations.
Trackers automatically spawn display entities for players in range:
// Check if spawned for a playerif (tracker.isSpawned(player)) { System.out.println("Model is visible to player");}// Check by UUIDif (tracker.isSpawned(player.getUniqueId())) { System.out.println("Model is spawned");}
// Hide from specific playertracker.hide(player);// Check if hiddenif (tracker.isHide(player)) { System.out.println("Hidden from player");}// Show to playertracker.show(player);
Hiding a tracker doesn’t remove it - it just makes display entities invisible. The tracker continues ticking.
// Every tick (50ms)tracker.tick((t, bundler) -> { // Runs every Minecraft tick});// Every N tickstracker.tick(20, (t, bundler) -> { // Runs once per second (20 ticks)});
// Get bone by nameRenderedBone bone = tracker.bone("head");if (bone != null) { System.out.println("Found head bone");}// Get bone by BoneNameRenderedBone bone = tracker.bone(BoneName.of("leftArm"));// Get bone by predicateRenderedBone bone = tracker.bone(b -> b.name().tagged(BoneTags.HEAD));// Get all bonesCollection<RenderedBone> bones = tracker.bones();for (RenderedBone bone : bones) { System.out.println("Bone: " + bone.name());}
// Get all display entities as a streamtracker.displays().forEach(display -> { // Manipulate display entity System.out.println("Display: " + display.uuid());});
// Get registry for an entityOptional<EntityTrackerRegistry> registry = BetterModel.registry( entity.getUniqueId());registry.ifPresent(reg -> { // Get all trackers on this entity Collection<EntityTracker> trackers = reg.allTrackers(); // Get specific tracker by name EntityTracker tracker = reg.getTracker("warrior").orElse(null); // Remove all trackers reg.clear();});
Don’t run expensive operations in frame handlers. Use tick handlers or scheduled tasks.
// Bad: runs 40 times per secondtracker.frame((t, b) -> { expensiveCalculation();});// Good: runs once per secondtracker.tick(20, (t, b) -> { expensiveCalculation();});
Use task() for entity operations
Entity manipulation must happen on the main thread.
tracker.task(() -> { // Safe: runs on main thread entity.teleport(newLocation);});
Check isClosed before operations
Always verify tracker state before operations.
if (!tracker.isClosed()) { tracker.animate("attack");}