This week I continued adding new behaviours specifically for moths. Along the way I had to update the movement code (again) and fix a minor bug. But now moths will be drawn to light and use a different landing animation.
“Butterfly” Type
Before I can do anything, I need a new attribute to tell us if a butterfly is actually a moth. To do this I added a new enumeration to the ButterflyData
.
// Represents the type of "butterfly" public enum ButterflyType { BUTTERFLY, MOTH }
This gets added to our JSON data, and it is read and synced across the network like all other attributes. With this attribute, we can now create different behaviours for moths and butterflies.
Butterfly Data Access
While working on the moth’s movement goal, I discovered there was a bug in the construction of butterflies. Basically, the data gets read after the super class constructor is called. I already tried to fix this by removing and adding the goals from scratch, but it turns out is affecting other parts of the butterfly creation, namely the navigation classes.
To fix this, I rewrote the class so it holds a reference to its ButterflyData
that gets created the first time it is accessed.
/** * Accessor to help get butterfly data when needed. * @return A valid butterfly data entry. */ private ButterflyData getData() { if (this.data == null) { String species = getSpeciesString(); ResourceLocation location = new ResourceLocation(ButterfliesMod.MODID, species); this.data = ButterflyData.getEntry(location); } return this.data; }
This means that if an overridden method tries to access the data during construction, the butterfly data reference should still be valid. I use getSpeciesString()
to create the ResourceLocation
. This method is just code moved from the constructor into its own method.
/** * Helper method to get the species string for creating resource locations. * @return A valid species string. */ private String getSpeciesString() { String species = "undiscovered"; String encodeId = this.getEncodeId(); if (encodeId != null) { String[] split = encodeId.split(":"); if (split.length >= 2) { species = split[1]; } } return species; }
I replaced all references to butterfly data with calls to getData()
to ensure they would be safe. Now it should be guaranteed that the correct data is being used and that it will always be created on access.
Better Movement Goals
I never really liked the way I did the butterfly’s wandering goal. It’s very basic and doesn’t take advantage of the pathfinding Minecraft already has in place. Minecraft has a WaterAvoidingRandomFlyingGoal
that I had tried to use before, but it would cause the butterflies to hover in place too often. This works for (e.g.) sheep who stop to graze, but I wanted butterflies to move around constantly.
This week I dug through the code again and realised I could just change the interval of how often the goal is used. By setting it to 1 tick, it pretty much behaves the same as my poorly written wander goal.
/** * Wander goal to determine the position a butterfly will move to. */ public class ButterflyWanderGoal extends WaterAvoidingRandomFlyingGoal { /** * Construction - set this to a movement goal. * @param butterfly The entity this goal belongs to. * @param speedModifier The speed modifier to apply to this goal. */ public ButterflyWanderGoal(Butterfly butterfly, double speedModifier) { super(butterfly, speedModifier); this.setInterval(1); } }
This code is much simpler, cleaner, and is a good base for creating our moth-specific behaviour.
Moth Wander Goal
For the moth wander goal, I based it upon ButterflyWanderGoal
. In order to get the behaviour I wanted, I just need to override getPosition()
. The algorithm is simple: if the light level of the target position is darker than the current position, then “reroll” the target position.
/** * Moths will tend toward brighter areas. * @return The target position, if a valid position is found. */ @Nullable @Override protected Vec3 getPosition() { Vec3 pos = super.getPosition(); if (pos != null) { int targetBrightness = this.mob.level().getRawBrightness(new BlockPos((int)pos.x(), (int)pos.y(), (int)pos.z()), 0); int localBrightness = this.mob.level().getRawBrightness(this.mob.blockPosition(), 0); if (targetBrightness < localBrightness) { pos = super.getPosition(); } } return pos; }
To get this to work, we need to actually use the goal. A small update to our addGoals()
method allows us to do this.
if (getData().type() == ButterflyData.ButterflyType.MOTH) { this.goalSelector.addGoal(8, new MothWanderGoal(this, 1.0)); } else { this.goalSelector.addGoal(8, new ButterflyWanderGoal(this, 1.0)); }
This simple change might not seem like much, but it does cause moths to slowly move toward lighter areas, while still allowing them to move toward darker areas. Over time, if you have several moths in an area, they will start to swarm around areas of light just like in real life.
Fixing the Landing
In previous articles you may have noticed that I use various start()
methods in goals to ensure butterflies aren’t in the landed state. This turns out to not be effective. If you forget to do it in one goal, or use a default goal (such as AvoidEntityGoal
), you will have butterflies flying around in the landed state.
The solution is to ensure that the landed state is exited when you leave the goals that use it. So I modified ButterflyRestGoal
and ButterflyLayEggGoal
to do just that with one method override.
/** * Ensure the butterfly isn't in the landed state when the goal ends. */ @Override public void stop() { this.butterfly.setLanded(false); super.stop(); }
Now it doesn’t matter which goal the butterfly moves on to, it will be in the correct state.
The last thing I changed this week was how moths look when they land. In real life, butterflies tend to hold their wings up, whereas moths tend to spread them. So I change the animation code in the model slightly to reflect this.
/** * Create a flying animation * @param entity The butterfly 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 Butterfly entity, float limbSwing, float limbSwingAmount, float ageInTicks, float netHeadYaw, float headPitch) { // When landed butterflies hold their wings together. if (entity.getLanded()) { this.body.yRot = 0.7853982F; if (entity.getIsMoth()) { this.right_wing.xRot = 0; } else { this.right_wing.xRot = -Mth.PI * 0.5F; } } else { this.body.yRot = 0.7853982F + Mth.cos(ageInTicks * 0.1F) * 0.15F; this.right_wing.xRot = Mth.sin(ageInTicks * 1.3F) * Mth.PI * 0.25F; } this.left_wing.xRot = -right_wing.xRot; }
getIsMoth()
is just an accessor that returns true
if this model is attached to a moth. With this change, moths will now spread their wings when they land.
I’m almost ready to start adding moths. Almost. I just have one more thing to add first.
One thought on “Just Like a Moth to a Flame”