Skip to content

Java Library Creating an Entity

jellymods edited this page Sep 19, 2026 · 1 revision

Creating an Entity

A Marionette mob is three classes and two registrations. This walks the worm example (net.jelly.marionette_lib.entity.examples.worm), which is a ten-segment chain that reaches for the nearest player.

If you rigged the mob in Blockbench, the plugin writes all three classes for you (see Exporting Java) and this page is then just a description of what you got. The only real useful section then is the last one on driving the limbs.

1. The entity

Implement Marionette, build your limbs in the constructor, and drive them in tick().

public class WormEntity extends WaterAnimal implements Marionette {
    private final Limb<MarionettePart<WormEntity>> tail;

    public WormEntity(EntityType entityType, Level level) {
        super(entityType, level);
        tail = Limb.builder(this)
                .segments(10, 0.5f, 0.5f, 2f / 8)
                .build();
    }

    @Override
    public List<Limb<?>> getLimbs() {
        return List.of(tail);
    }

    @Override
    public void tick() {
        super.tick();
        Player nearest = this.level().getNearestPlayer(this, 200);
        if (nearest != null) tail.animator().setFabrikTarget(nearest.position());
        tickMarionette();   // solves every limb and ticks every part
    }
}

Forge's Entity already defines concrete versions of three methods, so the interface defaults lose to them and you have to forward them by hand (copy directly):

@Override
public boolean isMultipartEntity() {
    return true;
}

@Override
public PartEntity<?>[] getParts() {
    return getMarionetteParts();
}

@Override
public void remove(RemovalReason reason) {
    super.remove(reason);
    removeMarionette(reason);
}

Set the entity's own sized(...) hitbox to cover the root only. The parts have their own hitboxes.

2. The model

Extend MarionetteModel<YourEntity> and hand the superclass the segment names in part order: index i in the array is driven by part i of your limbs, in the order getLimbs() returns them.

public class WormModel extends MarionetteModel<WormEntity> {
    public static LayerDefinition createBodyLayer() {
        MeshDefinition mesh = new MeshDefinition();
        PartDefinition root = mesh.getRoot();

        root.addOrReplaceChild("bone",
                CubeListBuilder.create().texOffs(0, 0)
                        .addBox(-1.0F, -1.0F, -2.5F, 2.0F, 2.0F, 5.0F, new CubeDeformation(0.0F)),
                PartPose.offset(0.0F, 24.0F, 0.0F));
        // ... one per segment

        return LayerDefinition.create(mesh, 32, 32);
    }

    public WormModel(ModelPart root) {
        super(root, new String[] { "bone", "bone2", /* ... */ "bone10" });
    }
}

3. The renderer

A regular MobRenderer with two overrides:

public class WormRenderer extends MobRenderer<WormEntity, WormModel> {
    private static final ResourceLocation TEXTURE =
            new ResourceLocation("yourmodid:textures/entity/worm.png");
    public static final ModelLayerLocation WORM_LAYER =
            new ModelLayerLocation(new ResourceLocation("yourmodid", "worm_layer"), "main");

    public WormRenderer(EntityRendererProvider.Context context) {
        super(context, new WormModel(context.bakeLayer(WORM_LAYER)), 1.0f);
    }

    @Override
    protected void setupRotations(WormEntity entity, PoseStack poseStack, float ageInTicks,
                                  float rotationYaw, float partialTicks) {
        // segment positions are already world-space, so the entity's own yaw must not be
        // applied on top. the 180 turn matches Blockbench's +Z forward.
        poseStack.mulPose(Axis.YP.rotationDegrees(180));
    }

    @Override
    public boolean shouldRender(WormEntity entity, Frustum camera, double x, double y, double z) {
        // a limb can reach far outside the entity's own box; without this the whole
        // model pops out of view when the root leaves the frustum.
        if (super.shouldRender(entity, camera, x, y, z)) return true;
        return camera.isVisible(entity.getMarionetteBoundingBoxForCulling(entity));
    }

    @Override
    public ResourceLocation getTextureLocation(WormEntity entity) {
        return TEXTURE;
    }
}

4. Registration

Standard for any Java Entity:

@SubscribeEvent
public static void registerLayer(EntityRenderersEvent.RegisterLayerDefinitions event) {
    event.registerLayerDefinition(WormRenderer.WORM_LAYER, WormModel::createBodyLayer);
}

@SubscribeEvent
public static void registerRenderers(EntityRenderersEvent.RegisterRenderers event) {
    event.registerEntityRenderer(ModEntities.WORM.get(), WormRenderer::new);
}

Driving the limbs

Basically the entirety of the interface for most applications is setFabrikTarget. Most of the character comes from how you pick the target and root positions. The examples in the repo cover the range:

Example Pattern
worm one chain, target = nearest player
octopus two chains, each easing toward a random reachable goal and picking a new one on arrival
wyvern a followRootOnly body chain, plus four legs whose roots are pinned to body segments and which step when a foot drifts too far from its resting spot, creating a crawling motion

Two additional tools for shaping a solve:

  • animator().setRoot(v) pins the chain's root somewhere other than the entity's position, e.g. a shoulder, or another limb's segment, as the wyvern's legs do.
  • animator().primeMultipart(dir) lays the chain out straight along dir before a solve, biasing which way it folds. For example, priming an arm backwards is how you keep the elbow from bending the wrong way. It has no effect after the solve, since the next tick recomputes every direction. This is incredibly useful and is a way of imposing pseudo-constraints on the joints, and will receive a Blockbench plugin feature for visualization soon.

Clone this wiki locally