Skip to main content

Overview

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.

Tracker Types

BetterModel provides two tracker types:

EntityTracker

Attached to entities, follows their movement

DummyTracker

Standalone at fixed locations

EntityTracker

EntityTracker attaches to a Minecraft entity and automatically follows its position and rotation.
src/main/java/com/example/EntityTrackerExample.java
ModelRenderer renderer = BetterModel.platform()
    .modelManager()
    .getRenderer("warrior")
    .orElseThrow();

// Create tracker attached to entity
EntityTracker 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)
  • Can be retrieved with getOrCreate() pattern

DummyTracker

DummyTracker spawns a model at a fixed location without an attached entity.
src/main/java/com/example/DummyTrackerExample.java
ModelRenderer renderer = BetterModel.platform()
    .modelManager()
    .getRenderer("statue")
    .orElseThrow();

// Create tracker at location
DummyTracker tracker = renderer.create(location);

// Must manually update location if needed
tracker.task(() -> {
    // Custom movement logic
});
Key Features:
  • Stationary position (unless manually updated)
  • No entity attachment
  • Useful for decorations, holograms, NPCs
  • Must be manually closed when no longer needed

Lifecycle Management

Creation

Trackers are created through ModelRenderer:
// Entity tracker
EntityTracker tracker = renderer.create(BaseEntity.of(entity));

// Dummy tracker
DummyTracker tracker = renderer.create(location);

// With modifier
EntityTracker tracker = renderer.create(
    BaseEntity.of(entity),
    TrackerModifier.builder()
        .renderDistance(128)
        .build()
);

Scheduling and Ticking

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 updating
if (tracker.isScheduled()) {
    System.out.println("Tracker is running");
}

// Get current player count
int viewers = tracker.playerCount();
The tracker tick rate (TRACKER_TICK_INTERVAL = 25ms) is faster than Minecraft’s tick rate (50ms) to provide smooth animations.

Pausing and Resuming

// Pause ticking (animations stop, packets stop sending)
tracker.pause(true);

// Resume ticking
tracker.pause(false);

Closing

Always close trackers when they’re no longer needed:
// Close the tracker
tracker.close();

// Check if closed
if (tracker.isClosed()) {
    System.out.println("Tracker is closed");
}

// Handle close event
tracker.handleCloseEvent((t, reason) -> {
    System.out.println("Closed: " + reason);
    if (reason == Tracker.CloseReason.REMOVE) {
        // Manual removal
    }
});
Close Reasons:
REMOVE
CloseReason
Manually removed via close() - tracker state is not saved
PLUGIN_DISABLE
CloseReason
Plugin is disabling - tracker state is saved for GENERAL models
DESPAWN
CloseReason
Entity despawned - tracker state is saved for GENERAL models

Player Visibility

Spawning for Players

Trackers automatically spawn display entities for players in range:
// Check if spawned for a player
if (tracker.isSpawned(player)) {
    System.out.println("Model is visible to player");
}

// Check by UUID
if (tracker.isSpawned(player.getUniqueId())) {
    System.out.println("Model is spawned");
}

Hiding and Showing

// Hide from specific player
tracker.hide(player);

// Check if hidden
if (tracker.isHide(player)) {
    System.out.println("Hidden from player");
}

// Show to player
tracker.show(player);
Hiding a tracker doesn’t remove it - it just makes display entities invisible. The tracker continues ticking.

Removing from Players

// Fully remove display entities for a player
boolean removed = tracker.remove(player);

if (removed) {
    System.out.println("Removed from player");
}

Despawning

// Despawn for all players (but don't close tracker)
tracker.despawn();

// Model can be respawned when players come back in range

Scheduled Tasks

Frame Handlers

Execute code every tracker tick (25ms):
tracker.frame((t, bundler) -> {
    // Runs every 25ms (40 times per second)
    // bundler allows sending custom packets
});

Tick Handlers

Execute code every Minecraft tick (50ms):
// Every tick (50ms)
tracker.tick((t, bundler) -> {
    // Runs every Minecraft tick
});

// Every N ticks
tracker.tick(20, (t, bundler) -> {
    // Runs once per second (20 ticks)
});

Scheduled Tasks

Schedule tasks at custom intervals:
// Every 5 tracker ticks (125ms)
tracker.schedule(5, (t, bundler) -> {
    System.out.println("Custom interval");
});

Main Thread Tasks

Schedule tasks to run on the main thread:
tracker.task(() -> {
    // Runs on next Minecraft tick
    // Safe for entity/block manipulation
    entity.setHealth(entity.getHealth() + 1);
});

Per-Player Tasks

Run code for each visible player every tick:
tracker.perPlayerTick((t, player) -> {
    // Runs for each player viewing the model
    if (player.getHealth() < 10) {
        t.hide(player);
    }
});

Rotation and Scaling

Rotation

