Butterflies Part 1: Entities

I’m developing my own version of a Butterfly mod for Minecraft. Naturally, one of the first features I want to implement are the butterflies themselves. To do this we need to add new entities to the game, along with models and renderers for the entity, and define how the entities will spawn in the game. The following is a step-by step of how I added butterfly entities to my mod, but can also serve as a tutorial for adding entities with Forge.

All of the code for this change can be found in my Github Repository.

Entity

An entity in Minecraft is essentially anything that isn’t a block or a particle. Items, Experience Orbs, Projectiles, even Lightning Bolts are all examples of entities. Players and Mobs are known as Living Entities. There are several different kinds of Mob, including Flying Mobs and Pathfinding Mobs.

The Butterfly class defines our butterfly entity. The mob’s behaviour will be similar to the bat’s in many ways, so we derive the class from the same base as the bat – AmbientCreature:

import net.minecraft.world.entity.ambient.AmbientCreature;

/**
 * The butterfly entity that flies around the world, adding some ambience.
 */
public class Butterfly extends AmbientCreature {

}

There is quite a bit in this class, so I will just go through the code and explain along the way what it is doing.

The Basics

    // The unique ID that is used to reference a butterfly entity.
    public static final String NAME = "butterfly";

This is a the ID of the entity. It will define how we reference it in other areas of code, data packs, console commands, and so on. For example, the ID for a cow entity in Minecraft is “minecraft:cow”. The ID will be referenced using our mod name, which is “butterflies”. So our full entity name will be “butterflies:butterfly”.

    /**
     * The default constructor.
     * @param entityType The type of the entity.
     * @param level The level where the entity exists.
     */
    public Butterfly(EntityType<? extends Butterfly> entityType,
                     Level level) {
        super(entityType, level);
    }

In the constructor we need to make sure we call the super constructor. Apart from the class name in the generic, an entity’s constructor will usually look the same in every entity.

    /**
     * Override to control an entity's relative volume. Butterflies are silent.
     * @return Always zero, so butterflies are silent.
     */
    @Override
    protected float getSoundVolume() {
        return 0.0f;
    }

Butterflies are silent so we set their volume to zero.

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

This defines the height of the entity’s eyes. For butterflies it isn’t really important so we just set it to half their height.

Entity Data

    // Holds a flag that is set to TRUE if a player placed the butterfly.
    // entity.
    private static final EntityDataAccessor<Boolean> DATA_PLACED_BY_PLAYER =
            SynchedEntityData.defineId(Butterfly.class, EntityDataSerializers.BOOLEAN);

    // Holds an integer that represents the variant of the butterfly.
    private static final EntityDataAccessor<Integer> DATA_VARIANT =
            SynchedEntityData.defineId(Butterfly.class, EntityDataSerializers.INT);

    // The name of the "respawned" attribute in the save data.
    private static final String PLACED_BY_PLAYER = "butterflyPlacedByPlayer";

    // The name of the "variant" attribute in the save data.
    private static final String VARIANT = "butterflyVariant";

The entity data accessors are used to define extra data to be stored and saved with an entity. In this case we define two data accessors: one will tell us if a player has placed a butterfly. This will be used with upcoming features to prevent despawning of butterflies the player wants to keep. The second defines the variant of the butterfly which will determine what the butterfly will look like.

    /**
     * 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(VARIANT, this.entityData.get(DATA_VARIANT));
        tag.putBoolean(PLACED_BY_PLAYER, this.entityData.get(DATA_PLACED_BY_PLAYER));
    }

    /**
     * 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 variant (species) of the butterfly if it exists.
        if (tag.contains(VARIANT)) {
            this.entityData.set(DATA_VARIANT, tag.getInt(VARIANT));
        }

        // Read the placed-by-player flag if it exists.
        if (tag.contains(PLACED_BY_PLAYER)) {
            this.entityData.set(DATA_PLACED_BY_PLAYER, tag.getBoolean(PLACED_BY_PLAYER));
        }
    }

These two methods add and read the extra data to the game save. Minecraft will only save certain data about each entity, so if we want any extra data to be saved we need to tell the game what to save. By doing this for butterflies, we can save their variant so they will look the same when we return to a world.

    /**
     * Override to define extra data to be synced between server and client.
     */
    @Override
    protected void defineSynchedData() {
        super.defineSynchedData();
        this.entityData.define(DATA_VARIANT, 0);
        this.entityData.define(DATA_PLACED_BY_PLAYER, false);
    }

