Pollinating Flowers with AI

This week I introduced a new system to the Butterfly mod. Since butterflies like to pollinate flowers in real life, I’ve added a system where they can do the same in Minecraft. A lot of thought went into this system so it doesn’t unbalance the game too much.

Flower Crop


I didn’t want flowers to grow too quickly. It would unbalance the game, and it would also look weird when new flowers kept cropping up out of nowhere. So I decided to implement a new flower crop that would slowly grow into a flower.

Flower crops basically behave the same as other crop blocks, however there are a few things that need to change for them to work. So naturally I have Flower crops inheriting their behaviour from Minecraft’s CropBlock.

/**
 * Represents a crop that grows into a flower.
 */
public class FlowerCropBlock extends CropBlock {

}

We want the block to change into an actual flower once it has grown, so we update its behaviour to return a flower item once it has reached the maximum age.

    // The flower block the crop will grow into.
    private final Block flowerBlock;

    /**
     * Construction - copy the properties from the flower it will grow into.
     * @param block The flower block.
     */
    public FlowerCropBlock(Block block) {
        super(BlockBehaviour.Properties.copy(block));
        this.flowerBlock = block;
    }
 
    /**
     * Gets the block state based on the age of the crop. Will convert to a
     * flower when it is fully grown.
     * @param age The age of the crop.
     * @return The updated block state.
     */
    @NotNull
    @Override
    public BlockState getStateForAge(int age) {
        return age == MAX_AGE ? this.flowerBlock.defaultBlockState() : super.getStateForAge(age);
    }

CropBlock defines a seed item that is dropped when the block is broken. For flowers there will be no seeds – players will have to rely on butterfly pollination instead. To do this, we override the method and have it return an empty item stack instead of a seed item.

    /**
     * Flower crops cannot be cloned or placed, even in creative mode.
     * @param blockGetter The block getter.
     * @param blockPos The block position.
     * @param blockState The block state.
     * @return An empty item stack.
     */
    @NotNull
    @Override
    public ItemStack getCloneItemStack(@NotNull BlockGetter blockGetter,
                                       @NotNull BlockPos blockPos,
                                       @NotNull BlockState blockState) {
        return ItemStack.EMPTY;
    }

Flower blocks also don’t need farmland like other crops to grow, so we need to override this behaviour and allow these crops to grow on grass.

    /**
     * FLower buds can be placed on grass blocks.
     * @param blockState The block state.
     * @param blockGetter The block getter.
     * @param blockPos The block position.
     * @return TRUE if the crop is on a grass block.
     */
    @Override
    protected boolean mayPlaceOn(BlockState blockState,
                                 @NotNull BlockGetter blockGetter,
                                 @NotNull BlockPos blockPos) {
        return blockState.is(Blocks.GRASS_BLOCK);
    }

Finally, the shape of flower crops are different, so we need to make sure their bounding/culling shapes are set up correctly.

    // The shape of the flower.
    private static final VoxelShape[] SHAPE_BY_AGE;

    /**
     * Get the shape of the flower crop.
     * @param blockState The block state.
     * @param blockGetter The block getter.
     * @param blockPos The block position.
     * @param collisionContext The collision context.
     * @return The shape of the crop based on its age.
     */
    @NotNull
    @Override
    public VoxelShape getShape(@NotNull BlockState blockState,
                               @NotNull BlockGetter blockGetter,
                               @NotNull BlockPos blockPos,
                               @NotNull CollisionContext collisionContext) {
        Vec3 offset = blockState.getOffset(blockGetter, blockPos);
        return SHAPE_BY_AGE[this.getAge(blockState)].move(offset.x, offset.y, offset.z);
    }

    // Defines the shape of the crop for each age.
    static {
        SHAPE_BY_AGE = new VoxelShape[] {
                Block.box(5.0, 0.0, 5.0, 11.0, 2.0, 11.0),
                Block.box(5.0, 0.0, 5.0, 11.0, 4.0, 11.0),
                Block.box(5.0, 0.0, 5.0, 11.0, 6.0, 11.0),
                Block.box(5.0, 0.0, 5.0, 11.0, 8.0, 11.0),
                Block.box(5.0, 0.0, 5.0, 11.0, 10.0, 11.0),
                Block.box(5.0, 0.0, 5.0, 11.0, 10.0, 11.0),
                Block.box(5.0, 0.0, 5.0, 11.0, 10.0, 11.0),
                Block.box(5.0, 0.0, 5.0, 11.0, 10.0, 11.0)
        };
    }

