Everyone knows that butterflies come from caterpillars. The next stage in the life cycle is for butterfly eggs to hatch into caterpillars. So we will add caterpillars to our mod that will hatch from the eggs we created last time.
Modelling a Caterpillar
Whenever I want to create a custom model for Minecraft, I start by using Blockbench. Blockbench is a tool that allows you to create simple models for use primarily in Minecraft. You can create “bones’ by using folders, and add cubes to each folder to help separate any animation you might want to add later. In this case we have the root
bone where we have attached the thorax, and added head
and abdomen
bones that we can use to animate the caterpillar later.
It also allows you to create a default texture. You can then either modify this texture directly, or you can save it to use as a reference later. We want to create 16 caterpillar types so we will save it for later use.
We can also export the model. Since entity models in Minecraft are hardcoded, there is no data format to export to. What BlockBench does instead is generate a java class that can be used as a base for your model.
// Made with Blockbench 4.8.1 // Exported for Minecraft version 1.17 or later with Mojang mappings // Paste this class into your mod and generate all required imports public class Caterpillar<T extends Entity> extends EntityModel<T> { // This layer location should be baked with EntityRendererProvider.Context in the entity renderer and passed into this model's constructor public static final ModelLayerLocation LAYER_LOCATION = new ModelLayerLocation(new ResourceLocation("modid", "caterpillar"), "main"); private final ModelPart root; public Caterpillar(ModelPart root) { this.root = root.getChild("root"); } public static LayerDefinition createBodyLayer() { MeshDefinition meshdefinition = new MeshDefinition(); PartDefinition partdefinition = meshdefinition.getRoot(); PartDefinition root = partdefinition.addOrReplaceChild("root", CubeListBuilder.create().texOffs(0, 9).addBox(-3.0F, -5.0F, -6.0F, 6.0F, 5.0F, 4.0F, new CubeDeformation(0.0F)) .texOffs(20, 9).addBox(-5.0F, -7.0F, -2.0F, 10.0F, 7.0F, 0.0F, new CubeDeformation(0.0F)), PartPose.offset(0.0F, 24.0F, 0.0F)); PartDefinition head = root.addOrReplaceChild("head", CubeListBuilder.create().texOffs(18, 23).addBox(-2.0F, -3.0F, -8.0F, 4.0F, 3.0F, 2.0F, new CubeDeformation(0.0F)) .texOffs(20, 16).addBox(-5.0F, -7.0F, -6.0F, 10.0F, 7.0F, 0.0F, new CubeDeformation(0.0F)), PartPose.offset(0.0F, 0.0F, 0.0F)); PartDefinition abdomen = root.addOrReplaceChild("abdomen", CubeListBuilder.create().texOffs(0, 0).addBox(-3.0F, -4.0F, -2.0F, 6.0F, 4.0F, 5.0F, new CubeDeformation(0.0F)) .texOffs(0, 18).addBox(-5.0F, -7.0F, 3.0F, 10.0F, 7.0F, 0.0F, new CubeDeformation(0.0F)) .texOffs(22, 0).addBox(-2.0F, -3.0F, 3.0F, 4.0F, 3.0F, 5.0F, new CubeDeformation(0.0F)), PartPose.offset(0.0F, 0.0F, 0.0F)); return LayerDefinition.create(meshdefinition, 64, 64); } @Override public void setupAnim(T entity, float limbSwing, float limbSwingAmount, float ageInTicks, float netHeadYaw, float headPitch) { } @Override public void renderToBuffer(PoseStack poseStack, VertexConsumer vertexConsumer, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) { root.render(poseStack, vertexConsumer, packedLight, packedOverlay, red, green, blue, alpha); } }
It will always need modifying to suit your needs, but it saves having to visualise the cubes generated by the code yourself.
Rendering a Caterpillar
We take the model class generated by BlockBench, and modify it slightly so it fits with our needs. Mainly we use a HierarchicalModel
and clean up the formatting a little. We also save off a few model parts that we will use for animation later.
/** * A model of a caterpillar. */ public class CaterpillarModel extends HierarchicalModel<Caterpillar> { // Holds the layers for the model. public static final ModelLayerLocation LAYER_LOCATION = new ModelLayerLocation(new ResourceLocation(ButterfliesMod.MODID, "caterpillar"), "main"); // The root of the model. private final ModelPart root; private final ModelPart head; private final ModelPart body; private final ModelPart thorax; /** * Construction * @param root The root of the model. */ public CaterpillarModel(ModelPart root) { this.root = root; this.body = root.getChild("body"); this.head = this.body.getChild("head"); this.thorax = this.body.getChild("thorax"); } /** * Creates a model of a caterpillar. * @return The layer definition of the model. */ public static LayerDefinition createBodyLayer() { MeshDefinition meshdefinition = new MeshDefinition(); PartDefinition partdefinition = meshdefinition.getRoot(); PartDefinition body = partdefinition.addOrReplaceChild("body", CubeListBuilder.create() .texOffs(0, 9).addBox(-3.0F, -5.0F, -6.0F, 6.0F, 5.0F, 4.0F, new CubeDeformation(0.0F)) .texOffs(20, 9).addBox(-5.0F, -7.0F, -2.0F, 10.0F, 7.0F, 0.0F, new CubeDeformation(0.0F)), PartPose.offset(0.0F, 24.0F, 0.0F)); body.addOrReplaceChild("head", CubeListBuilder.create() .texOffs(18, 23).addBox(-2.0F, -3.0F, -8.0F, 4.0F, 3.0F, 2.0F, new CubeDeformation(0.0F)) .texOffs(20, 16).addBox(-5.0F, -7.0F, -6.0F, 10.0F, 7.0F, 0.0F, new CubeDeformation(0.0F)), PartPose.offset(0.0F, 0.0F, 0.0F)); body.addOrReplaceChild("thorax", CubeListBuilder.create() .texOffs(0, 0).addBox(-3.0F, -4.0F, -2.0F, 6.0F, 4.0F, 5.0F, new CubeDeformation(0.0F)) .texOffs(0, 18).addBox(-5.0F, -7.0F, 3.0F, 10.0F, 7.0F, 0.0F, new CubeDeformation(0.0F)) .texOffs(22, 0).addBox(-2.0F, -3.0F, 3.0F, 4.0F, 3.0F, 5.0F, new CubeDeformation(0.0F)), PartPose.offset(0.0F, 0.0F, 0.0F)); return LayerDefinition.create(meshdefinition, 64, 64); } /** * Get the root of the model. * @return The root ModelPart */ @Override public @NotNull ModelPart root() { return this.root; } }
We can then create a renderer for this model. The renderer code for caterpillars is pretty simple and very similar to ButterflyRenderer
.
/** * The renderer for the caterpillar entity. */ public class CaterpillarRenderer extends MobRenderer<Caterpillar, CaterpillarModel> { /** * Bakes a new model for the renderer * @param context The current rendering context */ public CaterpillarRenderer(EntityRendererProvider.Context context) { super(context, new CaterpillarModel(context.bakeLayer(CaterpillarModel.LAYER_LOCATION)), 0.05F); } /** * Gets the texture to use * @param entity The butterfly entity * @return The texture to use for this entity */ @Override public @NotNull ResourceLocation getTextureLocation(@NotNull Caterpillar entity) { return entity.getTexture(); } /** * Scale the entity down * @param entity The butterfly entity * @param poses The current entity pose * @param scale The scale that should be applied */ @Override protected void scale(@NotNull Caterpillar entity, PoseStack poses, float scale) { float s = entity.getScale(); poses.scale(s, s, s); } }
Finally, we need an entity. The entity is closely modelled on the Butterfly
, however there are no overrides for fall damage, since caterpillars can’t fly.
/** * Creates the Caterpillar behaviour. */ public class Caterpillar extends AmbientCreature { // The unique IDs that are used to reference a butterfly entity. public static final String MORPHO_NAME = "morpho_caterpillar"; public static final String FORESTER_NAME = "forester_caterpillar"; public static final String COMMON_NAME = "common_caterpillar"; public static final String EMPEROR_NAME = "emperor_caterpillar"; public static final String HAIRSTREAK_NAME = "hairstreak_caterpillar"; public static final String RAINBOW_NAME = "rainbow_caterpillar"; public static final String HEATH_NAME = "heath_caterpillar"; public static final String GLASSWING_NAME = "glasswing_caterpillar"; public static final String CHALKHILL_NAME = "chalkhill_caterpillar"; public static final String SWALLOWTAIL_NAME = "swallowtail_caterpillar"; public static final String MONARCH_NAME = "monarch_caterpillar"; public static final String CABBAGE_NAME = "cabbage_caterpillar"; public static final String ADMIRAL_NAME = "admiral_caterpillar"; public static final String LONGWING_NAME = "longwing_caterpillar"; public static final String BUCKEYE_NAME = "buckeye_caterpillar"; public static final String CLIPPER_NAME = "clipper_caterpillar"; // Holds a flag that is set to TRUE if a player placed the butterfly. // entity. private static final EntityDataAccessor<Boolean> DATA_PERSISTENT = SynchedEntityData.defineId(Caterpillar.class, EntityDataSerializers.BOOLEAN); // The name of the "respawned" attribute in the save data. private static final String PERSISTENT = "butterflyPlacedByPlayer"; // Helper constant to modify speed private static final double CATERPILLAR_SPEED = 0.00325d; // The location of the texture that the renderer should use. private final ResourceLocation texture; // The current position the caterpillar is moving toward. @Nullable private Vec3 targetPosition; /** * Supplies attributes for the butterfly, in this case just 1 points of * maximum health (0.5 hearts). * @return The butterfly attribute supplier. */ public static AttributeSupplier.Builder createAttributes() { return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 1d); } /** * Create a Morpho butterfly * @param entityType The type of the entity. * @param level The current level. * @return A newly constrycted butterfly. */ @NotNull public static Caterpillar createMorphoCaterpillar(EntityType<? extends Caterpillar> entityType, Level level) { return new Caterpillar("caterpillar_blue.png", entityType, level); } /** * Create a Forester butterfly * @param entityType The type of the entity. * @param level The current level. * @return A newly constrycted butterfly. */ @NotNull public static Caterpillar createForesterCaterpillar(EntityType<? extends Caterpillar> entityType, Level level) { return new Caterpillar("caterpillar_nyan.png", entityType, level); } /** * Create a Common butterfly * @param entityType The type of the entity. * @param level The current level. * @return A newly constrycted butterfly. */ @NotNull public static Caterpillar createCommonCaterpillar(EntityType<? extends Caterpillar> entityType, Level level) { return new Caterpillar("caterpillar_birdwing.png", entityType, level); } /** * Create an Emperor butterfly * @param entityType The type of the entity. * @param level The current level. * @return A newly constrycted butterfly. */ @NotNull public static Caterpillar createEmperorCaterpillar(EntityType<? extends Caterpillar> entityType, Level level) { return new Caterpillar("caterpillar_purple.png", entityType, level); } /** * Create a Hairstreak butterfly * @param entityType The type of the entity. * @param level The current level. * @return A newly constrycted butterfly. */ @NotNull public static Caterpillar createHairstreakCaterpillar(EntityType<? extends Caterpillar> entityType, Level level) { return new Caterpillar("caterpillar_purple_trim.png", entityType, level); } /** * Create a Rainbow butterfly * @param entityType The type of the entity. * @param level The current level. * @return A newly constrycted butterfly. */ @NotNull public static Caterpillar createRainbowCaterpillar(EntityType<? extends Caterpillar> entityType, Level level) { return new Caterpillar("caterpillar_rainbow.png", entityType, level); } /** * Create a Heath butterfly * @param entityType The type of the entity. * @param level The current level. * @return A newly constrycted butterfly. */ @NotNull public static Caterpillar createHeathCaterpillar(EntityType<? extends Caterpillar> entityType, Level level) { return new Caterpillar("caterpillar_red.png", entityType, level); } /** * Create a Glasswing butterfly * @param entityType The type of the entity. * @param level The current level. * @return A newly constrycted butterfly. */ @NotNull public static Caterpillar createGlasswingCaterpillar(EntityType<? extends Caterpillar> entityType, Level level) { return new Caterpillar("caterpillar_seethru.png", entityType, level); } /** * Create a Chalkhill butterfly * @param entityType The type of the entity. * @param level The current level. * @return A newly constrycted butterfly. */ @NotNull public static Caterpillar createChalkhillCaterpillar(EntityType<? extends Caterpillar> entityType, Level level) { return new Caterpillar("caterpillar_sword.png", entityType, level); } /** * Create a Swallowtail butterfly * @param entityType The type of the entity. * @param level The current level. * @return A newly constrycted butterfly. */ @NotNull public static Caterpillar createSwallowtailCaterpillar(EntityType<? extends Caterpillar> entityType, Level level) { return new Caterpillar("caterpillar_white.png", entityType, level); } /** * Create a Monarch butterfly * @param entityType The type of the entity. * @param level The current level. * @return A newly constrycted butterfly. */ @NotNull public static Caterpillar createMonarchCaterpillar(EntityType<? extends Caterpillar> entityType, Level level) { return new Caterpillar("caterpillar_monarch.png", entityType, level); } /** * Create a Cabbage butterfly * @param entityType The type of the entity. * @param level The current level. * @return A newly constrycted butterfly. */ @NotNull public static Caterpillar createCabbageCaterpillar(EntityType<? extends Caterpillar> entityType, Level level) { return new Caterpillar("caterpillar_cabbage.png", entityType, level); } /** * Create an Admiral butterfly * @param entityType The type of the entity. * @param level The current level. * @return A newly constrycted butterfly. */ @NotNull public static Caterpillar createAdmiralCaterpillar(EntityType<? extends Caterpillar> entityType, Level level) { return new Caterpillar("caterpillar_admiral.png", entityType, level); } /** * Create a Longwing butterfly * @param entityType The type of the entity. * @param level The current level. * @return A newly constrycted butterfly. */ @NotNull public static Caterpillar createLongwingCaterpillar(EntityType<? extends Caterpillar> entityType, Level level) { return new Caterpillar("caterpillar_longwing.png", entityType, level); } /** * Create a Clipper butterfly * @param entityType The type of the entity. * @param level The current level. * @return A newly constrycted butterfly. */ @NotNull public static Caterpillar createClipperCaterpillar(EntityType<? extends Caterpillar> entityType, Level level) { return new Caterpillar("caterpillar_clipper.png", entityType, level); } /** * Create a Buckeye butterfly * @param entityType The type of the entity. * @param level The current level. * @return A newly constrycted butterfly. */ @NotNull public static Caterpillar createBuckeyeCaterpillar(EntityType<? extends Caterpillar> entityType, Level level) { return new Caterpillar("caterpillar_buckeye.png", entityType, level); } /** * Create a caterpillar entity. * @param entityType The entity type. * @param level The level we are creating the entity in. */ protected Caterpillar(String texture, EntityType<? extends AmbientCreature> entityType, Level level) { super(entityType, level); this.texture = new ResourceLocation("caterpillars:textures/entity/caterpillar/" + texture); } /** * Used to add extra parameters to the entity's save data. * @param tag The tag containing the extra save data. */ @Override public void addAdditionalSaveData(@NotNull CompoundTag tag) { super.addAdditionalSaveData(tag); tag.putBoolean(PERSISTENT, this.entityData.get(DATA_PERSISTENT)); } /** * Overrides how an entity handles triggers such as tripwires and pressure * plates. Caterpillars aren't heavy enough to trigger either. * @return Always TRUE, so caterpillars ignore block triggers. */ @Override public boolean isIgnoringBlockTriggers() { return true; } /** * Override this to control if an entity can be pushed or not. Caterpillars * can't be pushed by other entities. * @return Always FALSE, so caterpillars cannot be pushed. */ @Override public boolean isPushable() { return false; } /** * Override to read any additional save data. * @param tag The tag containing the entity's save data. */ @Override public void readAdditionalSaveData(@NotNull CompoundTag tag) { super.readAdditionalSaveData(tag); // Read the placed-by-player flag if it exists. if (tag.contains(PERSISTENT)) { this.entityData.set(DATA_PERSISTENT, tag.getBoolean(PERSISTENT)); } } /** * Override to stop an entity despawning. Caterpillars that have been placed * by a player won't despawn. * @return TRUE if we want to prevent despawning. */ @Override public boolean requiresCustomPersistence() { return this.entityData.get(DATA_PERSISTENT) || super.requiresCustomPersistence(); } /** * Sets the placed-by-player flag to true to prevent the butterfly * despawning. */ public void setPersistent() { entityData.set(DATA_PERSISTENT, true); } /** * Override to define extra data to be synced between server and client. */ @Override protected void defineSynchedData() { super.defineSynchedData(); this.entityData.define(DATA_PERSISTENT, false); } /** * Override to change how pushing other entities affects them. Caterpillars * don't push other entities. * @param otherEntity The other entity pushing/being pushed. */ @Override protected void doPush(@NotNull Entity otherEntity) { // No-op } /** * Override to control what kind of movement events the entity will emit. * Caterpillars will not emit sounds. * @return Movement events only. */ @NotNull @Override protected MovementEmission getMovementEmission() { return MovementEmission.EVENTS; } /** * Reduce the size of the caterpillar - they are small! * @return The new size of the caterpillar. */ @Override public float getScale() { return 0.1f; } /** * Override to control an entity's relative volume. Caterpillars are silent. * @return Always zero, so caterpillars are silent. */ @Override protected float getSoundVolume() { return 0.0f; } /** * Override to set the entity's eye height. * @param pose The current pose of the entity. * @param dimensions The dimensions of the entity. * @return The height of the entity's eyes. */ @Override protected float getStandingEyeHeight(@NotNull Pose pose, EntityDimensions dimensions) { return dimensions.height / 2.0f; } /** * Get the texture to use for rendering. * @return The resource location of the texture. */ public ResourceLocation getTexture() { return texture; } /** * Override to change how pushing other entities affects them. Caterpillars * don't push other entities. */ @Override protected void pushEntities() { // No-op } }
Finally we can register the caterpillar’s entity types, add spawn eggs, and localisation strings, in the same way we have done with the butterfly entity previously.
Animating a Caterpillar
We can now spawn caterpillars into the world, but they are quite boring and static. To give them some life we can add a bit of motion to them. The first thing we want to do is create a simple looping animation. We use a sine wave to generate a simple loop, and we normalise the value so it is between 1 and 0 (instead of 1 and -1).
We use this value to move the body up and the tail toward the middle. This creates a “wormlike” animation which looks more cartoony than realistic, but gives our caterpillars some nice movement.
/** * Animate the model * @param entity The entity * @param limbSwing Unused * @param limbSwingAmount Unused * @param ageInTicks The current age of the entity in ticks * @param netHeadYaw unused * @param headPitch unused */ @Override public void setupAnim(@NotNull Caterpillar entity, float limbSwing, float limbSwingAmount, float ageInTicks, float netHeadYaw, float headPitch) { // Use a sine wave based on the current time. float sin = (float)Math.sin(ageInTicks * 0.2f); // Normalise the sine wave so it is between 0.0f and 1.0f. float ymod = 0.5f * (sin + 1.f); this.head.y = ymod; this.body.y = 24.0f - ymod; this.thorax.y = ymod; this.thorax.z = -1.0f - sin; }
It’s still a little boring just wiggling while standing still. We want these things to actually move! To do this, we add some movement behaviour using customServerAiStep
.
The code first picks a random target position based on the current block the caterpillar is placed. It then updates its velocity and orientation based on the target position. By using Math.floor()
and basing the new target position on the current position, we guarantee that the caterpillar never moves away from the block it is spawned upon.
/** * A custom step for the AI update loop. */ @Override protected void customServerAiStep() { super.customServerAiStep(); // Set a new target position if: // 1. We don't have one already // 2. After a 1/30 random chance if (this.targetPosition == null || this.random.nextInt(30) == 0) { if (this.targetPosition == null) { this.targetPosition = this.position(); } this.targetPosition = new Vec3(Math.floor(this.targetPosition.x()) + (this.random.nextDouble() % 1.0), this.getY(), Math.floor(this.targetPosition.z()) + (this.random.nextDouble() % 1.0)); } // Calculate an updated movement delta. double dx = this.targetPosition.x() + 0.1d - this.getX(); double dz = this.targetPosition.z() + 0.1d - this.getZ(); Vec3 deltaMovement = this.getDeltaMovement(); Vec3 updatedDeltaMovement = deltaMovement.add( (Math.signum(dx) * 0.5d - deltaMovement.x) * CATERPILLAR_SPEED, 0.0, (Math.signum(dz) * 0.5d - deltaMovement.z) * CATERPILLAR_SPEED); this.setDeltaMovement(updatedDeltaMovement); this.zza = 0.5f; // Calculate the rotational velocity. double yRot = (Mth.atan2(updatedDeltaMovement.z, updatedDeltaMovement.x) * (180.0d / Math.PI)) - 90.0d; double yRotDelta = Mth.wrapDegrees(yRot - this.getYRot()); this.setYRot(this.getYRot() + (float)yRotDelta); }
Hatching a Caterpillar
Last time we created butterfly eggs that could be planted in leaves. Now that we have caterpillars implemented, we can have these eggs actually hatch. To implement this behaviour we need to ensure that the block will update on a random tick.
/** * We will need to random tick so that eggs will spawn caterpillars * @param blockState The current block state. * @return Always TRUE. */ @Override public boolean isRandomlyTicking(@NotNull BlockState blockState) { return true; }
Now we can override randomTick()
and implement a chance that the egg will hatch each time. We simply have a random chance each time we get a random tick that a caterpillar will hatch. When this happens, we call removeButterflyEgg()
, which is the same as plantButterflyEgg()
except it reverts the block back to a normal leaves block.
After all this is done we call the parent’s randomTick()
method, but only if the super class would randomly tick anyway.
/** * After a certain amount of time, a caterpillar will spawn. * @param blockState The current block state. * @param level The current level. * @param position The position of the block. * @param random The random number generator. */ @Override public void randomTick(@NotNull BlockState blockState, @NotNull ServerLevel level, @NotNull BlockPos position, @NotNull RandomSource random) { if (random.nextInt(15) == 0) { if (level.isEmptyBlock(position.above())) { Caterpillar.spawn(level, ButterflyIds.IndexToEntityId(blockState.getValue(BUTTERFLY_INDEX)), position.above()); removeButterflyEgg(level, position); } } // Only run the super's random tick if it would have ticked anyway. if (super.isRandomlyTicking(blockState)) { super.randomTick(blockState, level, position, random); } }
As we did with the butterfly, we hold the spawn code for the caterpillar in it’s entity class. All we do is try and create the entity based on the entityId, and spawn it into the world.
/** * Spawns a caterpillar into the world. * @param entityId The type of butterfly to release. * @param position The current position of the player. */ public static void spawn(ServerLevel level, String entityId, BlockPos position) { ResourceLocation key = new ResourceLocation(ButterfliesMod.MODID, entityId + "_caterpillar"); EntityType<?> entityType = ForgeRegistries.ENTITY_TYPES.getValue(key); if (entityType != null) { Entity entity = entityType.create(level); if (entity instanceof Caterpillar caterpillar) { caterpillar.moveTo(position.getX() + 0.45D, position.getY() + 0.2D, position.getZ() + 0.5D, 0.0F, 0.0F); caterpillar.finalizeSpawn(level, level.getCurrentDifficultyAt(position), MobSpawnType.NATURAL, null, null); level.addFreshEntity(caterpillar); } } }
Texturing a Caterpillar
The final step here is to add new textures for each caterpillar type. We are basing the textures on actual caterpillars, so there will be 16 different varieties – one for each species. Players will eventually be able to tell what species each caterpillar comes from just by looking at them.
We don’t use a pixel editing software. We probably should, but I’ve gotten used to using GIMP. We create a layer using the texture we exported from BlockBench, and create a new texture in a layer above it. In doing this we can keep all caterpillar textures in a single file, and export to a new texture as needed.
This probably isn’t the best method for pixel art textures – there is software (such as Aseprite) that is designed specifically for pixel art. But it’s working for now, so we’ll stick with it until we need something more efficient.
WIP
This feature isn’t quite finished yet. We still need to tidy up some code and create textures for all species. Functionally everything works – we just have some Is to dot and Ts to cross. The next feature we will look at will be the chrysalis. Caterpillars will eventually build one, then burst out as a butterfly, thus completing the life cycle.
After that we will release v1.0 of our mod to the world. There are more surprises to come with this mod as v2.0 will introduce a surprising new feature that has me very excited.