Minecraft will also only synchronise specific data over a network game. This method allows us to add extra data to be synchronised to other players. By doing this, we can guarantee butterflies will look the same for all players in a multiplayer game.

    /**
     * Override to stop an entity despawning. Butterflies 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_PLACED_BY_PLAYER)
                || super.requiresCustomPersistence();
    }

This method controls whether or not an entity should remain in the world or despawn. By default, butterflies will despawn if a player moves too far away. However, if a player places a butterfly they will remain in the world.

    /**
     * Get the variant (species) of this butterfly.
     * @return The index representing this butterfly's species.
     */
    public int getVariant() {
        return this.entityData.get(DATA_VARIANT);
    }

    /**
     * Sets the placed-by-player flag to true to prevent the butterfly
     * despawning.
     */
    public void setPlacedByPlayer() {
        entityData.set(DATA_PLACED_BY_PLAYER, true);
    }

    /**
     * Sets the variant (species) of this butterfly.
     * @param variant The variant of this butterfly.
     */
    public void setVariant(int variant) {
        entityData.set(DATA_VARIANT, variant);
    }

Finally we define some accessors/mutators for the entity data. These will be used by other classes to decide how to render the butterflies.

Spawning

    /**
     * Checks custom rules to determine if the entity can spawn.
     * @param entityType The type of the entity to spawn.
     * @param level The level/world to spawn the entity into.
     * @param spawnType The type of spawn happening.
     * @param position The position to spawn the entity into.
     * @param rng The global random number generator.
     * @return TRUE if the butterfly can spawn.
     */
    public static boolean checkButterflySpawnRules(@SuppressWarnings("unused") EntityType<Butterfly> entityType,
                                                   ServerLevelAccessor level,
                                                   @SuppressWarnings("unused") MobSpawnType spawnType,
                                                   BlockPos position,
                                                   @SuppressWarnings("unused") RandomSource rng) {
        return level.getRawBrightness(position, 0) > 8;
    }

When an entity attempts to spawn it will call a method to check if the spawn location is valid. We define a static method that we will register for the spawn system to use when a butterfly attempts to spawn. In this case we will allow a butterfly to spawn if the light level is higher than 8.

Since this is Java, we cannot use the same name as super classes for this method (i.e. “checkSpawnRules”), so we give it a more explicit name.

I’ve suppressed some “unused” warnings here so that they won’t output anything during compilation. This helps us to focus on other warnings that may actually impact the code.

    /**
     * Supplies attributes for the butterfly, in this case just 3 points of
     * maximum health (1.5 hearts).
     * @return The butterfly attribute supplier.
     */
    public static AttributeSupplier.Builder createAttributes() {
        return Mob.createMobAttributes().add(Attributes.MAX_HEALTH, 3d);
    }

All entities need attributes registered to be able to spawn and we use this method to do that. Butterflies just get the basic mob attributes, and we set their health to 3 (1.5 hearts).

    // The maximum number of butterfly variants.
    private static final int MAX_VARIANTS = 10;

    /**
     * Used to finalise an entity's data after spawning. This will set the butterfly to a random variant.
     * @param level The level the entity is spawning into.
     * @param difficulty The difficulty of the level.
     * @param spawnType The type of spawn happening.
     * @param spawnGroupData The group data for the spawn.
     * @param tag The data tag for the entity.
     * @return Updated group data for the entity.
     */
    @Nullable
    @Override
    @SuppressWarnings( {"deprecation", "OverrideOnly"} )
    public SpawnGroupData finalizeSpawn(@NotNull ServerLevelAccessor level,
                                        @NotNull DifficultyInstance difficulty,
                                        @NotNull MobSpawnType spawnType,
                                        @Nullable SpawnGroupData spawnGroupData,
                                        @Nullable CompoundTag tag) {
        setVariant(this.random.nextInt(MAX_VARIANTS));
        return super.finalizeSpawn(level, difficulty, spawnType, spawnGroupData, tag);
    }

When an entity has spawned completely, the game will call this method for any last minute changes that need to be made to the mob. In this case we are setting the butterfly to a random variant.

Due to some funkiness with Forge/Minecraft this method throws both deprecated and OverrideOnly warnings, however this is the valid way of doing this. In order to keep my compilation output clean I use annotations to suppress these warnings.

Movement

    /**
     * The main update loop for the entity.
     */
    @Override
    public void tick() {
        super.tick();

        //  Reduce the vertical movement to keep the butterfly close to the
        //  same height.
        this.setDeltaMovement(this.getDeltaMovement().multiply(1.0d, 0.6d, 1.0d));
    }

