Fabric 1.20 custom creature lesson

To make Blockbench an elephant a real creature.

This course is organized on the basis of a real realization process: `Elefant.bbmodel ' , and then step by step into the Fabric 1.20 project by looking at stickers, physical registrations, AI, brush eggs, client-end rendering machines and animating. Students will not only reproduce an elephant, but also understand that “the service is responsible for biology and the client is responsible for how it looks”.

Fabric 1.20 Blockbench EntityType Spawn Egg Custom Renderer Spawn Animation

Research recap

We examine the model like a detective before deciding on a code route.

After getting `Elefant.bbmodel`, the first step is not to write the entity class immediately, but to see what format it is. The check result is critical: it is Blockbench's `java_block` model, which has 24 blocks and 7 embedded PNG textures, but does not come with its own animation track.

This means that the course does not simply "play animated models." The route we use is: service ends make a real elephant entity, clients write a custom renderer to read bbmodel and then use Java codes to fill up ears, noses, legs and birth processes.

01

Model Checking

Confirm the file structure, number of textures, number of blocks and whether there is an animation track.

02

Solution selection

Instead of adding GeckoLib, use Fabric native entities + custom renderers.

03

Game Verification

An elephant is automatically generated into the world and can be tested manually with an Elephant Brushing Egg.

Minecraft style elephant creature generated from particles with code panel and Blockbench grid prompts next to it
This picture is generated by AI and is a visual reference to the Blockbench elephant screenshot uploaded this time: gray block body, big ears, trunk, white tusks and model editing grid.
Fabric elephant creature generation animation tutorial mind map
Mind map: from input resources, server entities, client rendering, animation, teaching focus to construction verification.

Mind map

By splitting the "elephant" into six modules, the code will no longer be messy.

  • Enter resources:`Elefant.bbmodel` and 7 textures determine the appearance.
  • Server:The `EntityType`, attributes, AI, and spawn egg determine whether it is a real Minecraft mob.
  • Client:The renderer determines how the model is displayed and how textures are attached to each face.
  • Animation:When there is no Blockbench animation track, Java can be used to do swing and birth effects in the rendering phase.
  • Authentication:Compiling, packaging, entering the world, and testing monster spawn eggs, none of them can be omitted.

Classes remind: students are most likely to mix "physical logic" with "model rendering". Repeatedly, the server knows where the elephant is and whether it will leave; the client is responsible for drawing what it looks like and how its ears move.

Build flow

Implementation process: resources are first implemented, and then the entities are brought to life.

Flow chart of Fabric elephant biology from model research to complete construction
Flow chart: research model, export texture, register entity, add behavior, register rendering, add animation, compile and build, in-game verification.

File map

Newly added and modified files for this lesson.

Entity Registration

ModEntities.java

Register `maik:elephant`, set collision box, tracking distance and default properties.

src/main/java/mls/maik/entity/ModEntities.java
Entity behavior

ElephantEntity.java

Set blood levels, speeds, AI targets, breeding food and birth particles.

src/main/java/mls/maik/entity/custom/ElephantEntity.java
Client

ElephantEntityRenderer.java

Reads bbmodel and draws a model by peg and UV, with animated parts.

src/client/java/mls/maik/client/render/entity/ElephantEntityRenderer.java
Generate entry

ModPlayerEvents.java

After the player enters the world, an elephant is generated at a safe location in front.

src/main/java/mls/maik/event/ModPlayerEvents.java
Item

ModItems.java

Added "elephant monster spawning egg" and added the monster spawning egg column in creative mode.

src/main/java/mls/maik/item/ModItems.java
Resources

bbmodel + textures

Keep the original model file and extract the embedded texture to the entity texture directory.

assets/maik/models/entity/elephant.bbmodel

Complete code

Complete code teaching: Each tab page is a real source code that can be copied.

Maik.java + ModEntities.java
package mls.maik;

import mls.maik.block.ModBlocks;
import mls.maik.entity.ModEntities;
import mls.maik.event.ModPlayerEvents;
import mls.maik.item.ModItems;
import net.fabricmc.api.ModInitializer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Maik implements ModInitializer {
	public static final String MOD_ID = "maik";

	// This logger is used to write text to the console and the log file.
	// It is considered best practice to use your mod id as the logger's name.
	// That way, it's clear which mod wrote info, warnings, and errors.
	public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID);

	@Override
	public void onInitialize() {
		// This code runs as soon as Minecraft is in a mod-load-ready state.
		// However, some things (like resources) may still be uninitialized.
		// Proceed with mild caution.
        ModEntities.registerModEntities();
        ModBlocks.registerModBlocks();
        ModItems.registerModItems();
        ModPlayerEvents.register();
		LOGGER.info("Hello Fabric world!");
	}
}
package mls.maik.entity;

import mls.maik.Maik;
import mls.maik.entity.custom.ElephantEntity;
import net.fabricmc.fabric.api.object.builder.v1.entity.FabricDefaultAttributeRegistry;
import net.fabricmc.fabric.api.object.builder.v1.entity.FabricEntityTypeBuilder;
import net.minecraft.entity.EntityDimensions;
import net.minecraft.entity.EntityType;
import net.minecraft.entity.SpawnGroup;
import net.minecraft.registry.Registries;
import net.minecraft.registry.Registry;
import net.minecraft.util.Identifier;

public class ModEntities {
    public static final EntityType<ElephantEntity> ELEPHANT = Registry.register(
            Registries.ENTITY_TYPE,
            new Identifier(Maik.MOD_ID, "elephant"),
            FabricEntityTypeBuilder.create(SpawnGroup.CREATURE, ElephantEntity::new)
                    .dimensions(EntityDimensions.fixed(2.25F, 2.75F))
                    .trackRangeBlocks(10)
                    .trackedUpdateRate(3)
                    .build()
    );

    public static void registerModEntities() {
        FabricDefaultAttributeRegistry.register(ELEPHANT, ElephantEntity.createElephantAttributes());
        Maik.LOGGER.info("Registering ModEntities for " + Maik.MOD_ID);
    }
}
ElephantEntity.java
package mls.maik.entity.custom;

import mls.maik.entity.ModEntities;
import net.minecraft.entity.EntityType;
import net.minecraft.entity.ai.goal.AnimalMateGoal;
import net.minecraft.entity.ai.goal.FollowParentGoal;
import net.minecraft.entity.ai.goal.LookAroundGoal;
import net.minecraft.entity.ai.goal.LookAtEntityGoal;
import net.minecraft.entity.ai.goal.SwimGoal;
import net.minecraft.entity.ai.goal.TemptGoal;
import net.minecraft.entity.ai.goal.WanderAroundFarGoal;
import net.minecraft.entity.attribute.DefaultAttributeContainer;
import net.minecraft.entity.attribute.EntityAttributes;
import net.minecraft.entity.mob.MobEntity;
import net.minecraft.entity.passive.AnimalEntity;
import net.minecraft.entity.passive.PassiveEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.ItemStack;
import net.minecraft.item.Items;
import net.minecraft.particle.ParticleTypes;
import net.minecraft.recipe.Ingredient;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.world.World;
import org.jetbrains.annotations.Nullable;

public class ElephantEntity extends AnimalEntity {
    public static final int SPAWN_ANIMATION_TICKS = 42;
    private static final Ingredient BREEDING_INGREDIENT = Ingredient.ofItems(Items.SUGAR_CANE, Items.WHEAT, Items.APPLE);

    public ElephantEntity(EntityType<? extends AnimalEntity> entityType, World world) {
        super(entityType, world);
    }

    public static DefaultAttributeContainer.Builder createElephantAttributes() {
        return MobEntity.createMobAttributes()
                .add(EntityAttributes.GENERIC_MAX_HEALTH, 44.0D)
                .add(EntityAttributes.GENERIC_MOVEMENT_SPEED, 0.18D)
                .add(EntityAttributes.GENERIC_FOLLOW_RANGE, 24.0D)
                .add(EntityAttributes.GENERIC_KNOCKBACK_RESISTANCE, 0.65D);
    }

    @Override
    protected void initGoals() {
        this.goalSelector.add(0, new SwimGoal(this));
        this.goalSelector.add(1, new AnimalMateGoal(this, 0.6D));
        this.goalSelector.add(2, new TemptGoal(this, 0.75D, BREEDING_INGREDIENT, false));
        this.goalSelector.add(3, new FollowParentGoal(this, 0.7D));
        this.goalSelector.add(4, new WanderAroundFarGoal(this, 0.55D));
        this.goalSelector.add(5, new LookAtEntityGoal(this, PlayerEntity.class, 8.0F));
        this.goalSelector.add(6, new LookAroundGoal(this));
    }

    @Override
    public boolean isBreedingItem(ItemStack stack) {
        return BREEDING_INGREDIENT.test(stack);
    }

    @Nullable
    @Override
    public PassiveEntity createChild(ServerWorld world, PassiveEntity entity) {
        return ModEntities.ELEPHANT.create(world);
    }

    @Override
    public void tickMovement() {
        super.tickMovement();

        if (this.getWorld().isClient && this.age < SPAWN_ANIMATION_TICKS && this.age % 3 == 0) {
            for (int i = 0; i < 4; i++) {
                this.getWorld().addParticle(
                        ParticleTypes.POOF,
                        this.getParticleX(1.2D),
                        this.getY() + 0.05D,
                        this.getParticleZ(1.2D),
                        (this.random.nextDouble() - 0.5D) * 0.03D,
                        0.035D,
                        (this.random.nextDouble() - 0.5D) * 0.03D
                );
            }
        }
    }
}
ModItems.java + ModPlayerEvents.java
package mls.maik.item;

import mls.maik.Maik;
import mls.maik.entity.ModEntities;
import net.fabricmc.fabric.api.itemgroup.v1.ItemGroupEvents;
import net.minecraft.item.Item;
import net.minecraft.item.ItemGroups;
import net.minecraft.item.Items;
import net.minecraft.item.SpawnEggItem;
import net.minecraft.registry.Registries;
import net.minecraft.registry.Registry;
import net.minecraft.util.Identifier;

public class ModItems {
    public static final Item MY_NEW_AXE = registerItem("mynewaxe",
            new MyNewAxeItem(ModToolMaterials.MYNEWAXE, 10, -2.8f, new Item.Settings().fireproof()));
    public static final Item ELEPHANT_SPAWN_EGG = registerItem("elephant_spawn_egg",
            new SpawnEggItem(ModEntities.ELEPHANT, 0x6F6A62, 0xE4D8C2, new Item.Settings()));

    private static Item registerItem(String name, Item item) {
        return Registry.register(Registries.ITEM, new Identifier(Maik.MOD_ID, name), item);
    }

    public static void registerModItems() {
        ItemGroupEvents.modifyEntriesEvent(ItemGroups.TOOLS)
                .register(entries -> entries.addAfter(Items.NETHERITE_PICKAXE, MY_NEW_AXE));
        ItemGroupEvents.modifyEntriesEvent(ItemGroups.SPAWN_EGGS)
                .register(entries -> entries.add(ELEPHANT_SPAWN_EGG));
        Maik.LOGGER.info("Registering ModItems for " + Maik.MOD_ID);
    }
}
package mls.maik.event;

import mls.maik.entity.ModEntities;
import mls.maik.entity.custom.ElephantEntity;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.Direction;

public class ModPlayerEvents {
    private static final int SPAWN_DISTANCE = 5;

    public static void register() {
        ServerPlayConnectionEvents.JOIN.register((handler, sender, server) -> {
            ServerPlayerEntity player = handler.getPlayer();
            spawnElephantInFront(player);
        });
    }

    private static void spawnElephantInFront(ServerPlayerEntity player) {
        ServerWorld world = player.getServerWorld();
        Direction forward = player.getHorizontalFacing();
        BlockPos center = player.getBlockPos().offset(forward, SPAWN_DISTANCE);

        ElephantEntity elephant = ModEntities.ELEPHANT.create(world);
        if (elephant != null) {
            BlockPos safePos = findSafeSpawnPos(world, center);
            elephant.refreshPositionAndAngles(
                    safePos.getX() + 0.5,
                    safePos.getY(),
                    safePos.getZ() + 0.5,
                    player.getYaw() + 180.0F,
                    0.0F
            );
            world.spawnEntity(elephant);
        }
    }

    private static BlockPos findSafeSpawnPos(ServerWorld world, BlockPos startPos) {
        for (int yOffset = 2; yOffset >= -3; yOffset--) {
            BlockPos pos = startPos.up(yOffset);
            if (world.getBlockState(pos).isAir()
                    && world.getBlockState(pos.up()).isAir()
                    && world.getBlockState(pos.up(2)).isAir()
                    && !world.getBlockState(pos.down()).isAir()) {
                return pos;
            }
        }

        return startPos;
    }
}
MaikClient.java + ElephantEntityRenderer.java
package mls.maik.client;

import mls.maik.client.render.entity.ElephantEntityRenderer;
import mls.maik.client.screen.MyScreen;
import mls.maik.entity.ModEntities;
import mls.maik.network.ModNetworking;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking;
import net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry;
import net.minecraft.client.MinecraftClient;

public class MaikClient implements ClientModInitializer {

    @Override
    public void onInitializeClient() {
        EntityRendererRegistry.register(ModEntities.ELEPHANT, ElephantEntityRenderer::new);

        ClientPlayNetworking.registerGlobalReceiver(ModNetworking.OPEN_GUI_PACKET,
                (client, handler, buf, responseSender) -> {

                    client.execute(() -> {
                        MinecraftClient.getInstance().setScreen(new MyScreen());
                    });

                });
    }
}
package mls.maik.client.render.entity;

import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import mls.maik.Maik;
import mls.maik.entity.custom.ElephantEntity;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.render.OverlayTexture;
import net.minecraft.client.render.RenderLayer;
import net.minecraft.client.render.VertexConsumer;
import net.minecraft.client.render.VertexConsumerProvider;
import net.minecraft.client.render.entity.EntityRenderer;
import net.minecraft.client.render.entity.EntityRendererFactory;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.resource.Resource;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.MathHelper;
import net.minecraft.util.math.RotationAxis;

import java.io.BufferedReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;

public class ElephantEntityRenderer extends EntityRenderer<ElephantEntity> {
    private static final Identifier MODEL_ID = new Identifier(Maik.MOD_ID, "models/entity/elephant.bbmodel");
    private static final Identifier FALLBACK_TEXTURE = new Identifier(Maik.MOD_ID, "textures/entity/elephant/koeper.png");
    private static final float PIXEL = 1.0F / 16.0F;
    private static final float MODEL_SCALE = 2.05F;
    private static final float[] MODEL_CENTER = {4.5F, 0.0F, 4.5F};
    private BlockbenchModel model;

    public ElephantEntityRenderer(EntityRendererFactory.Context context) {
        super(context);
        this.shadowRadius = 0.9F;
    }

    @Override
    public void render(ElephantEntity entity, float yaw, float tickDelta, MatrixStack matrices,
                       VertexConsumerProvider vertexConsumers, int light) {
        BlockbenchModel elephantModel = getModel();
        float spawnProgress = getSpawnProgress(entity, tickDelta);
        float birthScale = 0.18F + 0.82F * spawnProgress;

        matrices.push();
        matrices.translate(0.0D, -0.85D * (1.0F - spawnProgress), 0.0D);
        matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(180.0F - yaw));
        matrices.scale(MODEL_SCALE * birthScale, MODEL_SCALE * birthScale, MODEL_SCALE * birthScale);
        matrices.translate(-MODEL_CENTER[0] * PIXEL, 0.0F, -MODEL_CENTER[2] * PIXEL);

        elephantModel.render(entity, tickDelta, spawnProgress, matrices, vertexConsumers, light);
        matrices.pop();

        super.render(entity, yaw, tickDelta, matrices, vertexConsumers, light);
    }

    @Override
    public Identifier getTexture(ElephantEntity entity) {
        return FALLBACK_TEXTURE;
    }

    private BlockbenchModel getModel() {
        if (this.model == null) {
            this.model = BlockbenchModel.load();
        }

        return this.model;
    }

    private static float getSpawnProgress(ElephantEntity entity, float tickDelta) {
        float raw = MathHelper.clamp((entity.age + tickDelta) / ElephantEntity.SPAWN_ANIMATION_TICKS, 0.0F, 1.0F);
        return raw * raw * (3.0F - 2.0F * raw);
    }

    private static final class BlockbenchModel {
        private final List<Cube> cubes;
        private final TextureRef[] textures;
        private final float uvWidth;
        private final float uvHeight;

        private BlockbenchModel(List<Cube> cubes, TextureRef[] textures, float uvWidth, float uvHeight) {
            this.cubes = cubes;
            this.textures = textures;
            this.uvWidth = uvWidth;
            this.uvHeight = uvHeight;
        }

        private static BlockbenchModel load() {
            try {
                Optional<Resource> resource = MinecraftClient.getInstance().getResourceManager().getResource(MODEL_ID);
                if (resource.isEmpty()) {
                    Maik.LOGGER.error("Missing elephant model resource: {}", MODEL_ID);
                    return empty();
                }

                try (BufferedReader reader = resource.get().getReader()) {
                    JsonObject root = JsonParser.parseReader(reader).getAsJsonObject();
                    float uvWidth = getResolution(root, "width", 32.0F);
                    float uvHeight = getResolution(root, "height", 32.0F);
                    TextureRef[] textures = parseTextures(root);
                    Map<String, JsonObject> elements = objectsByUuid(root.getAsJsonArray("elements"));
                    Map<String, JsonObject> groups = objectsByUuid(root.getAsJsonArray("groups"));
                    List<Cube> cubes = new ArrayList<>();

                    JsonArray outliner = root.getAsJsonArray("outliner");
                    if (outliner != null) {
                        for (JsonElement entry : outliner) {
                            collectCubes(entry, "root", elements, groups, cubes);
                        }
                    }

                    return new BlockbenchModel(cubes, textures, uvWidth, uvHeight);
                }
            } catch (IOException | IllegalStateException exception) {
                Maik.LOGGER.error("Could not load elephant model", exception);
                return empty();
            }
        }

        private static BlockbenchModel empty() {
            return new BlockbenchModel(List.of(), new TextureRef[]{new TextureRef(FALLBACK_TEXTURE)}, 32.0F, 32.0F);
        }

        private void render(ElephantEntity entity, float tickDelta, float spawnProgress, MatrixStack matrices,
                            VertexConsumerProvider vertexConsumers, int light) {
            float age = entity.age + tickDelta;
            float limbAngle = entity.limbAnimator.getPos(tickDelta);
            float limbDistance = MathHelper.clamp(entity.limbAnimator.getSpeed(tickDelta), 0.0F, 1.0F);
            float headYaw = MathHelper.wrapDegrees(
                    MathHelper.lerp(tickDelta, entity.prevHeadYaw, entity.headYaw)
                            - MathHelper.lerp(tickDelta, entity.prevBodyYaw, entity.bodyYaw)
            );
            float headPitch = MathHelper.lerp(tickDelta, entity.prevPitch, entity.getPitch());

            for (Cube cube : this.cubes) {
                matrices.push();
                applyAnimatedTransform(cube, matrices, age, limbAngle, limbDistance, headYaw, headPitch, spawnProgress);
                applyRotation(matrices, cube.origin(), cube.rotation()[0], cube.rotation()[1], cube.rotation()[2]);
                renderCube(cube, matrices, vertexConsumers, light);
                matrices.pop();
            }
        }

        private void renderCube(Cube cube, MatrixStack matrices, VertexConsumerProvider vertexConsumers, int light) {
            float x1 = cube.from()[0] * PIXEL;
            float y1 = cube.from()[1] * PIXEL;
            float z1 = cube.from()[2] * PIXEL;
            float x2 = cube.to()[0] * PIXEL;
            float y2 = cube.to()[1] * PIXEL;
            float z2 = cube.to()[2] * PIXEL;

            for (Map.Entry<CubeFace, Face> faceEntry : cube.faces().entrySet()) {
                Face face = faceEntry.getValue();
                if (face.textureIndex() < 0 || face.textureIndex() >= this.textures.length) {
                    continue;
                }

                VertexConsumer vertices = vertexConsumers.getBuffer(RenderLayer.getEntityCutoutNoCull(this.textures[face.textureIndex()].id()));
                emitFace(vertices, matrices, light, faceEntry.getKey(), face, x1, y1, z1, x2, y2, z2);
            }
        }

        private void emitFace(VertexConsumer vertices, MatrixStack matrices, int light, CubeFace cubeFace, Face face,
                              float x1, float y1, float z1, float x2, float y2, float z2) {
            float u0 = face.uv()[0] / this.uvWidth;
            float v0 = face.uv()[1] / this.uvHeight;
            float u1 = face.uv()[2] / this.uvWidth;
            float v1 = face.uv()[3] / this.uvHeight;

            switch (cubeFace) {
                case NORTH -> {
                    vertex(vertices, matrices, x1, y1, z1, u0, v1, 0.0F, 0.0F, -1.0F, light);
                    vertex(vertices, matrices, x1, y2, z1, u0, v0, 0.0F, 0.0F, -1.0F, light);
                    vertex(vertices, matrices, x2, y2, z1, u1, v0, 0.0F, 0.0F, -1.0F, light);
                    vertex(vertices, matrices, x2, y1, z1, u1, v1, 0.0F, 0.0F, -1.0F, light);
                }
                case SOUTH -> {
                    vertex(vertices, matrices, x2, y1, z2, u0, v1, 0.0F, 0.0F, 1.0F, light);
                    vertex(vertices, matrices, x2, y2, z2, u0, v0, 0.0F, 0.0F, 1.0F, light);
                    vertex(vertices, matrices, x1, y2, z2, u1, v0, 0.0F, 0.0F, 1.0F, light);
                    vertex(vertices, matrices, x1, y1, z2, u1, v1, 0.0F, 0.0F, 1.0F, light);
                }
                case EAST -> {
                    vertex(vertices, matrices, x2, y1, z1, u0, v1, 1.0F, 0.0F, 0.0F, light);
                    vertex(vertices, matrices, x2, y2, z1, u0, v0, 1.0F, 0.0F, 0.0F, light);
                    vertex(vertices, matrices, x2, y2, z2, u1, v0, 1.0F, 0.0F, 0.0F, light);
                    vertex(vertices, matrices, x2, y1, z2, u1, v1, 1.0F, 0.0F, 0.0F, light);
                }
                case WEST -> {
                    vertex(vertices, matrices, x1, y1, z2, u0, v1, -1.0F, 0.0F, 0.0F, light);
                    vertex(vertices, matrices, x1, y2, z2, u0, v0, -1.0F, 0.0F, 0.0F, light);
                    vertex(vertices, matrices, x1, y2, z1, u1, v0, -1.0F, 0.0F, 0.0F, light);
                    vertex(vertices, matrices, x1, y1, z1, u1, v1, -1.0F, 0.0F, 0.0F, light);
                }
                case UP -> {
                    vertex(vertices, matrices, x1, y2, z1, u0, v1, 0.0F, 1.0F, 0.0F, light);
                    vertex(vertices, matrices, x1, y2, z2, u0, v0, 0.0F, 1.0F, 0.0F, light);
                    vertex(vertices, matrices, x2, y2, z2, u1, v0, 0.0F, 1.0F, 0.0F, light);
                    vertex(vertices, matrices, x2, y2, z1, u1, v1, 0.0F, 1.0F, 0.0F, light);
                }
                case DOWN -> {
                    vertex(vertices, matrices, x1, y1, z2, u0, v1, 0.0F, -1.0F, 0.0F, light);
                    vertex(vertices, matrices, x1, y1, z1, u0, v0, 0.0F, -1.0F, 0.0F, light);
                    vertex(vertices, matrices, x2, y1, z1, u1, v0, 0.0F, -1.0F, 0.0F, light);
                    vertex(vertices, matrices, x2, y1, z2, u1, v1, 0.0F, -1.0F, 0.0F, light);
                }
            }
        }

        private static void vertex(VertexConsumer vertices, MatrixStack matrices, float x, float y, float z,
                                   float u, float v, float normalX, float normalY, float normalZ, int light) {
            MatrixStack.Entry entry = matrices.peek();
            vertices.vertex(entry.getPositionMatrix(), x, y, z)
                    .color(255, 255, 255, 255)
                    .texture(u, v)
                    .overlay(OverlayTexture.DEFAULT_UV)
                    .light(light)
                    .normal(entry.getNormalMatrix(), normalX, normalY, normalZ)
                    .next();
        }

        private static void applyAnimatedTransform(Cube cube, MatrixStack matrices, float age, float limbAngle,
                                                   float limbDistance, float headYaw, float headPitch,
                                                   float spawnProgress) {
            String group = cube.groupName();
            float spawnKick = 1.0F - spawnProgress;

            if (isHeadGroup(group)) {
                float idleNod = MathHelper.sin(age * 0.08F) * 1.2F;
                applyRotation(matrices, cube.origin(), headPitch * 0.18F + idleNod, headYaw * 0.28F, 0.0F);
            }

            if ("Bein".equals(group)) {
                float frontBackPhase = cube.from()[2] < 8.0F ? 0.0F : (float) Math.PI;
                float sidePhase = cube.from()[0] < 8.0F ? 0.0F : (float) Math.PI;
                float walk = MathHelper.cos(limbAngle * 0.72F + frontBackPhase + sidePhase) * limbDistance * 18.0F;
                applyRotation(matrices, cube.origin(), walk, 0.0F, 0.0F);
            } else if ("Nase".equals(group)) {
                float sway = MathHelper.sin(age * 0.15F) * 4.5F + spawnKick * 18.0F;
                applyRotation(matrices, cube.origin(), sway, 0.0F, 0.0F);
            } else if ("OhrL".equals(group)) {
                float flap = 9.0F + MathHelper.sin(age * 0.19F) * 6.0F + spawnKick * 30.0F;
                applyRotation(matrices, cube.origin(), 0.0F, 0.0F, flap);
            } else if ("OhrR".equals(group)) {
                float flap = -9.0F - MathHelper.sin(age * 0.19F) * 6.0F - spawnKick * 30.0F;
                applyRotation(matrices, cube.origin(), 0.0F, 0.0F, flap);
            } else if ("Schwanz".equals(group)) {
                float tail = MathHelper.sin(age * 0.13F) * 10.0F;
                applyRotation(matrices, cube.origin(), 0.0F, tail, 0.0F);
            } else if ("Koeper".equals(group)) {
                float breathing = MathHelper.sin(age * 0.07F) * 0.01F;
                matrices.translate(0.0F, breathing, 0.0F);
            }
        }

        private static boolean isHeadGroup(String group) {
            return "Kopf".equals(group) || "Nase".equals(group) || "Zaehne".equals(group)
                    || "OhrL".equals(group) || "OhrR".equals(group);
        }

        private static void applyRotation(MatrixStack matrices, float[] origin, float xDegrees, float yDegrees, float zDegrees) {
            if (xDegrees == 0.0F && yDegrees == 0.0F && zDegrees == 0.0F) {
                return;
            }

            matrices.translate(origin[0] * PIXEL, origin[1] * PIXEL, origin[2] * PIXEL);
            if (xDegrees != 0.0F) {
                matrices.multiply(RotationAxis.POSITIVE_X.rotationDegrees(xDegrees));
            }
            if (yDegrees != 0.0F) {
                matrices.multiply(RotationAxis.POSITIVE_Y.rotationDegrees(yDegrees));
            }
            if (zDegrees != 0.0F) {
                matrices.multiply(RotationAxis.POSITIVE_Z.rotationDegrees(zDegrees));
            }
            matrices.translate(-origin[0] * PIXEL, -origin[1] * PIXEL, -origin[2] * PIXEL);
        }

        private static TextureRef[] parseTextures(JsonObject root) {
            JsonArray textureArray = root.getAsJsonArray("textures");
            if (textureArray == null || textureArray.isEmpty()) {
                return new TextureRef[]{new TextureRef(FALLBACK_TEXTURE)};
            }

            TextureRef[] textureRefs = new TextureRef[textureArray.size()];
            for (int i = 0; i < textureArray.size(); i++) {
                JsonObject texture = textureArray.get(i).getAsJsonObject();
                String name = texture.get("name").getAsString();
                String fileName = name.substring(0, name.lastIndexOf('.')).toLowerCase(Locale.ROOT);
                textureRefs[i] = new TextureRef(new Identifier(Maik.MOD_ID, "textures/entity/elephant/" + fileName + ".png"));
            }

            return textureRefs;
        }

        private static Map<String, JsonObject> objectsByUuid(JsonArray array) {
            Map<String, JsonObject> result = new HashMap<>();
            if (array == null) {
                return result;
            }

            for (JsonElement element : array) {
                JsonObject object = element.getAsJsonObject();
                if (object.has("uuid")) {
                    result.put(object.get("uuid").getAsString(), object);
                }
            }

            return result;
        }

        private static void collectCubes(JsonElement outlinerEntry, String currentGroup,
                                         Map<String, JsonObject> elements, Map<String, JsonObject> groups,
                                         List<Cube> cubes) {
            if (outlinerEntry.isJsonPrimitive()) {
                JsonObject element = elements.get(outlinerEntry.getAsString());
                if (element != null) {
                    cubes.add(parseCube(element, currentGroup));
                }
                return;
            }

            JsonObject groupEntry = outlinerEntry.getAsJsonObject();
            String uuid = groupEntry.get("uuid").getAsString();
            JsonObject group = groups.get(uuid);
            String groupName = group != null && group.has("name") ? group.get("name").getAsString() : currentGroup;
            JsonArray children = groupEntry.getAsJsonArray("children");
            if (children == null) {
                return;
            }

            for (JsonElement child : children) {
                collectCubes(child, groupName, elements, groups, cubes);
            }
        }

        private static Cube parseCube(JsonObject element, String groupName) {
            EnumMap<CubeFace, Face> faces = new EnumMap<>(CubeFace.class);
            JsonObject faceObject = element.getAsJsonObject("faces");
            if (faceObject != null) {
                for (CubeFace cubeFace : CubeFace.values()) {
                    JsonElement faceElement = faceObject.get(cubeFace.jsonName());
                    if (faceElement == null || !faceElement.isJsonObject()) {
                        continue;
                    }

                    JsonObject face = faceElement.getAsJsonObject();
                    if (!face.has("texture")) {
                        continue;
                    }

                    faces.put(cubeFace, new Face(face.get("texture").getAsInt(), vec(face.getAsJsonArray("uv"), 4)));
                }
            }

            return new Cube(
                    element.has("name") ? element.get("name").getAsString() : "cube",
                    groupName,
                    vec(element.getAsJsonArray("from"), 3),
                    vec(element.getAsJsonArray("to"), 3),
                    vec(element.getAsJsonArray("origin"), 3),
                    vec(element.getAsJsonArray("rotation"), 3),
                    faces
            );
        }

        private static float[] vec(JsonArray array, int expectedSize) {
            float[] values = new float[expectedSize];
            if (array == null) {
                return values;
            }

            for (int i = 0; i < expectedSize && i < array.size(); i++) {
                values[i] = array.get(i).getAsFloat();
            }

            return values;
        }

        private static float getResolution(JsonObject root, String key, float fallback) {
            if (!root.has("resolution")) {
                return fallback;
            }

            JsonObject resolution = root.getAsJsonObject("resolution");
            return resolution.has(key) ? resolution.get(key).getAsFloat() : fallback;
        }
    }

    private enum CubeFace {
        NORTH("north"),
        EAST("east"),
        SOUTH("south"),
        WEST("west"),
        UP("up"),
        DOWN("down");

        private final String jsonName;

        CubeFace(String jsonName) {
            this.jsonName = jsonName;
        }

        private String jsonName() {
            return this.jsonName;
        }
    }

    private record TextureRef(Identifier id) {
    }

    private record Face(int textureIndex, float[] uv) {
    }

    private record Cube(String name, String groupName, float[] from, float[] to, float[] origin, float[] rotation,
                        EnumMap<CubeFace, Face> faces) {
    }
}
zh_cn.json + en_us.json
{
  "entity.maik.elephant": "大象",
  "item.maik.elephant_spawn_egg": "大象刷怪蛋",
  "item.maik.mynewaxe": "MyNewAxe"
}
{
  "entity.maik.elephant": "Elephant",
  "item.maik.elephant_spawn_egg": "Elephant Spawn Egg",
  "item.maik.mynewaxe": "MyNewAxe"
}

Why is the renderer so long?

Because this bbmodel is not a simple texture, but a model composed of multiple textures, face-by-face UV, and multiple body parts. The renderer is responsible for reading model files, parsing blocks, selecting textures, emitting vertices, and adding rotation animations to different groups before drawing. In class, you can first talk about server-side entities, and then split the renderer into three parts: "reading data, drawing blocks, and adding animation".

Animation logic

Animation design: Even without animation tracks, the elephant can have a sense of life.

We designed four types of actions for the elephant. The first type is the birth animation: within the first 42 ticks of the generation, the elephant rises from the ground, and `POOF` particles appear at its feet. The second type is walking animation: four legs swing alternately according to `limbAnimator`. The third category is idle animation: ears flapping slightly, nose moving up and down, and tail wagging left and right. The fourth category is looking at the player: the head is slightly turned based on `headYaw` and `pitch`.

  • Birth animation:`SPAWN_ANIMATION_TICKS = 42`, use progress to control displacement and scaling.
  • Leg animation:According to the front, rear, left and right positions of the legs in the model, different phases are given.
  • Ear animation:The left ear and the right ear are in opposite directions, and the fanning is more obvious at birth.
  • Nose animation:Use the sine function to make a slight swing, and the amplitude will be larger when generated.
Elephant biological implementation flow chart part, used to explain animation steps
The animation does not have to be from Blockbench. As long as you know which group each part belongs to, you can perform a matrix rotation on it before rendering.

Debug board

Troubleshooting common problems.

No elephant in sight

Check Entity and Render Registration

The server needs to register `ModEntities.ELEPHANT`, and the client needs to register `ElephantEntityRenderer::new` in `MaikClient`.

The model is purple-black

Check map path

The drawings must be `assets/maik/textures/entity/elephant/ ' , and the filenames correspond to the captions in bbmodel.

