Custom Bone Behaviors

A bone behavior is extra per-bone logic that hooks into the model's calculation and render pipeline. Model Engine ships several built-in behaviors (HEAD, MOUNT, ITEM, LEASH, etc. — see Configure Bone Behavior), but you can add your own. A custom behavior has two parts: the BoneBehavior implementation that runs the logic, and a BlockbenchBehaviorParser that reads its configuration off a .bbmodel bone so the behavior gets attached during model generation.

Creating a behavior class

All behaviors implement BoneBehavior, but you will usually extend AbstractBoneBehavior instead, which stores the ModelBone, the BoneBehaviorType, and the parsed BoneBehaviorData for you (exposed via Lombok getters getBone(), getType(), getData()). Override only the pipeline callbacks you care about — every method on BoneBehavior has a default no-op implementation.

public class GlowBehavior extends AbstractBoneBehavior<GlowBehavior> {

    private final boolean enabled;

    public GlowBehavior(ModelBone bone, BoneBehaviorType<GlowBehavior> type, BoneBehaviorData data) {
        super(bone, type, data);
        // Read the parsed config off the data map. The typed overload returns the default if absent/mistyped.
        this.enabled = data.get("enabled", false);
    }

    @Override
    public void onModelInitialized() {
        // Called once the model is fully built
    }

    @Override
    public void postTransformDecompose() {
        // Mutate the bone's transform after it has been decomposed, e.g.
        // bone.getGlobalTransform()...
    }

    @Override
    public boolean isHidden() {
        return !enabled; // Hide this bone's render output
    }

}

Pipeline callbacks

BoneBehavior exposes a large set of ordered hooks that fire as each bone is processed every tick, including preAnimation() / onAnimation() / postAnimation(), the global-calculation hooks (preGlobalCalculation(), preGlobalRotate() / postGlobalRotate(), preGlobalScale() / postGlobalScale(), and so on), postTransformDecompose() / postTransformRecompose(), and the render hooks preRender() / onRender() / postRender(). There are also lifecycle hooks (onApply(), onRemove(), onModelInitialized(), onParentSwap(ModelBone parent), onFinalize()) and two value-returning hooks: onUpdateYaw(float yaw) and isHidden(). Pick the hook whose timing matches what you need; the built-in HeadImpl, for example, applies its rotation in postTransformDecompose() and postGlobalRotate().

Reading configuration data

BoneBehaviorData wraps the validated argument map. Use get(String key) for a nullable lookup or get(String key, T def) for a typed lookup with a fallback default. The keys available here are exactly the arguments your BoneBehaviorType declared as required(...) / optional(...) — Model Engine validates types and strips unknown keys before the data ever reaches your constructor.

Defining the behavior type

A BoneBehaviorType ties your behavior class to an id and declares its arguments. Build one with BoneBehaviorType.Builder.of(provider, managerProvider, id) and register it on the BoneBehaviorRegistry. The provider is a constructor reference matching (ModelBone, BoneBehaviorType, BoneBehaviorData) -> BoneBehavior; the manager provider may be null if your behavior does not need one.

// On plugin start-up
BoneBehaviorType<GlowBehavior> glowType = BoneBehaviorType.Builder
        .of(GlowBehavior::new, null, "glow")
        .optional("enabled", Boolean.class)
        .build();

ModelEngineAPI.getAPI().getBoneBehaviorRegistry().register(glowType);

Creating a parser

Configuration is read off the .bbmodel during model generation by a BlockbenchBehaviorParser. Its processBone method runs once per bone; write your parsed arguments into bone.getBehaviors(), a Map<String, Map<String, Object>> keyed by behavior id. Model Engine later validates that inner map against your type's declared arguments and compiles it into the BoneBehaviorData handed to your behavior. processModel runs once per model and is for whole-model concerns (the default parser uses it for hitbox and shadow groups).

public class GlowBehaviorParser implements BlockbenchBehaviorParser {

    @Override
    public void processModel(ErrorCollector collector, BlockbenchModel model, ModelBlueprint blueprint) {
        // Optional: whole-model parsing. Leave empty if unused.
    }

    @Override
    public void processBone(ErrorCollector collector, BlockbenchModel model, BlockbenchModel.Group group, BlueprintBone bone) {
        // Attach the "glow" behavior to bones whose name starts with "glow_"
        if (bone.getName().startsWith("glow_")) {
            var options = new HashMap<String, Object>();
            options.put("enabled", true);
            bone.getBehaviors().put("glow", options);
        }
    }

}

The behavior id you put into bone.getBehaviors() must match the id you passed to BoneBehaviorType.Builder.of(...), and the inner-map keys must match the arguments you declared on the type.

Registering the parser

Parsers are registered by listening for RegisterBehaviorParserEvent and calling event.register(parser). This event is fired once when the Blockbench parser is constructed, so register from a Bukkit listener.

public class BehaviorParserListener implements Listener {

    @EventHandler
    public void onRegister(RegisterBehaviorParserEvent event) {
        event.register(new GlowBehaviorParser());
    }

}

Register the listener as usual on plugin start-up:

Bukkit.getPluginManager().registerEvents(new BehaviorParserListener(), plugin);

With the type registered and the parser hooked in, any bone your parser tags will have your behavior attached the next time its model is generated.

Updated Aug 19, 2026