Regional Variety in Butterflies

Adding new textures to our implementation of butterflies is an easy way to add variety. However, we can strive to do better. Our ultimate goal is to have achievements for catching all butterfly varieties, but this becomes too easy when butterflies of all varieties can spawn anywhere. To address this, we will implement some changes to ensure that different varieties spawn in different biomes. Additionally, we will add more variety by allowing for different sizes of butterflies.

Modifying the Spawn Code

To approach biome-based spawning we could write some code that chooses a butterfly texture based on the biome. However, with the new and improved spawning system, this isn’t an easy thing to do since this point in the code doesn’t know what biome an entity has spawned into.

A more effective method involves creating multiple entity types, each with their own spawn rules. However, we still want to be able to re-use the entity code. To accomplish both of these goals, we begin by removing the VARIANT code from the Butterfly class.

We then replace the NAME constant with unique names for each butterfly:

    // The unique IDs that are used to reference a butterfly entity.
    public static final String MORPHO_NAME = "morpho";
    public static final String FORESTER_NAME = "forester";
    public static final String COMMON_NAME = "common";
    public static final String EMPEROR_NAME = "emperor";
    public static final String HAIRSTREAK_NAME = "hairstreak";
    public static final String RAINBOW_NAME = "rainbow";
    public static final String HEATH_NAME = "heath";
    public static final String GLASSWING_NAME = "glasswing";
    public static final String CHALKHILL_NAME = "chalkhill";
    public static final String SWALLOWTAIL_NAME = "swallowtail";

Next, we add Size and Texture parameters to the class:

    //  Represents the possible sizes of the butterflies.
    public enum Size {
        SMALL,
        MEDIUM,
        LARGE
    }

    //  The size of the butterfly
    private final Size size;

    //  The location of the texture that the renderer should use.
    private final ResourceLocation texture;

Using an enumeration for the size to keeps things simple. With three basic sizes we can introduce variety without having to be precise. If necessary, we can always add more sizes later.

The next step is to modify the default constructor so that it takes a size and a texture:

    public Butterfly(Size size,
                     String texture,
                     EntityType<? extends Butterfly> entityType,
                     Level level) {
        super(entityType, level);

        this.size = size;
        this.texture = new ResourceLocation("butterflies:textures/entity/butterfly/" + texture);
    }

We won’t be able to use this constructor directly when registering the entities, so we create a number of static methods to construct various butterflies. Here are two examples, but you can see all the constructors in the Github repository

    /**
     * Create a Morpho butterfly
     * @param entityType The type of the entity.
     * @param level The current level.
     * @return A newly constructed butterfly.
     */
    @NotNull
    public static Butterfly createMorphoButterfly(EntityType<? extends Butterfly> entityType,
                                                  Level level) {
        return new Butterfly(Size.LARGE, "butterfly_blue.png", entityType, level);
    }

    /**
     * Create a Forester butterfly
     * @param entityType The type of the entity.
     * @param level The current level.
     * @return A newly constructed butterfly.
     */
    @NotNull
    public static Butterfly createForesterButterfly(EntityType<? extends Butterfly> entityType,
                                                    Level level) {
        return new Butterfly(Size.MEDIUM, "butterfly_nyan.png", entityType, level);
    }

Next, in our EntityTypeRegistry we register the butterflies by type using the new names and the static methods, rather than using the constructor directly:

    // Register the butterflies.
    public static final RegistryObject<EntityType<Butterfly>> BUTTERFLY_MORPHO =
            INSTANCE.register(Butterfly.MORPHO_NAME, () -> EntityType.Builder.of(Butterfly::createMorphoButterfly, MobCategory.AMBIENT)
                    .sized(0.3f, 0.4f)
                    .build(Butterfly.NAME));
                    .build(Butterfly.MORPHO_NAME));

    public static final RegistryObject<EntityType<Butterfly>> BUTTERFLY_FORESTER =
            INSTANCE.register(Butterfly.FORESTER_NAME, () -> EntityType.Builder.of(Butterfly::createForesterButterfly, MobCategory.AMBIENT)
                    .sized(0.3f, 0.4f)
                    .build(Butterfly.FORESTER_NAME));

We then update registerEntityRenders, registerEntityAttributes, and registerEntitySpawnPlacement to include the new butterflies. While I won’t list the code here as it’s just a copy/paste job, you can see it in the pull request.

The final step is implementing biome modifiers to specify which biome each butterfly can spawn in. These modifiers are simply a group of JSON files that we put under
src/main/resources/data/butterflies/forge/biome_modifier/. For example the forester.json file for a butterfly that spawns mainly in forests looks like this:

