Overview
BetterModel supports player models with custom armor rendering. This allows you to:- Replace default armor with custom 3D models
- Add cosmetic armor layers
- Create unique armor sets with animations
- Apply armor models to specific players
Prerequisites
Create Player Model
Your BlockBench model must be configured as a player model and placed in
BetterModel/players/ directory.Basic Armor Model
Creating Armor Tracker
import kr.toxicity.model.api.BetterModel;
import kr.toxicity.model.api.entity.BaseEntity;
import kr.toxicity.model.api.profile.ModelProfile;
import kr.toxicity.model.api.tracker.PlayerTracker;
import org.bukkit.entity.Player;
public class ArmorModelManager {
public PlayerTracker applyArmorModel(Player player, String modelName) {
return BetterModel.limb(modelName).map(renderer -> {
// Create player profile from player
ModelProfile profile = ModelProfile.of(player);
// Create tracker with player skin
PlayerTracker tracker = (PlayerTracker) renderer.create(
BaseEntity.of(player),
profile,
TrackerModifier.DEFAULT
);
return tracker;
}).orElse(null);
}
public void removeArmorModel(PlayerTracker tracker) {
if (tracker != null && !tracker.isClosed()) {
tracker.close();
}
}
}
Dynamic Armor System
Full Armor Manager
import kr.toxicity.model.api.BetterModel;
import kr.toxicity.model.api.entity.BaseEntity;
import kr.toxicity.model.api.profile.ModelProfile;
import kr.toxicity.model.api.tracker.PlayerTracker;
import kr.toxicity.model.api.tracker.TrackerUpdateAction;
import kr.toxicity.model.api.util.function.BonePredicate;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerArmorStandManipulateEvent;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.inventory.ItemStack;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class DynamicArmorSystem implements Listener {
private final Map<UUID, PlayerTracker> armorTrackers = new ConcurrentHashMap<>();
private final String armorModelName = "custom_armor";
public void applyArmor(Player player) {
// Remove existing tracker if any
removeArmor(player);
BetterModel.limb(armorModelName).ifPresent(renderer -> {
ModelProfile profile = ModelProfile.of(player);
PlayerTracker tracker = (PlayerTracker) renderer.getOrCreate(
BaseEntity.of(player),
profile
);
armorTrackers.put(player.getUniqueId(), tracker);
// Update armor visibility based on equipped items
updateArmorVisibility(player, tracker);
// Start idle animation
tracker.animate("armor_idle");
});
}
public void removeArmor(Player player) {
PlayerTracker tracker = armorTrackers.remove(player.getUniqueId());
if (tracker != null) {
tracker.close();
}
}
public void updateArmorVisibility(Player player, PlayerTracker tracker) {
ItemStack helmet = player.getInventory().getHelmet();
ItemStack chestplate = player.getInventory().getChestplate();
ItemStack leggings = player.getInventory().getLeggings();
ItemStack boots = player.getInventory().getBoots();
// Show/hide armor pieces based on equipped items
tracker.update(
TrackerUpdateAction.togglePart(helmet != null && helmet.getType() != Material.AIR),
BonePredicate.name("helmet")
);
tracker.update(
TrackerUpdateAction.togglePart(chestplate != null && chestplate.getType() != Material.AIR),
BonePredicate.name("chestplate")
);
tracker.update(
TrackerUpdateAction.togglePart(leggings != null && leggings.getType() != Material.AIR),
BonePredicate.name("leggings")
);
tracker.update(
TrackerUpdateAction.togglePart(boots != null && boots.getType() != Material.AIR),
BonePredicate.name("boots")
);
}
@EventHandler
public void onArmorChange(PlayerArmorStandManipulateEvent event) {
Player player = event.getPlayer();
PlayerTracker tracker = armorTrackers.get(player.getUniqueId());
if (tracker != null) {
// Update on next tick after inventory updates
player.getServer().getScheduler().runTaskLater(
player.getServer().getPluginManager().getPlugin("YourPlugin"),
() -> updateArmorVisibility(player, tracker),
1L
);
}
}
@EventHandler
public void onPlayerQuit(PlayerQuitEvent event) {
removeArmor(event.getPlayer());
}
}
Animated Armor
Creating Armor with Animations
public class AnimatedArmorSet {
public void applyGlowingArmor(Player player) {
BetterModel.limb("glowing_armor").ifPresent(renderer -> {
PlayerTracker tracker = (PlayerTracker) renderer.create(
BaseEntity.of(player),
ModelProfile.of(player)
);
// Enable glow effect
tracker.update(TrackerUpdateAction.composite(
TrackerUpdateAction.glow(true),
TrackerUpdateAction.glowColor(0x00FFFF)
));
// Play pulsing animation
tracker.animate("armor_pulse");
// Add particle effects every second
tracker.tick(20, (t, bundler) -> {
Location loc = tracker.location().toBukkit();
loc.getWorld().spawnParticle(
Particle.ENCHANTMENT_TABLE,
loc.add(0, 1, 0),
10
);
});
});
}
}
Armor with Special Effects
Conditional Armor Effects
import kr.toxicity.model.api.animation.AnimationModifier;
import org.bukkit.potion.PotionEffectType;
public class SpecialArmorEffects {
public void setupArmorEffects(PlayerTracker tracker, Player player) {
// Glow when player has speed effect
tracker.perPlayerTick((t, p) -> {
if (player.hasPotionEffect(PotionEffectType.SPEED)) {
t.update(TrackerUpdateAction.composite(
TrackerUpdateAction.glow(true),
TrackerUpdateAction.glowColor(0xFFFF00)
));
} else {
t.update(TrackerUpdateAction.glow(false));
}
});
// Play attack animation when player attacks
tracker.tick((t, bundler) -> {
if (player.getAttackCooldown() == 1.0F) {
t.animate("armor_attack", AnimationModifier.builder()
.type(AnimationIterator.Type.PLAY_ONCE)
.speed(2.0F)
.build()
);
}
});
}
}
Color-Tinted Armor
Apply Leather Armor Colors
import org.bukkit.Color;
import org.bukkit.inventory.meta.LeatherArmorMeta;
public void applyArmorColor(PlayerTracker tracker, Player player) {
ItemStack chestplate = player.getInventory().getChestplate();
if (chestplate != null && chestplate.getItemMeta() instanceof LeatherArmorMeta meta) {
Color color = meta.getColor();
int rgb = (color.getRed() << 16) | (color.getGreen() << 8) | color.getBlue();
// Apply tint to chestplate model
tracker.update(
TrackerUpdateAction.tint(rgb),
BonePredicate.name("chestplate")
);
}
}
Best Practices
- Use player models (limbs) for armor, not general models
- Always provide a player profile to ensure correct skin rendering
- Update armor visibility dynamically when players change equipment
- Cache tracker references to avoid creating duplicates
- Player models require more resources than general models
- Limit the number of active player trackers for performance
- Always remove trackers when players leave the server
- Test armor models with different player skins
Complete Example
import kr.toxicity.model.api.BetterModel;
import kr.toxicity.model.api.entity.BaseEntity;
import kr.toxicity.model.api.profile.ModelProfile;
import kr.toxicity.model.api.tracker.PlayerTracker;
import kr.toxicity.model.api.tracker.TrackerUpdateAction;
import kr.toxicity.model.api.util.function.BonePredicate;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.*;
import org.bukkit.plugin.java.JavaPlugin;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class ArmorSystem implements Listener {
private final JavaPlugin plugin;
private final Map<UUID, PlayerTracker> trackers = new ConcurrentHashMap<>();
public ArmorSystem(JavaPlugin plugin) {
this.plugin = plugin;
}
@EventHandler
public void onJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
if (hasCustomArmor(player)) {
applyCustomArmor(player);
}
}
private void applyCustomArmor(Player player) {
BetterModel.limb("custom_armor").ifPresent(renderer -> {
PlayerTracker tracker = (PlayerTracker) renderer.getOrCreate(
BaseEntity.of(player),
ModelProfile.of(player)
);
trackers.put(player.getUniqueId(), tracker);
updateArmorParts(player, tracker);
tracker.animate("idle");
});
}
private void updateArmorParts(Player player, PlayerTracker tracker) {
boolean hasHelmet = player.getInventory().getHelmet() != null;
boolean hasChestplate = player.getInventory().getChestplate() != null;
boolean hasLeggings = player.getInventory().getLeggings() != null;
boolean hasBoots = player.getInventory().getBoots() != null;
tracker.update(TrackerUpdateAction.togglePart(hasHelmet), BonePredicate.name("helmet"));
tracker.update(TrackerUpdateAction.togglePart(hasChestplate), BonePredicate.name("chestplate"));
tracker.update(TrackerUpdateAction.togglePart(hasLeggings), BonePredicate.name("leggings"));
tracker.update(TrackerUpdateAction.togglePart(hasBoots), BonePredicate.name("boots"));
}
@EventHandler
public void onQuit(PlayerQuitEvent event) {
PlayerTracker tracker = trackers.remove(event.getPlayer().getUniqueId());
if (tracker != null) {
tracker.close();
}
}
private boolean hasCustomArmor(Player player) {
// Your logic to check if player should have custom armor
return player.hasPermission("customarmor.use");
}
}
Next Steps
Player Cosmetics
Learn about cosmetic items and accessories
Multi-part Entities
Create complex multi-model entities
Performance Optimization
Optimize model rendering for large servers
API Reference
Full Tracker API documentation