Now that I have the flower crop block set up, I can register our new flower crops in our BlockRegistry.

    // Flower Buds
    public static final RegistryObject<Block> ALLIUM_BUD = INSTANCE.register(
            "bud_allium", () -> new FlowerCropBlock(Blocks.ALLIUM)
    );

    public static final RegistryObject<Block> AZURE_BLUET_BUD = INSTANCE.register(
            "bud_azure_bluet", () -> new FlowerCropBlock(Blocks.AZURE_BLUET)
    );

    public static final RegistryObject<Block> BLUE_ORCHID_BUD = INSTANCE.register(
            "bud_blue_orchid", () -> new FlowerCropBlock(Blocks.BLUE_ORCHID)
    );

    public static final RegistryObject<Block> CORNFLOWER_BUD = INSTANCE.register(
            "bud_cornflower", () -> new FlowerCropBlock(Blocks.CORNFLOWER)
    );

    public static final RegistryObject<Block> DANDELION_BUD = INSTANCE.register(
            "bud_dandelion", () -> new FlowerCropBlock(Blocks.DANDELION)
    );

    public static final RegistryObject<Block> LILY_OF_THE_VALLEY_BUD = INSTANCE.register(
            "bud_lily_of_the_valley", () -> new FlowerCropBlock(Blocks.LILY_OF_THE_VALLEY)
    );

    public static final RegistryObject<Block> ORANGE_TULIP_BUD = INSTANCE.register(
            "bud_orange_tulip", () -> new FlowerCropBlock(Blocks.ORANGE_TULIP)
    );

    public static final RegistryObject<Block> OXEYE_DAISY_BUD = INSTANCE.register(
            "bud_oxeye_daisy", () -> new FlowerCropBlock(Blocks.OXEYE_DAISY)
    );

    public static final RegistryObject<Block> PINK_TULIP_BUD = INSTANCE.register(
            "bud_pink_tulip", () -> new FlowerCropBlock(Blocks.PINK_TULIP)
    );

    public static final RegistryObject<Block> POPPY_BUD = INSTANCE.register(
            "bud_poppy", () -> new FlowerCropBlock(Blocks.POPPY)
    );

    public static final RegistryObject<Block> RED_TULIP_BUD = INSTANCE.register(
            "bud_red_tulip", () -> new FlowerCropBlock(Blocks.RED_TULIP)
    );

    public static final RegistryObject<Block> WHITE_TULIP_BUD = INSTANCE.register(
            "bud_white_tulip", () -> new FlowerCropBlock(Blocks.WHITE_TULIP)
    );

    public static final RegistryObject<Block> WITHER_ROSE_BUD = INSTANCE.register(
            "bud_wither_rose", () -> new FlowerCropBlock(Blocks.WITHER_ROSE)
    );

