Rendering Debug Information

The landing animations are still a bit unreliable. All too often when testing Bok’s Banging Butterflies I still see landed butterflies flapping their wings. This week I decided to figure out exactly why it was happening, and to do that I had to be able to see their state in real time.

In Minecraft, Goals dictate how many mobs prioritize their actions, like moving, resting, or interacting with the environment. As butterflies are mobs , they also use Goals to determine their behaviour. Usually, more than one goal can be running at a time, but for butterflies there is only be one goal running at any given moment.

During the night, butterflies will become inactive, and moths will become inactive during the day. This means that they will enter the Rest goal. When in this goal they will attempt to find a leaf block and land on it. When they have landed, they will stop flapping their wings, and remain still until sun rises (or sets) and they become active again.

This behaviour improves player immersion by having butterflies and moths behave in ways similar to real life. It also helps players a little, giving them an opportunity to sneak up and catch a butterfly while it is busy resting.

However, there is a problem. Landed butterflies will eventually start flapping their wings again. They will remain on the block they landed on, but they will flap as if they are still flying. This doesn’t make sense: if they are using the Rest goal they shouldn’t animate, but if the aren’t they should actually be moving around.

This problem has been frustrating me for many versions of the mod, so it was finally time to figure out what was going wrong. But first, I needed a plan.

The Plan


The plan I had in mind was to render the current goals of each butterfly in the world. This way I’d be able to load into a world and see what state each butterfly was in without having to rely on console output to figure out what each butterfly was doing.

Of course, I don’t want this rendering for normal players, so I’d need a way to switch the rendering on and off. So I’ll add a config option for it, so if I ever need to use this again in the future I can just change a single flag.

One problem to overcome will be the rendering of the AI’s state. The AI is only run on the client, and the renderer is only run on the server. The two systems don’t interact directly and this have no idea the other exists. So I’ll also need a way to synchronise the state to the client, which will also be disabled if the debug flag is disabled.

Finally the plan is to observe the butterflies in game and try to improve their landing behaviour.

Debug Flag


In order to switch the debug information on and off, I’ve decided to add a flag to the config. This makes it easy to switch the mod to a debug mode where we’ll be able to get more information about the behaviour of entities in the game. To add this flag, I just update the ButterfliesConfig class:

public class ButterfliesConfig {
    public static final ForgeConfigSpec SERVER_CONFIG;

    // Other config values omitted for brevity.
    public static ForgeConfigSpec.BooleanValue debugInformation;

    /**
     * Set up the server configuration.
     * @param builder The spec builder.
     */
    private static void setupServerConfig(ForgeConfigSpec.Builder builder) {
        builder.comment("This category holds configs for the butterflies mod.");
        builder.push("Butterfly Options");

        // <snip>

        debugInformation = builder
                .comment("If set to TRUE debug information will be rendered.")
                .define("debug_info", false);

        builder.pop();
    }
}

Now we can swap the debugInformation flag in our server config to switch to debug mode. When implementing any debug mode features, we can first check it’s enabled by using the following conditional statement:

        if (ButterfliesConfig.debugInformation.get()) {
            // Do the thing!
        }

With the debug flag in place, I needed a way to synchronize AI states between server and client. This is a crucial step for rendering debug information, as it tells the client what information we want to display.

Synchronisation


The next problem to overcome is telling the renderer what information to display. The AI only runs on the server, and the renderer on the client. Each of them have no idea what the other is doing. So we need a way of communicating the right information to the client so it can be rendered.

To do this I add a new data serialiser to the Butterfly entity. Data serialisers are used to synchronise information between a server and a client. In this case, a list of the currently running goals, represented as a string will be sent from the server to a client. This data is stored in an EntityDataAccessor, which is then added to the list of data that needs to be synchronised between client and server.

    protected static final EntityDataAccessor<String> DATA_GOAL_STATE =
            SynchedEntityData.defineId(Butterfly.class, EntityDataSerializers.STRING);

    /**
     * Get the current goal state, used for debugging.
     * @return The current goal state.
     */
    public String getGoalState() {
        return entityData.get(DATA_GOAL_STATE);
    }

    /**
     * Set the goal state displayed when debugging.
     * @param goalState The current goal state.
     */
    public void setGoalState(String goalState) {
        entityData.set(DATA_GOAL_STATE, goalState);
    }

    /**
     * Override to define extra data to be synced between server and client.
     */
    @Override
    protected void defineSynchedData() {
        super.defineSynchedData();
        this.entityData.define(DATA_IS_FERTILE, false);
        this.entityData.define(DATA_LANDED, false);
        this.entityData.define(DATA_NUM_EGGS, 1);
        this.entityData.define(DATA_GOAL_STATE, "");
    }

Since this data doesn’t need to be saved, I don’t need to modify the saveAdditionalData() or readAdditionalData() methods.