{
  "type": "forge:add_spawns",
  "biomes": [
    "minecraft:forest",
    "minecraft:flower_forest",
    "minecraft:birch_forest",
    "minecraft:dark_forest",
    "minecraft:old_growth_birch_forest",
    "minecraft:old_growth_pine_taiga",
    "minecraft:old_growth_spruce_taiga",
    "minecraft:windswept_forest"
  ],
  "spawners":
  {
    "type": "butterflies:forester",
    "weight": 10,
    "minCount": 3,
    "maxCount": 5
  }
}

With these changes, different types of butterflies will now spawn in different biomes. However, we still need to make sure they actually all look different to one another.

Rendering the Butterfly

To render the butterfly, we need to access the texture and size of the butterfly. Thus we add new accessors to the Butterfly class:

    /**
     * Get the scale to use for the butterfly.
     * @return A scale value based on the butterfly's size.
     */
    public float getScale() {
        switch (size) {
            case SMALL -> { return 0.25f; }
            case LARGE ->{ return 0.45f; }
            default -> { return 0.35f; }
        }
    }

    /**
     * Get the texture to use for rendering.
     * @return The resource location of the texture.
     */
    public ResourceLocation getTexture() {
        return texture;
    }

With getScale(), we return a float value rather than the enumeration itself. This way, if we decide to add more butterfly sizes, or modify their sizes, we only need to make changes in a single place.

Next we need to update the ButterflyRenderer to use these values. We remove the list of textures from the class and update the getTextureLocation method:

    @Override
    public @NotNull ResourceLocation getTextureLocation(@NotNull Butterfly entity) {
        return entity.getTexture();
    }

Updating the size of the butterfly is equally straightforward:

    @Override
    protected void scale(@NotNull Butterfly entity, PoseStack poses, float scale) {
        float s = entity.getScale();
        poses.scale(s, s, s);
    }

Now, the butterflies will not only spawn in specific areas but also appear visually distinct. We’re not quite done yet. There’s one more thing we can do to complete this feature.

Egg

With each butterfly having its own EntityType entry, we can create spawn eggs for each one. In our ItemRegistry we register new items for each butterfly:

    private static final RegistryObject<Item> BUTTERFLY_MORPHO_EGG = INSTANCE.register(Butterfly.MORPHO_NAME,
            () -> new ForgeSpawnEggItem(EntityTypeRegistry.BUTTERFLY_MORPHO, 0x0000aa, 0x0088ff, new Item.Properties()));

    private static final RegistryObject<Item> BUTTERFLY_FORESTER_EGG = INSTANCE.register(Butterfly.FORESTER_NAME,
            () -> new ForgeSpawnEggItem(EntityTypeRegistry.BUTTERFLY_FORESTER, 0xeeee77, 0xff7777, new Item.Properties()));

Then, we register them with the creative tab so that they appear in the creative inventory:

    @SubscribeEvent
    public static void registerCreativeTabContents(CreativeModeTabEvent.BuildContents event) {
        if (event.getTab() == CreativeModeTabs.SPAWN_EGGS) {
            event.accept(BUTTERFLY_MORPHO_EGG);
            event.accept(BUTTERFLY_FORESTER_EGG);
            event.accept(BUTTERFLY_COMMON_EGG);
            event.accept(BUTTERFLY_EMPEROR_EGG);
            event.accept(BUTTERFLY_HAIRSTREAK_EGG);
            event.accept(BUTTERFLY_RAINBOW_EGG);
            event.accept(BUTTERFLY_HEATH_EGG);
            event.accept(BUTTERFLY_GLASSWING_EGG);
            event.accept(BUTTERFLY_CHALKHILL_EGG);
            event.accept(BUTTERFLY_SWALLOWTAIL_EGG);
        }
    }

We also need to create an item model for each egg. These are just renamed versions of the egg model we used for the original butterfly, and they can be seen under ./resources/assets/butterflies/models/item/.

Lastly, we need to do is update our language strings. Each butterfly needs its own string related to its entity ID in en_lang.json:

{
  "item.butterflies.butterfly": "Butterfly"
  "item.butterflies.morpho": "Morpho Butterfly",
  "item.butterflies.forester": "Forester Butterfly",
  "item.butterflies.common": "Common Butterfly",
  "item.butterflies.emperor": "Emperor Butterfly",
  "item.butterflies.hairstreak": "Hairstreak Butterfly",
  "item.butterflies.rainbow": "Rainbow Butterfly",
  "item.butterflies.heath": "Heath Butterfly",
  "item.butterflies.glasswing": "Glasswing Butterfly",
  "item.butterflies.chalkhill": "Chalkhill Butterfly",
  "item.butterflies.swallowtail": "Swallowtail Butterfly"
}

If we ever want to support other languages we would need to add extra json files for each one in this folder, but for now we will just support English.

Now all we need to do is keep adding more butterfly types!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.