I’ve always struggled to get my head around modern day rendering code. There’s a reason I became a network and server programmer. Still, I managed to get this working in the end. Though I’m sure my computer knows many more swear works now…
1
Maintaining a separate branch for development has already paid off. I decided to play some of the mod packs that had already included Bok’s Butterflies. At the time of writing there are 6 such mod packs. While playing SteinCraft RPG, I noticed that it had a mod that displays entity names above their heads. Naturally, I wanted to see how a butterfly would appear with this mod, so I went looking for one a
Ah. I guess I didn’t add translation strings for this. I don’t think they’re actually used anywhere in vanilla Minecraft, which is why I never noticed. Thankfully, since I have a separate branch for the next version of the mod, I was able to create a quick fix in the main branch. I replaced the mod in the pack with my new version, loaded into my game and…
After this it was easy to release a new version that would support any mod pack using these strings.
2
Last week I implemented Butterfly Scrolls in the development branch. I wanted players to be able to place these in the world – creating another way to collect the butterflies. This change took me a while to figure out, but I managed to get there in the end.
I based this code on a HangingEntity
and used vanilla Item Frames as a reference. It took me a long time to implement the Butterfly Scroll entity class. No matter what I did, I couldn’t get the entities to rotate and connect to the surface I put them on. Everything seemed to be in place, but it just wouldn’t rotate.
/** * An entity representing a hanging butterfly scroll. */ public class ButterflyScroll extends HangingEntity { /** * The name used for registration. */ public static final String NAME = "butterfly_scroll"; /** * The index of the butterfly on the scroll. */ private int butterflyIndex; /** * Create method, used to register the entity. * @param entityType The type of the entity. * @param level The current level. * @return A new Butterfly Scroll Entity. */ @NotNull public static ButterflyScroll create(EntityType<? extends ButterflyScroll> entityType, Level level) { return new ButterflyScroll(entityType, level); } /** * Create a Butterfly Scroll Entity. * @param level The current level. * @param blockPos The position of the block it is being placed upon. * @param direction The direction the scroll is facing. */ public ButterflyScroll(Level level, BlockPos blockPos, Direction direction) { this(EntityTypeRegistry.BUTTERFLY_SCROLL.get(), level); this.pos = blockPos; this.setDirection(direction); } /** * Get the index of the butterfly. * @return The butterfly index. */ public int getButterflyIndex() { return this.butterflyIndex; } /** * Get the height of the scroll. * @return The height of the scroll. */ @Override public int getHeight() { return 21; } /** * Get the width of the scroll. * @return The width of the scroll. */ @Override public int getWidth() { return 16; } /** * Play the sound for placing a scroll. */ @Override public void playPlacementSound() { this.playSound(SoundEvents.ITEM_FRAME_PLACE, 1.0F, 1.0F); } /** * Recalculate the bounding box of the scroll. */ @Override protected void recalculateBoundingBox() { // Direction can actually be null even if intellisense says otherwise. if (this.direction != null) { double x = (double) this.pos.getX() + 0.5D - (double) this.direction.getStepX() * 0.46875D; double y = (double) this.pos.getY() + 0.5D - (double) this.direction.getStepY() * 0.46875D; double z = (double) this.pos.getZ() + 0.5D - (double) this.direction.getStepZ() * 0.46875D; this.setPosRaw(x, y, z); double width = this.getWidth(); double height = this.getHeight(); double breadth = this.getWidth(); Direction.Axis axis = this.direction.getAxis(); switch (axis) { case X -> width = 1.0D; case Y -> height = 1.0D; case Z -> breadth = 1.0D; default -> { } } width /= 32.0D; height /= 32.0D; breadth /= 32.0D; this.setBoundingBox(new AABB(x - width, y - height, z - breadth, x + width, y + height, z + breadth)); } } /** * Set the butterfly index. * @param index The index of the butterfly. */ public void setButterflyIndex(int index) { this.butterflyIndex = index; } /** * Set the direction and rotate the entity, so it faces the correct way. * @param direction The direction the entity is facing. */ @Override protected void setDirection(@NotNull Direction direction) { Validate.notNull(direction); this.direction = direction; this.setXRot(0.0F); this.setYRot((float)(this.direction.get2DDataValue() * 90)); this.xRotO = this.getXRot(); this.yRotO = this.getYRot(); this.recalculateBoundingBox(); } /** * Default constructor. * @param entityType The type of the entity. * @param level The current level. */ private ButterflyScroll(EntityType<? extends ButterflyScroll> entityType, Level level) { super(entityType, level); } }
After a long and heavy debugging session, I finally realised that HangingEntitity
s aren’t synchronised over a network by default that I managed to get it working. Minecraft in single player runs as a listen-server, meaning that it stull behaves as if there was a server and a client, just running both on the local machine. In this case, the client was maintaining the default “south” orientation, since the server never told it the direction it was supposed to use.
/** * Add extra data for this entity to a save. * @param tag The tag with the entity's save data. */ @Override public void addAdditionalSaveData(@NotNull CompoundTag tag) { super.addAdditionalSaveData(tag); tag.putByte("Facing", (byte)this.direction.get3DDataValue()); tag.putInt(CompoundTagId.CUSTOM_MODEL_DATA, this.butterflyIndex); } /** * Send entity data to the client. * @return The packet to send. */ @NotNull @Override public Packet<ClientGamePacketListener> getAddEntityPacket() { int data = ((this.direction.get3DDataValue() & 0xFFFF) << 16) | (butterflyIndex & 0xFFFF); return new ClientboundAddEntityPacket(this, data, this.getPos()); } /** * Read the entity's data from a save. * @param tag The tag loaded from the save data. */ @Override public void readAdditionalSaveData(@NotNull CompoundTag tag) { super.readAdditionalSaveData(tag); this.setDirection(Direction.from3DDataValue(tag.getByte("Facing"))); if (tag.contains(CompoundTagId.CUSTOM_MODEL_DATA)) { this.butterflyIndex = tag.getInt(CompoundTagId.CUSTOM_MODEL_DATA); } } /** * Recreate the entity from a received packet. * @param packet The packet sent from the server. */ @Override public void recreateFromPacket(@NotNull ClientboundAddEntityPacket packet) { super.recreateFromPacket(packet); int data = packet.getData();; int direction = ((data >> 16) & 0xFFFF); this.butterflyIndex = (data & 0xFFFF); this.setDirection(Direction.from3DDataValue(direction)); } }
You’ll notice that I use bit packing in order to store two values. Since this isn’t a LivingEntity
, there isn’t as much code to support replication. By default you can only synchronise one integer. I could write extra code to support more values, but these values will always be small, so it’s quicker and easier just to send them together.
The final step was to write some code that would drop the correct item when a poster is destroyed. This part was pretty simple, as HangingEntity
provides a method we can override for this.
/** * Drop a Butterfly Scroll when this gets destroyed * @param entity The player entity. */ @Override public void dropItem(@Nullable Entity entity) { ItemStack stack = new ItemStack(ItemRegistry.BUTTERFLY_SCROLL.get()); ButterflyContainerItem.setButterfly(stack, "butterflies:" + ButterflyIds.IndexToEntityId(this.butterflyIndex)); this.spawnAtLocation(stack); }
I have the entities positioned correctly now, but as you can see, rendering them properly is still going to need some work.
3
To start with, we need a model to render. I created a simple model that was essentially just a single plane. I wanted to reuse the GUI textures here, so I made sure it was big enough to accommodate the texture.
@OnlyIn(Dist.CLIENT) public class ButterflyScrollModel extends Model { /** * The root of the model. */ private final ModelPart root; /** * The layer location to register with Forge. */ public static final ModelLayerLocation LAYER_LOCATION = new ModelLayerLocation(new ResourceLocation(ButterfliesMod.MODID, "butterfly_scroll"), "main"); /** * Defines a simple model for the Butterfly Scroll. * @return A new layer definition. */ public static LayerDefinition createBodyLayer() { MeshDefinition meshDefinition = new MeshDefinition(); PartDefinition partDefinition = meshDefinition.getRoot(); partDefinition.addOrReplaceChild( "main", CubeListBuilder.create() .texOffs(26, 8) .addBox(-129.0F, -165.0F, -1.0F, 130.0F, 165.0F, 0.0F, new CubeDeformation(0.0F)), PartPose.offset(0.0F, 24.0F, 0.0F)); return LayerDefinition.create(meshDefinition, 256, 256); } /** * Construction. * @param root The root of the model. */ public ButterflyScrollModel(ModelPart root) { super(RenderType::entityCutoutNoCull); this.root = root.getChild("main"); } /** * Render the model to the specified buffer. * @param poseStack The current translation stack. * @param vertexConsumer The vertices to render. * @param packedLight The current light. * @param packedOverlay The overlay. * @param red Red tint. * @param green Green tint. * @param blue Blue tint. * @param alpha Alpha tint. */ @Override public void renderToBuffer(@NotNull PoseStack poseStack, @NotNull VertexConsumer vertexConsumer, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) { this.root.render(poseStack, vertexConsumer, packedLight, packedOverlay, red, green, blue, alpha); } }
The renderer comes next. This renderer is extremely simple (though it took me way too long to write properly). I moved the list of textures to a generic class so I could reference them here as well. Since the model was actually pretty large, I scale it down to around 12% so we can support the whole texture.
We also need to rotate it so it faces the correct direction, and offset the texture so it aligns with the bounding box.
/** * Renders a butterfly scroll hanging off a surface. */ public class ButterflyScrollRenderer extends EntityRenderer<ButterflyScroll> { /** * The scroll model for rendering the scroll. */ private final ButterflyScrollModel model; /** * Construction * @param context The rendering context. */ public ButterflyScrollRenderer(EntityRendererProvider.Context context) { super(context); model = new ButterflyScrollModel(context.bakeLayer(ButterflyScrollModel.LAYER_LOCATION)); } /** * Reduce the size of the texture image. * @return Around 12%. */ public float getScale() { return 0.123f; } /** * Get the texture to use. * @param scroll The current scroll. * @return The texture to render with. */ @Override @NotNull public ResourceLocation getTextureLocation(@NotNull ButterflyScroll scroll) { return ButterflyScrollTexture.TEXTURES[scroll.getButterflyIndex()]; } /** * Render the scroll entity. * @param scroll The scroll entity to render. * @param yaw The current yaw. * @param partialTicks The current partial ticks. * @param poseStack The matrix stack. * @param buffers The render buffers. * @param overlay The overlay. */ @Override public void render(@NotNull ButterflyScroll scroll, float yaw, float partialTicks, @NotNull PoseStack poseStack, @NotNull MultiBufferSource buffers, int overlay) { poseStack.pushPose(); poseStack.mulPose(Axis.YP.rotationDegrees(scroll.getDirection().get2DDataValue() * -90)); poseStack.translate(0.5D, -0.5D, 0.0D); float scale = this.getScale(); poseStack.scale(scale, -scale, -scale); RenderType renderType = RenderType.entitySmoothCutout(getTextureLocation(scroll)); VertexConsumer vertexConsumer = buffers.getBuffer(renderType); model.renderToBuffer(poseStack, vertexConsumer, overlay, 0, 1.0f, 1.0f, 1.0f, 1.0f); poseStack.popPose(); }
The last thing is to make sure that the entity is registered properly. This should be obvious by now, but I bring it up because I forgot to register the renderers and model layers when I first tested this. The game crashes when you place a scroll if you don’t do this properly.
/** * The Butterfly Scroll enitity. */ public static final RegistryObject<EntityType<ButterflyScroll>> BUTTERFLY_SCROLL = INSTANCE.register(ButterflyScroll.NAME, () -> EntityType.Builder.of(ButterflyScroll::create, MobCategory.MISC) .sized(1.0f, 1.0f) .build(ButterflyScroll.NAME)); @SubscribeEvent public static void registerEntityRenders(final EntityRenderersEvent.RegisterRenderers event) { event.registerEntityRenderer(BUTTERFLY_SCROLL.get(), ButterflyScrollRenderer::new); /// etc. } /** * Registers models to be used for rendering * @param event The event information */ @SubscribeEvent public static void onRegisterLayers(EntityRenderersEvent.RegisterLayerDefinitions event) { event.registerLayerDefinition(ButterflyModel.LAYER_LOCATION, ButterflyModel::createBodyLayer); event.registerLayerDefinition(CaterpillarModel.LAYER_LOCATION, CaterpillarModel::createBodyLayer); event.registerLayerDefinition(ChrysalisModel.LAYER_LOCATION, ChrysalisModel::createBodyLayer); event.registerLayerDefinition(ButterflyScrollModel.LAYER_LOCATION, ButterflyScrollModel::createBodyLayer); }
We can now place the item in the world and actually see our butterflies pinned to the walls.
One thing I want to highlight is the RenderType
I used. Initially, I used a type that culls faces, and this meant that you couldn’t see the textures from the back. This was a problem when the texture overhangs a block (since it’s larger than one block), and even more serious with glass.
To fix this I had to make sure I was using a non-culling render type. I went with RenderType.entitySmoothCutout
, and now you can see the scrolls from the back as well.
4
This feature is merged into the development branch now, along with a few fixes for minor issues I noticed as I was implementing the feature. One of the larger changes is that all renderers, models, and textures live in the client
package and will only be included in client builds, as they should be.
One thing I did notice is that I often construct strings for items that should really be using ResourceLocation
s. What I have works, but the code would be more robust if I used the proper type. I’ll probably rework this at some point, but for now I need a bit of a break. I still have a couple more things I’d like to add before I release a second version of Bok’s Butterflies.