Chrysalis: Completing the Cycle

We finally implement the chrysalis and complete the life cycle of the butterfly. Butterflies lay eggs. Eggs hatch into caterpillars. Caterpillars create a chrysalis. Butterflies hatch out of a chrysalis. The circle of life is complete.

Within this article I will be referencing only the interesting parts of the code and leaving out most of the boiler plate. The full change can be viewed in the mod’s repository.

Model and Renderer

I decided to implement the chrysalis as an entity rather than a block, as it meant that I could reuse a lot of the behaviour from the caterpillar. This also means that it needs a model and a renderer as with all entities.

Since chrysalises (is “chrysalises” the correct plurilisation?) don’t move, we don’t use a HeirarchicalModel as the base for our model. Instead all we need is an EntityModel.

/**
 * Model for a chrysalis entity.
 */
public class ChrysalisModel extends EntityModel<Chrysalis> {
    // Code Here
}

For the model I used two planes intersecting each other, similar to how grass and flowers are modelled in vanilla.

    /**
     * Create a simple model for the chrysalis.
     * @return The new layer definition.
     */
    public static LayerDefinition createBodyLayer() {
        MeshDefinition meshdefinition = new MeshDefinition();
        PartDefinition partdefinition = meshdefinition.getRoot();

        partdefinition.addOrReplaceChild("main",
                CubeListBuilder.create()
                        .texOffs(0, 0).addBox(-4.0F, -16.0F, 0.0F, 8.0F, 16.0F, 0.0F, new CubeDeformation(0.0F))
                        .texOffs(0, -8).addBox(0.0F, -16.0F, -4.0F, 0.0F, 16.0F, 8.0F, new CubeDeformation(0.0F)),
                        PartPose.offset(0.0F, 24.0F, 0.0F));

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

The renderer copies the rotation code from the caterpillar to ensure that the chrysalis is facing in the right direction.

    /**
     * Rotates the caterpillar so it's attached to its block.
     * @param entity The caterpillar entity.
     * @param p_115456_ Unknown.
     * @param p_115457_ Unknown.
     * @param poseStack The posed model to render.
     * @param multiBufferSource The render buffer.
     * @param p_115460_ Unknown.
     */
    @Override
    public void render(@NotNull Chrysalis entity,
                       float p_115456_,
                       float p_115457_,
                       @NotNull PoseStack poseStack,
                       @NotNull MultiBufferSource multiBufferSource,
                       int p_115460_) {
        Direction direction = entity.getSurfaceDirection();
        if (direction == Direction.UP) {
            poseStack.mulPose(Axis.XP.rotationDegrees(180.f));
        } else if (direction == Direction.NORTH) {
            poseStack.mulPose(Axis.XP.rotationDegrees(90.f));
        } else if (direction == Direction.SOUTH) {
            poseStack.mulPose(Axis.XP.rotationDegrees(-90.f));
        } else if (direction == Direction.WEST) {
            poseStack.mulPose(Axis.ZP.rotationDegrees(-90.f));
        } else if (direction == Direction.EAST){
            poseStack.mulPose(Axis.ZP.rotationDegrees(90.f));
        }

        super.render(entity, p_115456_, p_115457_, poseStack, multiBufferSource, p_115460_);
    }

Other than this, there isn’t anything special about the model and renderer. It’s a simple model, with no animation so nothing special is needed.

Directional Creature

By using an entity I wanted to reuse code from the caterpillar in the chrysalis entity. To do this I created the DirectionalCreature as a base class of the Caterpillar. This class is abstract so it can only be used as a base class, and I pulled all the code for DIRECTION, PERSISTANT and SURFACE_BLOCK storage into this class, as well as the texture information.

This class also adds an AGE tag that will be used to complete the cycle later. The age of the entity will increase by one each tick (see customServerAiStep() below), and will be used as a timer to determine when a caterpillar or chrysalis moves on to the next phase.

/**
 * The code below shows only the relevant implementation details for the new AGE
 *  tag. The full class can be seen in the GitHub repo.
 */
public abstract class DirectionalCreature extends AmbientCreature {

    // Serializers for data stored in the save data.
    protected static final EntityDataAccessor<Integer> DATA_AGE =
            SynchedEntityData.defineId(DirectionalCreature.class, EntityDataSerializers.INT);

    // Names of the attributes stored in the save data.
    protected static final String AGE = "age";

    /**
     * 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.putInt(AGE, this.entityData.get(DATA_AGE));
    }

    /**
     * Get the age of the entity.
     * @return The age.
     */
    protected int getAge() {
        return this.entityData.get(DATA_AGE);
    }

    /**
     * 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);

        // Get the age.
        if (tag.contains(AGE)) {
            this.entityData.set(DATA_AGE, tag.getInt(AGE));
        }
    }

    @Override
    protected void customServerAiStep() {
        super.customServerAiStep();
        this.setAge(this.getAge() + 1);
    }

    /**
     * Construction
     * @param texture The location of the texture used to render the entity.
     * @param entityType The entity's type.
     * @param level The current level.
     */
    protected DirectionalCreature(String texture,
                                  EntityType<? extends AmbientCreature> entityType,
                                  Level level) {
        super(entityType, level);
        this.texture = new ResourceLocation(ButterfliesMod.MODID, texture);
    }

    /**
     * Override to define extra data to be synced between server and client.
     */
    @Override
    protected void defineSynchedData() {
        super.defineSynchedData();
        this.entityData.define(DATA_AGE, 0);
    }
    /**
     * Set the age of the entity.
     * @param age The new age.
     */
    private void setAge(int age) {
        this.entityData.set(DATA_AGE, age);
    }
}

Chrysalis

The Chrysalis class handles the behaviour of the chrysalis. It doesn’t do much, but there are still some things we need to implement. It inherits from the DirectionalCreature class to get the behaviour we want to share with Caterpillar. We have the usual constants for each species’ name and their respective static methods for creating new instances. We also have the usual overrides for making sure that the chrysalis doesn’t trigger pressure plates and so on.

When we spawn a chrysalis we set it’s position and direction based on the current position and direction of the caterpillar that made it.

   /**
     * Spawns a chrysalis into the world.
     *
     * @param entityId The type of chrysalis to release.
     * @param position The current position of the player.
     */
    @SuppressWarnings({"deprecation", "OverrideOnly"})
    public static void spawn(ServerLevel level,
                             String entityId,
                             BlockPos position,
                             Direction direction,
                             float yRotation) {

        ResourceLocation key = new ResourceLocation(entityId + "_chrysalis");

        EntityType<?> entityType = ForgeRegistries.ENTITY_TYPES.getValue(key);
        if (entityType != null) {
            Entity entity = entityType.create(level);
            if (entity instanceof Chrysalis chrysalis) {
                BlockPos spawnPosition;
                if (direction == Direction.DOWN) {
                    spawnPosition = position.below();
                } else if (direction == Direction.UP) {
                    spawnPosition = position.above();
                } else if (direction == Direction.NORTH) {
                    spawnPosition = position.north();
                } else if (direction == Direction.SOUTH) {
                    spawnPosition = position.south();
                } else if (direction == Direction.WEST) {
                    spawnPosition = position.west();
                } else {
                    spawnPosition = position.east();
                }

                chrysalis.moveTo(position, 0.0F, 0.0F);
                chrysalis.setYRot(yRotation);
                chrysalis.setSurfaceDirection(direction);
                chrysalis.setSurfaceBlock(spawnPosition);

                chrysalis.finalizeSpawn(level,
                        level.getCurrentDifficultyAt(position),
                        MobSpawnType.NATURAL,
                        null,
                        null);

                level.addFreshEntity(chrysalis);
            }
        }
    }

A chrysalis will always ignore gravity, and we overload the isNoGravity() method to implement this.

    /**
     * Chrysalises ignore gravity.
     */
    @Override
    public boolean isNoGravity() {
        return true;
    }

Instead, a chrysalis will simply die if its surface block is destroyed.

    /**
     * A custom step for the AI update loop.
     */
    @Override
    protected void customServerAiStep() {
        super.customServerAiStep();

        // If the surface block is destroyed then the chrysalis dies.
        if (this.level().isEmptyBlock(getSurfaceBlock())) {
            kill();
        }
    }

Completing the Cycle

Now that we have a chrysalis, all we have left to do is to complete the life cycle of the butterfly. We start by adding a random chance for a caterpillar to morph into a chrysalis after a set amount of time.

    /**
     * A custom step for the AI update loop.
     */
    @Override
    protected void customServerAiStep() {
        super.customServerAiStep();

        {
            // Spawn Chrysalis.
            if (this.getAge() > 24000 && this.random.nextInt(0, 15) == 0) {
                String encodeId = this.getEncodeId();
                if (encodeId != null) {
                    String[] splitEncodeId = encodeId.split("_");
                    Chrysalis.spawn((ServerLevel) this.level(), splitEncodeId[0], this.blockPosition(),
                                    this.getSurfaceDirection(), this.getYRot());
                    this.remove(RemovalReason.DISCARDED);
                }
            }
        }
    }

Finally we add similar code to the Chrysalis, only this will spawn a butterfly in its place instead.

    /**
     * A custom step for the AI update loop.
     */
    @Override
    protected void customServerAiStep() {
        super.customServerAiStep();

        // If the surface block is destroyed then the chrysalis dies.
        if (this.level().isEmptyBlock(getSurfaceBlock())) {
            kill();
        }

        // Spawn Butterfly.
        if (this.getAge() > 24000 && this.random.nextInt(0, 15) == 0) {
            String encodeId = this.getEncodeId();
            if (encodeId != null) {
                String[] splitEncodeId = encodeId.split("_");
                Butterfly.spawn(this.level(), splitEncodeId[0], this.blockPosition(), false);
                this.remove(RemovalReason.DISCARDED);
            }
        }
    }

I had to modify the butterfly’s spawn() method slightly to get this to work, but internally it still does the same things.

Mod Complete?

Not quite. While they are functionally there, I still need to create textures for each of the 16 chrysalises. There are also a couple of minor details I want to take another look at, such as the persistence of the butterflies (which isn’t consistent or logical right now), and whether butterflies should actually be AgeableMobs instead of AmbientCreatures.

It’s almost ready to release. It just needs a bit more polish first.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.