Bone Hitboxes

Bone hitboxes let an entity be hit per body part. Each hitbox follows a bone in your model, rotates with it, and stays in sync with whatever animation is playing, because it is read from the same bone matrices the model is drawn with.

Quick start

Implement AzHitboxHolder on your entity and return a static AzHitboxSet:

public class XenoEntity extends Monster implements AzHitboxHolder {
private static final AzHitboxSet HITBOXES = AzHitboxSet.builder()
// Fitted to the head bone's cubes, double damage
.add("head", h -> h.damageMultiplier(2f).tag("weak_point"))
.add("body")
// Second hitbox name on a bone, explicit size in pixels
.add("tail_tip", "tail4", h -> h.size(4, 4, 10).inflate(1))
.build();
@Override
public AzHitboxSet getHitboxSet() {
return HITBOXES;
}
}

That is all that is required. The entity must be rendered with an AzEntityRenderer. Press F3 + B to see the hitboxes: red is above 1x damage, blue is below 1x, green is 1x, cyan is an interaction hitbox and yellow is an anchor.

Defining a hitbox

All lengths are Blockbench pixels and offsets use Blockbench's axes, so values can be read straight from the model.

MethodEffect
size(x, y, z) / size(s)Explicit size. Without it the box fits the cubes owned by the bone (not its children).
offset(x, y, z)Moves the box from its default center.
inflate(pixels)Grows or shrinks every side.
anchorAtPivot()Centers on the bone pivot instead of the center of the bone's cubes.
damageMultiplier(f)Used by the default onHitboxHit.
tag(String)Free-form tags for your own logic.

A hitbox is disabled while its bone (or a parent hiding its children) is hidden, or scaled to zero.

Roles

Every hitbox has a role that decides which systems use it:

RoleCrosshairProjectilesonHitboxHitonHitboxInteractQueries
COMBAT (default)✓✓✓✓
INTERACT✓✓✓
ANCHOR✓

Set it with .interact(), .anchor() or .role(AzHitboxRole). The crosshair always lands on the nearest pickable box, so a combat hitbox in front of an interaction hitbox blocks the click, the same way a wall would.

Interaction hitboxes

Right-clicking an INTERACT hitbox calls onHitboxInteract on both sides, before the entity's normal mobInteract. Return anything other than PASS to consume the click:

.add("saddlebag", "sled", h -> h.interact().size(10, 8, 12).anchorAtPivot().offset(18, -6, 14))
@Override
public InteractionResult onHitboxInteract(AzHitboxHit hit, Player player, InteractionHand hand) {
if (hit.name().equals("saddlebag")) {
if (!level().isClientSide()) {
openSaddlebag(player);
}
return InteractionResult.sidedSuccess(level().isClientSide());
}
return InteractionResult.PASS;
}

The client tells the server which hitbox it clicked, and the server checks it against reach, so small or fast-moving interaction hitboxes resolve reliably. This works with clientOnly() sets too. Attacking an interaction hitbox deals normal damage with no hitbox attached.

Anchor hitboxes

An ANCHOR hitbox is never hit by anything; it only tracks a bone so your own logic can read its position:

.add("mouth", "lowJaw", h -> h.anchor().size(2))
var mouth = AzHitboxes.getWorldBox(this, "mouth");
if (mouth != null) {
var bite = mouth.inflate(0.5);
for (var target : level().getEntitiesOfClass(LivingEntity.class, bite.bounds(), e -> e != this)) {
if (bite.intersects(target.getBoundingBox())) {
target.hurt(damageSources().mobAttack(this), 8f);
}
}
}

To find an exact point on a bone (a muzzle, a hand) rather than a box, use a bone socket. Sockets use the bone directly on the client and an anchor hitbox on the server; AzBoneSocket#toAnchorHitbox() builds the matching anchor for you.

Give anchors a non-zero size; a box with no size is treated as empty. On the server, anchors follow the pose reported by nearby clients, so they trail the animation by a few ticks and are unavailable when no player is near the mob. Keep a fallback for that case, and time server-side checks slightly after the keyframe they belong to.

Reacting to hits

onHitboxHit runs on the server at the start of LivingEntity#hurt and returns the damage to apply:

@Override
public float onHitboxHit(AzHitboxHit hit, DamageSource source, float amount) {
if (hit.hasTag("weak_point")) {
this.playSound(SoundEvents.SHIELD_BREAK);
}
return amount * hit.hitbox().damageMultiplier();
}

It runs before vanilla's invulnerability checks, so treat it as a damage modifier. Inside your own hurt override, AzHitboxes.getLastHit(this) returns the same hit for the rest of the tick.

After the damage is applied

onHitboxDamaged runs on the server once vanilla has accepted the damage, at the end of LivingEntity#actuallyHurt. Hits that are blocked by invulnerability frames or isInvulnerableTo never reach it, and it runs at most once per hit, so use it for anything that should only change when the entity actually took damage:

@Override
public void onHitboxDamaged(AzHitboxHit hit, DamageSource source, float amount) {
if (hit.hasTag("armor_plate")) {
crackPlate(hit.name(), amount);
}
}

amount is the damage onHitboxHit returned (or, during invulnerability frames, the part above the previous hit), before armor and absorption. The entity's health has already been reduced, so isDeadOrDying() tells you whether the hit killed it. Limb Health builds on this hook.

Querying hitboxes

