I got a couple of bug reports about butterfly movement recently. Some were stuck floating in the sky, others wouldn’t animate properly when moving. So I took some time this week to clean up the movement and landing code to make it more robust.
Invalid Landing Blocks
One of the first problems I spotted was that the check for valid landing blocks doesn’t actually check if there is an empty block next to it. This is a problem, since the code could theoretically select a landing block surrounded by a solid block.
To fix this, I updated the isValidTarget()
method in the land on block goal to check for empty space for the butterfly to land as well.
/** * Tells the base goal which blocks are valid targets. * @param levelReader Gives access to the level. * @param blockPos The block position to check. * @return TRUE if the block is a valid target. */ @Override protected boolean isValidTarget(@NotNull LevelReader levelReader, @NotNull BlockPos blockPos) { if (this.butterfly.isValidLandingBlock(levelReader.getBlockState(blockPos))) { for (Direction d : Direction.values()) { if (levelReader.isEmptyBlock(blockPos.relative(d))) { return true; } } } return false; }
I also added a check in isReachedTarget()
to ensure the current block the butterfly is in is empty:
/** * Overrides to return TRUE if any valid block is below the butterfly. * @return TRUE if the butterfly can land. */ protected boolean isReachedTarget() { Level level = this.butterfly.level(); BlockPos position = this.butterfly.blockPosition(); if (level.isEmptyBlock(position)) { // Code that checks each direction for a landing block goes here. } }
While fixing valid landing checks was a start, it didn’t solve the more visible problem: butterflies were still ending up inside their landing blocks.
Sinking Butterflies
One of the problems being reported was that butterflies would sink into leaf blocks and stay hidden inside. This obviously wasn’t what I intended when I rewrote the code for butterfly landings, so it warranted investigation.
When I spawned a butterfly on a leaf block it would land and set its position properly. All good so far. But when I spawned a butterfly some distance away from a landing block, it would fly to the block as expected, then its position would be set somewhere inside the landing block.
I managed to get this to reproduce every single time, so clearly this was a major bug. The problem was that the code attempts to guess the position the butterfly should be based on its current position:
/** * Set whether the butterfly has landed. * @param landed TRUE if the butterfly has landed. */ public void setLanded(boolean landed) { // Don't repeat this otherwise the butterflies fall if (!this.getIsLanded() && landed) { switch (this.getLandedDirection()) { case DOWN -> this.setPos(this.getX(), Math.floor(this.getY()), this.getZ()); case UP -> this.setPos(this.getX(), Math.floor(this.getY()) + 0.9, this.getZ()); case NORTH -> this.setPos(this.getX(), this.getY(), Math.floor(this.getZ())); case SOUTH -> this.setPos(this.getX(), this.getY(), Math.floor(this.getZ()) + 0.9); case WEST -> this.setPos(Math.floor(this.getX()), this.getY(), this.getZ()); case EAST -> this.setPos(Math.floor(this.getX()) + 0.9, this.getY(), this.getZ()); } } entityData.set(DATA_LANDED, landed); }
This approach assumes the butterfly’s current position is close enough to predict where it will land. But that assumption fails when it has momentum. The butterfly’s position isn’t static, leading to this predicted position being incorrect most of the time.
I needed a static, predictable position to base this code on, and I already have one: the position of the landing block. I rewrote the setLanded()
method to take in the block’s position, and based the butterfly’s position using an offset of that instead:
/** * Set a butterfly to landed. * @param landingBlockPosition The position of the block the butterfly has landed on. */ public void setLanded(BlockPos landingBlockPosition) { switch (this.getLandedDirection()) { case DOWN -> this.setPos(this.getX(), landingBlockPosition.getY() + 1.1, this.getZ()); case UP -> this.setPos(this.getX(), landingBlockPosition.getY() - 0.1, this.getZ()); case NORTH -> this.setPos(this.getX(), this.getY(), landingBlockPosition.getZ() + 1.1); case SOUTH -> this.setPos(this.getX(), this.getY(), landingBlockPosition.getZ() - 0.1); case WEST -> this.setPos(landingBlockPosition.getX() + 1.1, this.getY(), this.getZ()); case EAST -> this.setPos(landingBlockPosition.getX() - 0.1, this.getY(), this.getZ()); } entityData.set(DATA_LANDED, true); }
I also removed the check that prevented the butterfly’s position from being set more than once. Now it can correct itself even if something goes wrong after the initial landing.
Of course, now that setLanded()
always sets the landed state to true
, I had to provide a method to reset the flag as well:
/** * Set a butterfly to not landed. */ public void setNotLanded() { entityData.set(DATA_LANDED, false); }
I also updated the tick()
method of the goal so that it will always run, and allow the butterfly to take off if the target block becomes invalid (i.e. a player breaks it).
/** * Update the butterfly after it has landed. */ @Override public void tick() { if (this.isReachedTarget()) { --this.tryTicks; this.mob.getNavigation().stop(); this.butterfly.setLanded(this.blockPos); } else { ++this.tryTicks; this.butterfly.setNotLanded(); if (this.shouldRecalculatePath()) { moveMobToBlock(); } } }
After testing with this code, butterflies seem to always land on blocks in the correct position, and no longer sink into leaves. It’s much more robust and deterministic now, but there’s still potential that I may have missed something.
“Landed Wanderers”
Another reported problem was that butterflies would be in their “landed” animations while flying around. I had noticed this, but thought it only occurred with butterflies already in the world. After the first time they landed, the problem fixed itself, so I didn’t think it would be a major issue.
Two people reported this bug.
What’s worse, they were reporting it for butterflies that were newly spawned in the world. This was a bigger problem than I thought.
When a butterfly leaves the land on block goal, I call setNotLanded()
to ensure the butterfly is in the correct state. Unfortunately, if it doesn’t run through this code, the flag can be set incorrectly and won’t get reset until the next time they rest. If players constantly skip the night, which is common for players of Minecraft, butterflies may never get the chance to rest and will never fix themselves.
The solution I settled on was to reset the flag every time a butterfly enters into its wander goal. I just add an override to the start()
method that calls setNotLanded()
before it does anything else:
/** * Fix to force the animation into the "not landed" state. */ @Override public void start() { this.butterfly.setNotLanded(); super.start(); }
With these fixes in place, butterfly movement and animation should now feel far more natural and true to life. No more motionless drones hovering mid-air.