I also added a method to help get the right crop for the flower when it is pollinated. For the torchflower I use the crop that already exists in the base game.

    /**
     * Gets the flower bud for the specified flower block.
     * @param flowerBlock The flower we are trying to seed.
     * @return The bud block for the specified flower, if any.
     */
    public static Block getFlowerBud(Block flowerBlock) {
        if (flowerBlock == Blocks.ALLIUM) {
            return ALLIUM_BUD.get();
        }

        if (flowerBlock == Blocks.AZURE_BLUET) {
            return AZURE_BLUET_BUD.get();
        }

        if (flowerBlock == Blocks.BLUE_ORCHID) {
            return BLUE_ORCHID_BUD.get();
        }

        if (flowerBlock == Blocks.CORNFLOWER) {
            return CORNFLOWER_BUD.get();
        }

        if (flowerBlock == Blocks.DANDELION) {
            return DANDELION_BUD.get();
        }

        if (flowerBlock == Blocks.LILY_OF_THE_VALLEY) {
            return LILY_OF_THE_VALLEY_BUD.get();
        }

        if (flowerBlock == Blocks.ORANGE_TULIP) {
            return ORANGE_TULIP_BUD.get();
        }

        if (flowerBlock == Blocks.OXEYE_DAISY) {
            return OXEYE_DAISY_BUD.get();
        }

        if (flowerBlock == Blocks.PINK_TULIP) {
            return PINK_TULIP_BUD.get();
        }

        if (flowerBlock == Blocks.POPPY) {
            return POPPY_BUD.get();
        }

        if (flowerBlock == Blocks.RED_TULIP) {
            return RED_TULIP_BUD.get();
        }

        if (flowerBlock == Blocks.TORCHFLOWER) {
            return Blocks.TORCHFLOWER_CROP;
        }

        if (flowerBlock == Blocks.WHITE_TULIP) {
            return WHITE_TULIP_BUD.get();
        }

        if (flowerBlock == Blocks.WITHER_ROSE) {
            return WITHER_ROSE_BUD.get();
        }

        return null;
    }

Now comes the tedious part. I need to add 13 block states, 6 models for each flower, and 6 textures for each flower. This is a lot of work. Luckily, I can cheat using Python. To start with I still need to define a single blockstate.

{
  "variants": {
    "age=0": {
      "model": "butterflies:block/flower_buds/allium_stage0"
    },
    "age=1": {
      "model": "butterflies:block/flower_buds/allium_stage1"
    },
    "age=2": {
      "model": "butterflies:block/flower_buds/allium_stage2"
    },
    "age=3": {
      "model": "butterflies:block/flower_buds/allium_stage3"
    },
    "age=4": {
      "model": "butterflies:block/flower_buds/allium_stage4"
    },
    "age=5": {
      "model": "butterflies:block/flower_buds/allium_stage5"
    },
    "age=6": {
      "model": "butterflies:block/flower_buds/allium_stage6"
    }
  }
}

I also need to define one set of models. 6 in total, which is one for each age, except for the last age when it becomes a flower. I use the render_type property here so that the flower renders properly, and inherit from block/cross like the flowers in vanilla Minecraft.

{
  "render_type": "minecraft:cutout",
  "parent": "minecraft:block/cross",
  "textures": {
    "cross": "butterflies:block/flower_bud/allium_stage3"
  }
}

Now I can adapt our Python script to generate the files needed for all the other flower buds. Most of the work is done already, since I can just modify the function used to generate butterfly files slightly.

# List of files to generate json files for.
FLOWERS = [
    'allium',
    'azure_bluet',
    'blue_orchid',
    'cornflower',
    'dandelion',
    'lily_of_the_valley',
    'orange_tulip',
    'oxeye_daisy',
    'pink_tulip',
    'poppy',
    'red_tulip',
    'white_tulip',
    'wither_rose'
]


def generate_data_files(input):
    # Get list of files containing input[0]
    files = []
    for (path, _, filenames) in os.walk(os.getcwd()):

        # We only want json
        filenames = [f for f in filenames if f.endswith(".json") and input[0] in f]

        for name in filenames:
            files.append(pathlib.Path(path, name))

    # Loop Start
    for entry in input:

        # We don't need to do this for the template
        if entry == input[0]:
            continue

        for file in files:

            # Get the new filename
            new_file = pathlib.Path(str(file).replace(input[0], entry))

            # Check if the file exists
            if not new_file.is_file():

                # Create the new file if it doesn't exist
                shutil.copy(file, new_file)

                # Read in the new file
                with open(new_file, 'r') as input_file:
                    file_data = input_file.read()

                # Replace the butterfly species
                file_data = file_data.replace(input[0], entry)

                # Write the file out again
                with open(new_file, 'w') as output_file:
                    output_file.write(file_data)


if __name__ == "__main__":
    generate_data_files(BUTTERFLIES)
    generate_data_files(FLOWERS)
    #...etc

With this done, all the JSON files needed are generated automatically. The only modification I made was to reuse some of the stages for the tulips, since they look the same until they start flowering (the first four stages).