Stuck after generation

Check collision box and spawn position

The elephant is much taller than the chicken, and the safe position needs to detect that `pos`, `pos.up()` and `pos.up(2)` are all air.

Actions are too exaggerated

Lower angle value

The animations of ears, nose, and legs are all angle controlled. Turn down numbers such as 18 and 30, and the movements will be gentler.

The spawn egg did not appear

Check the creation inventory

`ItemGroupEvents.modifyEntriesEvent(ItemGroups.SPAWN_EGGS)` is responsible for adding elephant spawn eggs to the column.

Compilation failed

Read the first error first

Different versions of Fabric/Yarn will cause changes in class names. This tutorial corresponds to Fabric 1.20 and Yarn `1.20+build.1` in the project.

Classroom tasks

Extended tasks for students.

After completing the basic elephant, the task can be divided into three levels. The first level only changes numbers, the second level changes behavior, and the third level changes rendering and animation. In this way, students at different levels have achievable goals.

  • Getting Started:Change the elephant's HP to 80 and its movement speed to 0.12 to make it more like a large animal that walks slowly.
  • Step:To make elephants like apples, sugar cane and watermelon, students check for `Items.MELON_SLICE'.
  • Challenge:Change the birth animation to "falling slowly from the sky" and add cloud particles.
  • Creativity:Make a copy of the entity and make it into a small elephant. The collision box and model scaling will become smaller.
Elephant tutorial mind map, used for classroom extended task review
When expanding, still think along the same picture: where to change entity attributes, AI, rendering, animation, and resource paths respectively.

What students learn

What this class really learns is the full link to "Change Art Resources into Game objects".

Resource awareness

Models, textures, language files, and Java classes must be placed in the correct path for the game to find them.

Client/Server Awareness

Entity registration and AI are on the server side, and rendering and animation are on the client side. This is the core dividing line in MOD development.

Debug Awareness

Compile first, then check the resources into the package, and finally enter the game for verification. When encountering problems, troubleshoot by module instead of making random changes.