Control how the model rotates:
// Set rotation supplier
tracker.rotation(() -> {
    float yaw = entity.getLocation().getYaw();
    return ModelRotation.of(0, yaw, 0);
});

// Get current rotation
ModelRotation rotation = tracker.rotation();
System.out.println("Yaw: " + rotation.yaw());

Rotation Strategies

// Follow entity yaw only (default)
tracker.rotator(ModelRotator.YAW);

// Follow entity pitch and yaw
tracker.rotator(ModelRotator.PITCH_AND_YAW);

// No rotation
tracker.rotator(ModelRotator.NONE);

// Custom rotator
tracker.rotator((t, baseRotation) -> {
    // Custom rotation logic
    return baseRotation.add(0, 45, 0);
});

Scaling

// Set scaler
tracker.scaler(ModelScaler.of(2.0f));  // 2x size

// Entity-based scaling (uses entity scale)
tracker.scaler(ModelScaler.entity());

// Get current scaler
ModelScaler scaler = tracker.scaler();

Accessing Bones

Get individual bones for manipulation:
// Get bone by name
RenderedBone bone = tracker.bone("head");
if (bone != null) {
    System.out.println("Found head bone");
}

// Get bone by BoneName
RenderedBone bone = tracker.bone(BoneName.of("leftArm"));

// Get bone by predicate
RenderedBone bone = tracker.bone(b -> b.name().tagged(BoneTags.HEAD));

// Get all bones
Collection<RenderedBone> bones = tracker.bones();
for (RenderedBone bone : bones) {
    System.out.println("Bone: " + bone.name());
}

Display Entities

Access underlying display entities:
// Get all display entities as a stream
tracker.displays().forEach(display -> {
    // Manipulate display entity
    System.out.println("Display: " + display.uuid());
});

Model Height

Get the calculated height of the model:
// Get model height (based on head bone position)
double height = tracker.height();
System.out.println("Model height: " + height);
Height is calculated from head-tagged bones and cached per tick for performance.

Force Updates

Force display entity data updates:
// Flag for force update on next tick
tracker.forceUpdate(true);

// All display entities will send full data packets

EntityTrackerRegistry

For entity-based trackers, access the registry:
// Get registry for an entity
Optional<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();
});

Best Practices

Trackers consume resources. Always close them when no longer needed.
@EventHandler
public void onEntityDeath(EntityDeathEvent event) {
    BetterModel.registry(event.getEntity().getUniqueId())
        .ifPresent(EntityTrackerRegistry::clear);
}
Avoid creating duplicate trackers on the same entity.
// Safe: reuses existing tracker
EntityTracker tracker = renderer.getOrCreate(BaseEntity.of(entity));
Don’t run expensive operations in frame handlers. Use tick handlers or scheduled tasks.
// Bad: runs 40 times per second
tracker.frame((t, b) -> {
    expensiveCalculation();
});

// Good: runs once per second
tracker.tick(20, (t, b) -> {
    expensiveCalculation();
});
Entity manipulation must happen on the main thread.
tracker.task(() -> {
    // Safe: runs on main thread
    entity.teleport(newLocation);
});
Always verify tracker state before operations.
if (!tracker.isClosed()) {
    tracker.animate("attack");
}

Complete Example

src/main/java/com/example/CompleteTrackerExample.java
public class CustomNPC {
    private final EntityTracker tracker;
    private final Entity entity;
    
    public CustomNPC(Location location, ModelRenderer renderer) {
        // Spawn invisible entity
        this.entity = location.getWorld().spawn(
            location,
            Zombie.class,
            e -> {
                e.setAI(false);
                e.setInvisible(true);
                e.setInvulnerable(true);
            }
        );
        
        // Create tracker
        this.tracker = renderer.create(
            BaseEntity.of(entity),
            TrackerModifier.builder()
                .renderDistance(64)
                .sightTrace(true)
                .build()
        );
        
        // Start idle animation
        tracker.animate("idle");
        
        // Health indicator
        tracker.perPlayerTick((t, player) -> {
            double distance = player.getLocation().distance(
                entity.getLocation()
            );
            if (distance < 5) {
                // Show name tag when close
            }
        });
        
        // Periodic wave animation
        tracker.tick(100, (t, bundler) -> {
            t.animate("wave", AnimationModifier.DEFAULT_WITH_PLAY_ONCE);
        });
        
        // Cleanup handler
        tracker.handleCloseEvent((t, reason) -> {
            if (reason == Tracker.CloseReason.REMOVE) {
                entity.remove();
            }
        });
    }
    
    public void remove() {
        tracker.close();
        entity.remove();
    }
    
    public boolean isActive() {
        return !tracker.isClosed() && entity.isValid();
    }
}

See Also

Models & Renderers

Creating trackers from models

Animations

Playing animations on trackers

Bones & Hitboxes

Manipulating individual bones

API Reference

Complete Tracker API

Build docs developers (and LLMs) love