Last week I released version 4 of Bok’s Banging Butterflies, introducing flower pollination and improved AI for butterflies. With that done, it’s time to start focusing on the next feature I want to introduce. Moths will be main feature for Bok’s Banging Butterflies v5.0.0.
But first, a different kind of bug appears.
The Great Butterfly Escape
I released version 4 this week and, of course, it didn’t take long for someone to report a bug. I got a comment by OtaPic on CurseForge saying that bottled butterflies were escaping. Well damn. I basically spent the last month rewriting the movement code for butterflies. And it broke the mod.
I loaded up the game and started placing bottles. Lots of bottles. Just as the user said, it seemed around 40% of the butterflies would escape. Worse than that, I discovered that if I left the world and loaded back in, 40% of the remaining butterflies would also escape. This was bad.
I experimented with removing goals and restricting movement if they were in bottles. But the code I was writing felt hacky, and introduced some new problems. Then I remembered a minor change I had made to the butterflies last month. I had been messing around with their bounding boxes.
I loaded into the test world again and used F3 + B to show bounding boxes. Sure enough you could see their bounding boxes poking out the top of the bottles. You could even hit and kill them. Once I saw this, I knew that reducing the height of the bounding box would fix it. They’d just fly around inside the bottle as designed.
/** * Register the butterflies. */ private static RegistryObject<EntityType<? extends Butterfly>> registerButterfly(int butterflyIndex) { return INSTANCE.register(Butterfly.getRegistryId(butterflyIndex), () -> EntityType.Builder.of(Butterfly::new, MobCategory.CREATURE) .sized(0.3f, 0.2f) .build(Butterfly.getRegistryId(butterflyIndex))); }
This smaller bounding box fits inside the bottle so the butterfly can no longer escape. I built a new version with this fix, and pushed a new version (4.0.1) out before too many people downloaded the bugged version.
Moths
I’ve been wanting to introduce moths for a while. But I didn’t want them to be simple reskins of butterflies. I want them to actually feel like a different creature. The main things I want to implement are as follows:
- Moths are usually nocturnal, whereas butterflies generally come out in the night.
- Moths will be drawn to flames and other light sources at night.
- Landed moths will have their wings open rather than straight up.
There are, of course, more differences than this between real-life moths and butterflies, but this should be enough to make them actually feel different in a Minecraft world.
This week, I started by implementing Diurnality, and adding a day-night cycle to butterflies that can be flipped by editing the butterfly data’s JSON files.
Diurnality
Diurnality is a new attribute I’ve added to butterflies. It is used to determine when a butterfly will be active. There are four different types of Diurnality I want to support:
- Diurnal: The butterfly will generally be active during the daytime.
- Nocturnal: The butterfly (moth) will generall be active at night.
- Crepuscular: The butterfly is active during twilight (sunrise/sunset).
- Cathemeral: The butterfly is active at all times of the day.
It all starts with an enumeration.
// Represents where in the day/night cycle a butterfly is most active. @SuppressWarnings("unused") public enum Diurnality { DIURNAL, NOCTURNAL, CREPUSCULAR, CATHEMERAL }
This is added to the ButterflyData
and is also synchronised through the NetworkEventListener
and ClientBoundDataPacket
. The new attribute is also added to the butterfly JSON files. For now, they are all diurnal
, but this also helps to prepare us for moths.
To use this new property, we need a way of finding out if the butterfly should be active. I added a new method to the Butterfly
class to tell us if the butterfly should be active.
/** * Checks the time of day to see if the butterfly is active based on its * diurnality. * @return TRUE if the butterfly is active at this time of day. */ public boolean getIsActive() { switch (diurnality) { case DIURNAL -> { return this.level().isDay(); } case NOCTURNAL -> { return this.level().isNight(); } case CREPUSCULAR -> { return !this.level().dimensionType().hasFixedTime() && this.level().getSkyDarken() == 4; } default -> { return true; } } }
We use the level
‘s isDark()
and isDay()
methods to determine if a butterfly should be active. For crepuscular butterflies we use the level’s skyDarken
attribute, which is used internally by the other methods. When skyDarken
is 4, Minecraft is usually switching between night and day.
Another advantage of using these methods is that skyDarken
is affected by the weather. So on particularly stormy days, butterflies may go into hiding early. Of course, we can’t just add a new attribute and expect it to change things. We need to modify the butterfly’s behaviour.
Behaviour
In this design, inactive butterflies won’t mate, lay eggs, or pollinate flowers. To implement this, I modified the canUse()
and canContinueToUse()
for each of these goals so that they could only be used when the butterfly is active. This means that butterflies won’t do these things at night.
/** * Stop using if time of day changes to inactive. * @return Whether the goal can continue being active. */ @Override public boolean canContinueToUse() { return this.butterfly.getIsActive() && super.canContinueToUse(); } /** * Butterflies can only pollinate when active. * @return TRUE if the butterfly can pollinate right now. */ @Override public boolean canUse() { return this.butterfly.getIsActive() && super.canUse(); }
Another behaviour I wanted to add was to make the butterflies try and rest when they are inactive. For this one I added a new goal, the ButterflyRestGoal
, which is a stripped down version of the ButterflyMatingGoal
.
/** * Goal used when a butterfly is inactive and wants to rest. */ public class ButterflyRestGoal extends MoveToBlockGoal { // The butterfly using this goal. private final Butterfly butterfly; /** * Construction * @param mob The instance of the butterfly. * @param speedModifier The speed modifier applied when this goal is in progress. * @param searchRange The range to search for blocks. * @param verticalSearchRange The vertical range to search for blocks. */ public ButterflyRestGoal(Butterfly mob, double speedModifier, int searchRange, int verticalSearchRange) { super(mob, speedModifier, searchRange, verticalSearchRange); this.butterfly = mob; } /** * Stop using if time of day changes to an active time. * @return Whether the goal can continue being active. */ @Override public boolean canContinueToUse() { return !this.butterfly.getIsActive() && super.canContinueToUse(); } /** * Butterflies can only rest when inactive. * @return TRUE if the butterfly can pollinate right now. */ @Override public boolean canUse() { if (!this.butterfly.getIsActive()) { nextStartTick = 0; return super.canUse(); } return false; } /** * Start using the goal - ensure the butterfly is not landed. */ @Override public void start() { this.butterfly.setLanded(false); super.start(); } /** * Update the butterfly after it has landed. */ @Override public void tick() { super.tick(); if (this.isReachedTarget()) { Vec3 deltaMovement = this.butterfly.getDeltaMovement(); this.butterfly.setLanded(true); this.butterfly.setDeltaMovement(0.0, deltaMovement.y, 0.0); } } /** * 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 (!levelReader.isEmptyBlock(blockPos.above())) { return false; } if (levelReader.getBlockState(blockPos).is(BlockTags.LEAVES)) { return blockPos.getY() < this.butterfly.getBlockY(); } return false; } }
By basing it on MoveToBlockGoal
we create a goal where the butterfly will attempt to find a leaf block to rest. It can only be used when the butterfly is inactive, meaning that in our case butterflies will attempt to rest at night.
I added this goal as a higher priority than the ButterflyWanderGoal
so that it takes priority. ButterflyWanderGoal
remains usable at all times, meaning the butterfly will continue to wander if it can’t find a place to rest.
this.goalSelector.addGoal(6, new ButterflyRestGoal(this, 0.8, 8, 8)); this.goalSelector.addGoal(8, new ButterflyWanderGoal(this));
AvoidEntityGoal
remains unchanged at the highest priority. So butterflies will still get spooked and fly away if another creature gets too close.
Now that the groundwork has been laid, we can look forward to adding moths to the game. Though there are still those couple of behaviours I want to add to make moths a little different.