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.