Tick is the entity’s update loop. We could do anything we want in this method, but for the butterfly we just adjust its movement. We reduce the vertical movement to 60% so that it will appear to fly in a more horizontal path most of the time.

    // The position the butterfly is flying towards. Butterflies can spawn
    // anywhere the light level is above 8.
    @Nullable private BlockPos targetPosition;

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

        // Check the current move target is still an empty block.
        if (this.targetPosition != null && (!this.level.isEmptyBlock(this.targetPosition) || this.targetPosition.getY() <= this.level.getMinBuildHeight())) {
            this.targetPosition = null;
        }

        // Set a new target position if:
        //  1. We don't have one already
        //  2. After a 1/30 random chance
        //  3. We get too close to the current target position
        if (this.targetPosition == null || this.random.nextInt(30) == 0 || this.targetPosition.closerToCenterThan(this.position(), 2.0d)) {
            this.targetPosition = new BlockPos((int) this.getX() + this.random.nextInt(7) - this.random.nextInt(7),
                                               (int) this.getY() + this.random.nextInt(6) - 2,
                                               (int) this.getZ() + this.random.nextInt(7) - this.random.nextInt(7));
        }

        // Calculate an updated movement delta.
        double dx = this.targetPosition.getX() + 0.5d - this.getX();
        double dy = this.targetPosition.getY() + 0.1d - this.getY();
        double dz = this.targetPosition.getZ() + 0.5d - this.getZ();

        Vec3 deltaMovement = this.getDeltaMovement();
        Vec3 updatedDeltaMovement = deltaMovement.add((Math.signum(dx) * 0.5d - deltaMovement.x) * 0.1d,
                                                      (Math.signum(dy) * 0.7d - deltaMovement.y) * 0.1d,
                                                      (Math.signum(dz) * 0.5d - deltaMovement.z) * 0.1d);
        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);
    }

This method is the entity’s chance to think. Generally anything related to AI should go in here. We use it to determine where the butterfly is trying to fly to.

Collisions

    /**
     * Overrides how fall damage is applied to the entity. Butterflies ignore
     * all fall damage.
     * @param fallDistance The distance fallen.
     * @param blockModifier The damage modifier for the block landed on.
     * @param damageSource The source of the damage.
     * @return Always FALSE, as no damage is applied.
     */
    @Override
    public boolean causeFallDamage(float fallDistance,
                                   float blockModifier,
                                   @NotNull DamageSource damageSource) {
        return false;
    }

    /**
     * Overrides how an entity handles triggers such as tripwires and pressure
     * plates. Butterflies aren't heavy enough to trigger either.
     * @return Always TRUE, so butterflies ignore block triggers.
     */
    @Override
    public boolean isIgnoringBlockTriggers() {
        return true;
    }

    /**
     * Override this to control if an entity can be pushed or not. Butterflies
     * can't be pushed by other entities.
     * @return Always FALSE, so butterflies cannot be pushed.
     */
    @Override
    public boolean isPushable() {
        return false;
    }

    /**
     * Override to control how an entity checks for fall damage. In this case
     * butterflies just ignore the check.
     * @param yPos The current height of the entity.
     * @param onGround TRUE if the entity is on the ground.
     * @param blockState The state of the block just below the entity.
     * @param position The entity's current position.
     */
    @Override
    protected void checkFallDamage(double yPos,
                                   boolean onGround,
                                   @NotNull BlockState blockState,
                                   @NotNull BlockPos position) {
        //No-op
    }

    /**
     * Override to change how pushing other entities affects them. Butterflies
     * don't push other entities.
     * @param otherEntity The other entity pushing/being pushed.
     */
    @Override
    protected void doPush(@NotNull Entity otherEntity) {
        // No-op
    }

    /**
     * Override to change how pushing other entities affects them. Butterflies
     * don't push other entities.
     */
    @Override
    protected void pushEntities() {
        // No-op
    }

I won’t go through each of these methods individually as they are pretty much all doing the same thing. Basically we don’t want butterflies to collide with any other entities or to take fall damage, so we override these methods to disable collisions for butterflies.

Events

    // The number of ticks per flap. Used for event emissions.
    private static final int TICKS_PER_FLAP = Mth.ceil(2.4166098f);

    /**
     * Controls when a flapping event should be emitted.
     * @return TRUE when a flapping event should be emitted.
     */
    @Override
    protected boolean isFlapping() {
        return this.tickCount % TICKS_PER_FLAP == 0;
    }

This simple piece of code just controls how often a “flapping” event is emitted to Minecraft. I’m not really sure if this is needed – it’s just copy/pasted from the Bat entity.

    /**
     * Override to control what kind of movement events the entity will emit.
     * Butterflies will not emit sounds.
     * @return Movement events only.
     */
    @NotNull
    @Override
    protected MovementEmission getMovementEmission() {
        return MovementEmission.EVENTS;
    }

This controls what kind of events the butterfly will emit. Butterflies will not emit any sound events, so we override this method to control that.

Next Step

There are several more steps to getting our butterfly into the game. We need to add a model, textures, spawn rules, and so on. This article is getting long, so I plan to cover the rest of this in an upcoming article.

For now we have a base entity class that defines the butterfly’s behaviours and attributes. This is perhaps the most complicated part of the process as we will see in Part 2.

One thought on “Butterflies Part 1: Entities

Comments are closed.