AzHitboxes works on either side:

  • raycast(entity, from, to) returns the nearest hitbox hit.
  • getIntersecting(entity, aabb) returns every hitbox touching a box (for sweeps and area attacks).
  • getWorldBox(entity, name) and getActiveHitboxes(entity) return world-space boxes (for particles, AI targeting).
  • raycast, getIntersecting and getActiveHitboxes also take a Predicate<AzHitbox>, e.g. AzHitbox::isPickable or h -> h.role() == AzHitboxRole.COMBAT.
  • hasLivePose(entity) tells you whether hitboxes are currently active.

Custom projectiles and hitscan

AzureLib hooks vanilla's projectile collision (ProjectileUtil.getEntityHitResult), which arrows, tridents, thrown items and fireballs all use. Projectiles that do their own collision, such as many modded bullets, and hitscan weapons with no projectile entity, never go through it. Against hitbox entities they hit the vanilla box and never trigger onHitboxHit or onHitboxDamaged.

To give them hitbox support, do two things in your own hit check:

  1. Test the shot against the target's hitboxes with AzHitboxes.raycast, instead of (or as well as) its bounding box.
  2. Right before calling hurt, pass the hit to AzHitboxDamageTracker.recordProjectileHit. This is how hurt finds out which hitbox was hit.

This helper does both, and treats misses the same way vanilla projectiles do:

/**
* Returns where a shot from {@code from} to {@code to} hits {@code target}, or empty if it misses. If it hits a
* hitbox, the hit is recorded against {@code direct} for the next {@code hurt} call.
*/
public static Optional<Vec3> clipWithHitboxes(Entity target, Entity direct, Vec3 from, Vec3 to) {
var boxHit = target.getBoundingBox().clip(from, to);
if (
!(target instanceof AzHitboxHolder holder)
|| AzHitboxes.getActiveHitboxes(target, AzHitbox::blocksProjectiles).isEmpty()
) {
return boxHit; // No live hitboxes: use the vanilla box.
}
var hit = AzHitboxes.raycast(target, from, to, AzHitbox::blocksProjectiles);
var keepEntityBox = holder.getHitboxSet().keepEntityBox();
if (hit.isEmpty()) {
return keepEntityBox ? boxHit : Optional.empty();
}
var location = hit.get().location();
if (keepEntityBox && boxHit.isPresent() && from.distanceToSqr(boxHit.get()) < from.distanceToSqr(location)) {
return boxHit; // The vanilla box is closer: a plain body hit.
}
if (!target.level().isClientSide()) {
AzHitboxDamageTracker.recordProjectileHit(direct, hit.get());
}
return Optional.of(location);
}

Then hurt the target straight away, in the same tick:

if (clipWithHitboxes(target, this, from, to).isPresent()) {
// thrown(direct, causing): the projectile is the direct entity. Your own damage type works the same way.
target.hurt(damageSources().thrown(this, getOwner()), damage);
}

direct must be the entity the damage source names as its direct entity (DamageSource#getDirectEntity), because that's what hurt looks the hit up by:

  • Projectile entities: pass the projectile itself, and build the damage source with the projectile as the direct entity, as vanilla does.
  • Hitscan weapons: there's no projectile, so pass the shooter, and use a damage source whose direct entity is the shooter.

A recorded hit is used once and expires after a tick, so record it right before hurt rather than earlier in the projectile's life.

Some more things to check in your own collision code:

  • Hitboxes can stick out past the vanilla box. When searching for entities that a shot might hit, inflate the search area by AzHitboxes.maxOverhang(), or entities whose hitboxes stick into the shot's path will be skipped.
  • Use AzHitbox::blocksProjectiles as the filter, so shots pass through INTERACT and ANCHOR hitboxes like vanilla projectiles do.
  • On the server, hitboxes are only live while a nearby client reports the entity's pose (see below). Without one, clipWithHitboxes falls back to the vanilla box.

How syncing works

Animations only run on the client, so the server has no bone positions of its own.

  1. Client: after the model renders, each hitbox is read from its bone's matrix. Crosshair targeting and client-side projectile checks use these boxes.
  2. Melee: when you attack, the client tells the server which hitbox the crosshair was on. The server checks it against reach before using it.
  3. Server: clients that render the entity report its pose every few ticks. The server listens to the closest one, rejects out-of-range values, and uses the pose for projectiles and reach checks.

Entities a nearby player isn't looking at still report. Each tick, the client poses any hitbox entity within 64 blocks that it didn't render, by running its animator and computing bone positions without drawing anything, and reports that pose when one is due. Only entities that use an AzEntityRenderer get this.

If no client has reported recently (no player within 64 blocks of the mob), hit detection falls back to the vanilla bounding box.

Trust

A modified client can move a mob's hitboxes, but only within maxReach of the entity and up to maxHalfExtent in size. That only changes where projectiles land on that mob. If that is not acceptable for your entity, call clientOnly(): melee hitboxes still work, but projectiles use the vanilla box.

Set options

MethodDefaultEffect
keepEntityBox(true)falseKeep the vanilla box hittable alongside the hitboxes (as a plain body hit).
clientOnly()offDisable pose reports to the server.
reportInterval(ticks)2How often clients report a pose.
poseTimeout(ticks)10When a pose is considered stale.
maxReach(blocks)max(4, 3 * max(width, height))Furthest a hitbox center may be from the entity.
maxHalfExtent(blocks)4Largest accepted hitbox half size.

Large entities

If hitboxes stick far outside the vanilla box (long tails or necks), the game can cull the entity while part of it is on screen, which also stops the hitboxes updating. Grow the culling box:

@Override
public AABB getBoundingBoxForCulling() {
return AzHitboxes.getCullingBox(this, super.getBoundingBoxForCulling());
}