Skip to main content
Learn how to create custom armor models that can replace default armor rendering or add cosmetic armor overlays.

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

1

Create Player Model

Your BlockBench model must be configured as a player model and placed in BetterModel/players/ directory.
2

Setup Armor Bones

Name your armor bones according to conventions:
  • helmet, chestplate, leggings, boots
  • Or use bone tags for more flexibility

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

Build docs developers (and LLMs) love