I’ve been playing a game with the mod installed for a while now, and there are a few things I want to improve upon. First, I wanted to tweak the butterflies a bit to add some extra variety. Second, I’ve noticed that butterflies tend to grow in population exponentially.
I have one more feature I want to implement before I release a second version of the butterfly mod. But I keep getting distracted by the details. I’ve been reading up a lot on butterfly species this week, and I ended up creating a design that takes into account the real-life counterparts of the butterflies I included in the mod.
I’ve altered some of the butterfly’s sizes, made some butterflies faster, and tweaked the spawn rates of the butterflies. I’ve also changed the length of time that butterflies remain in each phase of their life.
Butterfly Entry
To support these new details, I created a ButterflyEntry
class that contains the various butterfly attributes. This way I only need a single hash map for all the data, referenced by the butterfly index.
private static final Map<Integer, Entry> BUTTERFLY_ENTRIES = new HashMap<>(); /** * Class to hold all the data for a specific butterfly. */ public static class Entry { public final String entityId; public final Size size; public final Speed speed; public final int caterpillarLifespan; public final int chrysalisLifespan; public final int butterflyLifespan; /** * Construction * @param entityId The id of the butterfly species. * @param size The size of the butterfly. * @param speed The speed of the butterfly. * @param caterpillarLifespan How long it remains in the caterpillar stage. * @param chrysalisLifespan How long it takes for a chrysalis to hatch. * @param butterflyLifespan How long it lives as a butterfly. */ private Entry(String entityId, Size size, Speed speed, int caterpillarLifespan, int chrysalisLifespan, int butterflyLifespan) { this.entityId = entityId; this.size = size; this.speed = speed; this.caterpillarLifespan = caterpillarLifespan * 2; this.chrysalisLifespan = chrysalisLifespan; this.butterflyLifespan = butterflyLifespan * 2; } }
With this set up I can use static initialisation to set up all the data. All it needs is for all the data from the table above to be passed into it.
private static void addButterfly(int index, String species, Size size, Speed speed, int caterpillarLifespan, int chrysalisLifespan, int butterflyLifespan) { ENTITY_ID_TO_INDEX_MAP.put(species, index); BUTTERFLY_ENTRIES.put(index, new Entry(species, size, speed, caterpillarLifespan, chrysalisLifespan, butterflyLifespan)); } static { addButterfly(0, "admiral", Size.MEDIUM, Speed.MODERATE, LIFESPAN_SHORT, LIFESPAN_MEDIUM, LIFESPAN_MEDIUM); addButterfly(1, "buckeye", Size.MEDIUM, Speed.MODERATE, LIFESPAN_SHORT, LIFESPAN_SHORT, LIFESPAN_MEDIUM); addButterfly(2, "cabbage", Size.MEDIUM, Speed.MODERATE, LIFESPAN_SHORT, LIFESPAN_SHORT, LIFESPAN_SHORT); addButterfly(3, "chalkhill", Size.SMALL, Speed.FAST, LIFESPAN_MEDIUM, LIFESPAN_MEDIUM, LIFESPAN_MEDIUM); addButterfly(4, "clipper", Size.LARGE, Speed.FAST, LIFESPAN_MEDIUM, LIFESPAN_LONG, LIFESPAN_MEDIUM); addButterfly(5, "common", Size.SMALL, Speed.MODERATE, LIFESPAN_SHORT, LIFESPAN_SHORT, LIFESPAN_MEDIUM); addButterfly(6, "emperor", Size.MEDIUM, Speed.MODERATE, LIFESPAN_MEDIUM, LIFESPAN_SHORT, LIFESPAN_MEDIUM); addButterfly(7, "forester", Size.SMALL, Speed.MODERATE, LIFESPAN_LONG, LIFESPAN_MEDIUM, LIFESPAN_MEDIUM); addButterfly(8, "glasswing", Size.MEDIUM, Speed.MODERATE, LIFESPAN_SHORT, LIFESPAN_SHORT, LIFESPAN_LONG); addButterfly(9, "hairstreak", Size.SMALL, Speed.MODERATE, LIFESPAN_SHORT, LIFESPAN_MEDIUM, LIFESPAN_SHORT); addButterfly(10, "heath", Size.SMALL, Speed.MODERATE, LIFESPAN_LONG, LIFESPAN_MEDIUM, LIFESPAN_LONG); addButterfly(11, "longwing", Size.MEDIUM, Speed.MODERATE, LIFESPAN_SHORT, LIFESPAN_SHORT, LIFESPAN_LONG); addButterfly(12, "monarch", Size.LARGE, Speed.MODERATE, LIFESPAN_SHORT, LIFESPAN_SHORT, LIFESPAN_MEDIUM); addButterfly(13, "morpho", Size.LARGE, Speed.MODERATE, LIFESPAN_MEDIUM, LIFESPAN_SHORT, LIFESPAN_MEDIUM); addButterfly(14, "rainbow", Size.SMALL, Speed.FAST, LIFESPAN_MEDIUM, LIFESPAN_MEDIUM, LIFESPAN_MEDIUM); addButterfly(15, "swallowtail", Size.LARGE, Speed.MODERATE, LIFESPAN_SHORT, LIFESPAN_MEDIUM, LIFESPAN_SHORT); }
I add a private helper method to convert an integer to an entity ID.
/** * Converts an index to an entity ID. * @param index The index to convert to an entity ID. * @return The entity ID string. */ private static String indexToEntityId(int index) { if (BUTTERFLY_ENTRIES.containsKey(index)) { return BUTTERFLY_ENTRIES.get(index).entityId; } return null; }
This is then used in all the indexToXLocation
methods to help convert an index to a resource ID. I’ve only included one example here, but the method is basically the same for the others.
public static ResourceLocation indexToButterflyEggLocation(int index) { if (INDEX_TO_ENTITY_ID_MAP.containsKey(index)) { return new ResourceLocation(ButterfliesMod.MODID, INDEX_TO_ENTITY_ID_MAP.get(index) + "_egg"); String entityId = indexToEntityId(index); if (entityId != null) { return new ResourceLocation(ButterfliesMod.MODID, entityId + "_egg"); } }
Finally, I remove the accessors to the size, and replace them with accessors to the data.
/** * Get butterfly data by index. * @param index The butterfly index. * @return The butterfly entry. */ public static Entry getEntry(int index) { if (BUTTERFLY_ENTRIES.containsKey(index)) { return BUTTERFLY_ENTRIES.get(index); } return null; } public static Size getSize(ResourceLocation location) { /** * Get butterfly data by resource location. * @param location The resource location of the butterfly. * @return The butterfly entry. */ public static Entry getEntry(ResourceLocation location) { int index = locationToIndex(location); return getEntry(index); }
The game is now set to hold all the butterfly data, but we also need to implement code that actually does stuff with this data.
Fast Butterflies, Mortal Butterflies
In order for these to have an effect on the actual butterflies, I add size and speed member variables to the Butterfly
class. I then set them in the constructor based on the species of butterfly.
// The size of the butterfly. private final ButterflyData.Size size; // The speed of the butterfly. private final double speed; /** * The default constructor. * @param species The species of the butterfly. * @param entityType The type of the entity. * @param level The level where the entity exists. */ public Butterfly(String species, EntityType<? extends Butterfly> entityType, Level level) { super(entityType, level); this.texture = new ResourceLocation("butterflies:textures/entity/butterfly/butterfly_" + species + ".png"); ResourceLocation location = new ResourceLocation(ButterfliesMod.MODID, species); ButterflyData.Entry data = ButterflyData.getEntry(location); this.size = data.size; if (data.speed == ButterflyData.Speed.FAST) { this.speed = BUTTERFLY_SPEED * 1.2d; } else { this.speed = BUTTERFLY_SPEED; } setAge(-data.butterflyLifespan); }
This way I can now access these members directly rather than doing a dictionary lookup every time I want a butterfly’s stats. In customServerAiStep()
, I replace BUTTERFLY_SPEED
with this.speed
so it has an effect.
I modify the Caterpillar
and Chrysalis
classes in similar ways in order to support the Size
attribute. For the most part the change is similar to what I have done for the Butterfly
class so I won’t repeat it here, but you can see the changes in full in the pull request.
I also add some code at the end of the method that causes a butterfly to die after its lifespan is reached. This will not happen if the butterfly has been made persistent somehow (e.g. by the player using a nametag on it). This allows players the option of having a permanent butterfly pet (or pets).
// If the caterpillar gets too old it will die. This won't happen if it // has been set to persistent (e.g. by using a nametag). if (!this.isPersistenceRequired() && this.getAge() >= 0 && this.random.nextInt(0, 15) == 0) { this.kill(); }
My reasoning for giving the butterflies a limited lifespan comes from playtesting the game with the mod installed. It turns out that if you live in an area with butterflies and trees, they will create new butterflies without any player intervention. The population will quickly double. Then it will double again. And again.
It grows exponentially. At some point the world will be nothing but butterflies. I mentioned in an earlier post that I don’t want the mod to overwhelm the game or other mods the player might have installed. This is exactly what will happen eventually, so by giving the butterflies a limited lifespan I hope to mitigate this while still maintaining butterflies as a presence in the world.
Rare Butterflies
Another way to reduce the impact of butterflies was to make some of them rarer. If you refer to the chart at the top, you will notice there is a Rarity
column that I haven’t talked about yet. This part is entirely data driven by Forge. All I need to do is modify the biome modifiers, found under src/main/resources/data/butterflies/forge/biome_modifier/..
.
I’ve talked about these before, but basically they allow you to set a new entity to spawn naturally in any biome using a JSON file. For example, I set uncommon butterflies like the glasswing using a file that looks like this:
{ "type": "forge:add_spawns", "biomes": [ "minecraft:dark_forest", "minecraft:old_growth_pine_taiga", "minecraft:old_growth_spruce_taiga", "minecraft:jungle", "minecraft:sparse_jungle", "minecraft:bamboo_jungle", "minecraft:lush_caves" ], "spawners": { "type": "butterflies:glasswing", "weight": 5, "minCount": 2, "maxCount": 4 } }
By reducing the weight from 10 to 5, and by reducing minCount
and maxCount
, I make them spawn less often and in fewer numbers. Rare butterflies have even lower numbers (a weight
of 2, and a count of 1-3). This also adds a bit more to the gameplay. If players want to collect all the butterflies, they may have to search a little harder for some.
I want to go on a little bit of a tangent here. I didn’t just select which butterflies would be rare at random. Many species of butterfly are having their habitat threatened, either by deforestation or other means, and are on various lists as a “threatened” species. The species I’ve chosen to make rare here are species where I found it mentioned that they are under threat.
Though I have to admit my research isn’t exactly thorough, and it strikes me that all butterfly species are at risk somehow. Butterflies are important for the pollination of many flowers, and serve as food sources for many other species. I found a quote from a conservation botanist, Megan O’Connell, that said, “Butterflies are important indicators of the health of the environment”.
Maybe I’m hoping that by showing how rare some species can be, people will subconsciously adopt that idea. But I guess it’s the height of naivety to think a small mod like this could make much of a difference.
Data Driving
If this code wasn’t already begging to be data driven last week, it definitely is now. I do plan to move this into JSON data that gets read in before entities are registered. Once this is done, we will be able to add new butterflies without writing a single line of Java.
I’ve decided to put this off until release version 3, however. Next week I plan to implement one more feature and then we can release version 2 of Bok’s Butterflies. This feature won’t benefit from data driving the butterflies, so it makes sense to delay the implementation for now.
That’s pretty much it for this week. If I get enough done, then next week I’ll be writing about releasing version 2.0!