The next step was to create the textures for each of the flowers. I needed 6 images for each flower, except for the tulips where I could reuse some images. I came up with an easy way of generating the images using the wheat textures as a mask which helped create them quickly. I still needed to modify a few to get them just right, but was able to create the 76 images I needed in an afternoon.

With this done, it was almost time to implement the pollination goal that would create these flower crops for us. But first…

Preferred Flowers


When GreenForzeGaming suggested the pollination of flowers, it had been something I had been thinking about doing for a while. One thing he suggested that I hadn’t thought of was to have preferred flowers for each butterflies.

To do this I started by modifying the JSON data for butterflies to accept a preferred flower.

{
  "index": 0,
  "entityId": "admiral",
  "size": "medium",
  "speed": "moderate",
  "rarity": "common",
  "habitat": "forests",
  "lifespan": {
    "egg": "medium",
    "caterpillar": "short",
    "chrysalis": "medium",
    "butterfly": "medium"
  },
  "preferredFlower": "blue_orchid"
}

Following on from this I needed to modify the ButterflyData record and the network functions to accept the extra parameter. These changes are one line changes scattered over a few files so it’s hard to show them in a single code block, but you can see them in the pull request.

But how do players know what a butterfly’s preferred flower is? The obvious way is by rigorous testing, lots of note-taking, maths, and statistics, over several hours of play. This seems like a bit much, however, so I also added a new entry into the Butterfly Book.

                    // Preferred Flower
                    component.append("\n");
                    component.append(Component.translatable("gui.butterflies.preferred_flower"));
                    Component description = BuiltInRegistries.ITEM.get(entry.preferredFlower()).asItem().getDescription();
                    component.append(description);

This provides an easy way for players to learn the preferred flower at the cost of a single butterfly’s life.

Pollination Goal


Finally I can modify the pollinate flower goal that will make the butterflies pollinate the flowers. To start, I set up the construction. Here I pull out the preferred flower from the Butterfly Data and set up a couple of other parameters. The attemptedToPollinate flag is used to limit the attempt to pollinate to once each time the goal runs.

/**
 * Goal that enables butterflies to pollinate flowers.
 */
public class ButterflyPollinateFlowerGoal extends MoveToBlockGoal {

    // The butterfly using this goal.
    private final Butterfly butterfly;

    // The flower this butterfly prefers.
    private final Block preferredFlower;

    // The RNG.
    public final RandomSource random;

    // Has pollination been attempted yet?
    public boolean attemptedToPollinate;

    /**
     * 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.
     */
    @SuppressWarnings("deprecation")
    public ButterflyPollinateFlowerGoal(Butterfly mob,
                                        double speedModifier,
                                        int searchRange,
                                        int verticalSearchRange) {
        super(mob, speedModifier, searchRange, verticalSearchRange);
        this.butterfly = mob;

        ButterflyData data = ButterflyData.getEntry(this.butterfly.getButterflyIndex());
        if (data != null) {
            this.preferredFlower = BuiltInRegistries.BLOCK.get(data.preferredFlower());
        } else {
            this.preferredFlower = null;
        }

        this.random = this.butterfly.getRandom();
    }
}

Before we can start the goal we need to find a valid flower. If a preferred flower is found, this will always succeed. Otherwise there is only a 50% chance. When building a flower farm it can be faster to use butterflies with their preferred flowers.

    /**
     * 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.getBlockState(blockPos.above()).isAir()) {
            return false;
        }

        BlockState blockState = levelReader.getBlockState(blockPos);
        if (blockState.is(BlockTags.SMALL_FLOWERS)) {

            // If this is the butterfly's preferred flower it is always valid.
            if (blockState.is(preferredFlower)) {
                return true;
            }

            // Butterflies will only fly to other flowers 50% of the time.
            return (this.random.nextInt() % 2 == 0);
        }

        return false;
    }

When the goal starts we ensure the flags we need are set to false.

    /**
     * Start using the goal - ensure the butterfly is not landed.
     */
    @Override
    public void start() {
        this.butterfly.setLanded(false);
        attemptedToPollinate = false;
        super.start();
    }