Now that we have a way of synchronising the data, I modify the customServerAIStep() method, which only runs on the server. I check that the debug flag is active first, which means that the data won’t be synchronised if we are playing the game normally.

    /**
     * A custom step for the AI update loop.
     */
    @Override
    protected void customServerAiStep() {
        super.customServerAiStep();

        // <snip>

        //  Don't do this unless the debug information flag is set.
        if (ButterfliesConfig.debugInformation.get()) {

            // Get the current list of running goals.
            StringBuilder debugOutput = new StringBuilder();
            WrappedGoal[] runningGoals = goalSelector.getRunningGoals().toArray(WrappedGoal[]::new);

            // Convert each goal to a string and add it to the list.
            for (WrappedGoal goal : runningGoals) {
                debugOutput.append(goal.getGoal());
                debugOutput.append(" / ");
            }

            // Set the data so that it will get synchronised to the client.
            setGoalState(debugOutput.toString());
        }
    }

The data being synchronised is just the list of goals represented as a string. To ensure that the correct string is displayed, I override toString() in all of my custom goals. For the Rest goal, I add a bit more information so I can see what position the butterflies are aiming for and what the various flags are set to:

    /**
     * Used for debug information.
     * @return The name of the goal.
     */
    @NotNull
    @Override
    public String toString() {
        return "Rest / Target: " + this.getMoveToTarget() +
        " / Position: " + butterfly.getOnPos() +
        " / Reached: " + this.isReachedTarget() +
        " / Landed: " + butterfly.getIsLanded();
    }

Being able to see this on the fly in-game will really help in figuring out what each butterfly is doing while the game is running.

With the data being synchronised correctly, we now need to actually render the text.

Rendering


Rendering the text is relatively simple. I add a new method to the ButterflyRenderer that is called by the render() method. This method is loosely based on the renderNameTag() method in EntityRenderer.

    /**
     * Renders debug information for the butterfly.
     * @param butterfly The butterfly entity.
     * @param poseStack The current pose stack.
     * @param multiBufferSource The render buffer.
     * @param packedLightCoordinates The light coordinates.
     */
    private void renderDebugInfo(Butterfly butterfly,
                                 PoseStack poseStack,
                                 MultiBufferSource multiBufferSource,
                                 int packedLightCoordinates) {
        if (ButterfliesConfig.debugInformation.get()) {
            String debugOutput = butterfly.getGoalState();
            if (!debugOutput.isBlank()) {

                MutableComponent component = Component.literal(debugOutput);

                float nameTagOffsetY = butterfly.getNameTagOffsetY();
                poseStack.pushPose();
                poseStack.translate(0.0F, nameTagOffsetY, 0.0F);
                poseStack.mulPose(this.entityRenderDispatcher.cameraOrientation());
                poseStack.scale(-0.025F, -0.025F, 0.025F);
                Matrix4f pose = poseStack.last().pose();
                float backgroundOpacity = Minecraft.getInstance().options.getBackgroundOpacity(0.25F);
                int alpha = (int) (backgroundOpacity * 255.0F) << 24;
                Font font = this.getFont();
                float fontWidth = (float) (-font.width(component) / 2);
                font.drawInBatch(component, fontWidth, 0, 553648127, false, pose, multiBufferSource, Font.DisplayMode.SEE_THROUGH, alpha, packedLightCoordinates);
                font.drawInBatch(component, fontWidth, 0, -1, false, pose, multiBufferSource, Font.DisplayMode.NORMAL, 0, packedLightCoordinates);

                poseStack.popPose();
            }
        }
    }

This method calls Butterfly‘s getGoalState() method to fetch any data that might have been synchronised from the server. If it finds any, then it renders the string above the butterfly. The translations are copied verbatim from renderNameTag() so the string will render in the same place a nametag would render.

Fixing


By observing the goal transitions in real-time, I noticed butterflies leaving and re-entering the Rest goal, which resets the isLanded flag to false. This causes the butterfly to not believe it is landed anymore, and it will start flapping its wings again.

The default behaviour for the Rest goal is to always leave the goal after a short amount of time. When the goal is exited, the isLanded flag is reset. So, even if the butterfly goes back into the Rest goal, it no longer believes it has landed anymore.

To fix this, I rewrote the canContinueToUse() method so that it doesn’t rely on behaviour from its super-class. This means the butterfly can remain in the goal indefinitely and will remain in the Landed state until a new goal takes over.

    /**
     * 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() && this.isValidTarget(this.mob.level(), this.blockPos);
    }

The other thing I noticed is that occasionally butterflies get trapped and stop a little short of their goal. I had a couple of lines of code that would stop a butterfly moving if it reached its goal, but removing these two lines seems to make their movement smoother and reduces the chances of this happening.

New Toy


With this tool I can see the state of all butterflies as they fly around. Before this I would have to rely on break points or generating log output, but with this tool I can see the state of multiple butterflies in real time. If there are any more bugs in the butterfly’s behaviour, this will help me figure out what they are trying to do and why.

So now butterfly landing is more consistent and looks the way it originally designed to. It seems like it may have been a lot of work just to fix an animation glitch, but this debugging tool isn’t just a fix for an animation glitch. It’s also a versatile aid for future AI enhancements and fixes. By making butterfly behavior visible in real time, it opens the door to smoother, more immersive interactions and helps identify subtle bugs more efficiently.

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.