Our tick method attempts to pollinate the flower once after it reaches the target. There is a 20% chance it will succeed as long as there is a valid block nearby.

    /**
     * Update the butterfly after it has landed.
     */
    @Override
    public void tick() {
        super.tick();

        if (this.isReachedTarget()) {

            // Don't stay in the landed state for too long.
            this.tryTicks -= 11;
            Vec3 deltaMovement = this.butterfly.getDeltaMovement();
            this.butterfly.setDeltaMovement(0.0, deltaMovement.y, 0.0);

            if (!attemptedToPollinate) {
                attemptedToPollinate = true;

                if (this.random.nextInt() % 5 == 0) {
                    BlockPos spawnPos = findNearestFlowerSpot();
                    if (spawnPos != null) {
                        BlockState blockState = this.mob.level().getBlockState(this.blockPos);
                        Block budBlock = BlockRegistry.getFlowerBud(blockState.getBlock());
                        if (budBlock != null) {
                            this.mob.level().setBlockAndUpdate(spawnPos, budBlock.defaultBlockState());
                        }
                    }
                }
            }
        }
    }

To find a flower spot we use a similar method to the base goal, except it is specifically searching for a grass block with air above it. In the case of a torchflower, it seeks out a farmland block.

    /**
     * Find a good position for a flower to grow.
     * @return A suitable position for a flower to grow, if any.
     */
    private BlockPos findNearestFlowerSpot() {
        BlockPos.MutableBlockPos mutableBlockPos = new BlockPos.MutableBlockPos();

        for(int yOffset = this.verticalSearchStart; yOffset <= 2; yOffset = yOffset > 0 ? -yOffset : 1 - yOffset) {
            for(int range = 0; range < 2; ++range) {
                for(int xOffset = 0; xOffset <= range; xOffset = xOffset > 0 ? -xOffset : 1 - xOffset) {
                    for(int zOffset = xOffset < range && xOffset > -range ? range : 0; zOffset <= range; zOffset = zOffset > 0 ? -zOffset : 1 - zOffset) {
                        mutableBlockPos.setWithOffset(this.blockPos, xOffset, yOffset - 1, zOffset);

                        // Torchflowers require farmland.
                        Block requiredBlock = Blocks.GRASS_BLOCK;
                        if (this.mob.level().getBlockState(this.blockPos).is(Blocks.TORCHFLOWER)) {
                            requiredBlock = Blocks.FARMLAND;
                        }

                        if (this.mob.level().getBlockState(mutableBlockPos).isAir() &&
                            this.mob.level().getBlockState(mutableBlockPos.below()).is(requiredBlock)) {

                            return mutableBlockPos;
                        }
                    }
                }
            }
        }

        return null;
    }

Now that the goal has been updated, butterflies will occasionally fly toward flowers and pollinated them.

Config


Some time ago, there was a bug submitted pointing out that butterflies would breed too fast. Butterflies would end up taking over the world, causing havoc on servers. There was no quick fix for this, but after redesigning the way that butterflies would breed and placing limits on how many could exist in a chunk, the problem was resolved.

At that time I also implemented some config options so that players and servers could control how fast/slow butterflies could breed (or stop it altogether). While I don’t anticipate any problems with this new system, I’ve added a new config option to disable flower pollination just in case.

        enablePollination = builder
                .comment("If set to TRUE butterflies will pollinate flowers and cause new ones to grow.")
                        .define("enable_pollination", true);

This is checked when adding goals to the butterfly and disables the entire system if is set to false.

        // Pollination can be configured to be off.
        if (ButterfliesConfig.enablePollination.get()) {
            this.goalSelector.addGoal(4, new ButterflyPollinateFlowerGoal(this, 0.8d, 8, 8));
        }

Now, if there are any bugs or problems with this system, players will at least be able to disable it while I work on a fix.

Flower Farming


So the Butterfly Mod is now also a farming mod, specifically for flowers. Obviously using bonemeal on a grass block can grow flowers much faster, but there are a couple of advantages to using butterflies:

  1. It doesn’t require bonemeal at all (although it can still be sped up with bonemeal).
  2. You can grow any type of flower anywhere as long as you have grass (or farmland for torchflowers).

Clever redstoners should also be able to find a way to fully automate flower collection, though I’m leaving that one as an exercise for the players.

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.