Model Checking
Confirm the file structure, number of textures, number of blocks and whether there is an animation track.
Fabric 1.20 custom creature lesson
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”.
Research recap
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.
Confirm the file structure, number of textures, number of blocks and whether there is an animation track.
Instead of adding GeckoLib, use Fabric native entities + custom renderers.
An elephant is automatically generated into the world and can be tested manually with an Elephant Brushing Egg.
Mind map
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
File map
Register `maik:elephant`, set collision box, tracking distance and default properties.
src/main/java/mls/maik/entity/ModEntities.java
Set blood levels, speeds, AI targets, breeding food and birth particles.
src/main/java/mls/maik/entity/custom/ElephantEntity.java
Reads bbmodel and draws a model by peg and UV, with animated parts.
src/client/java/mls/maik/client/render/entity/ElephantEntityRenderer.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
Added "elephant monster spawning egg" and added the monster spawning egg column in creative mode.
src/main/java/mls/maik/item/ModItems.java
Keep the original model file and extract the embedded texture to the entity texture directory.
assets/maik/models/entity/elephant.bbmodel
Complete code
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);
}
}
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
);
}
}
}
}
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;
}
}
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) {
}
}
{
"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"
}
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
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`.
Debug board
The server needs to register `ModEntities.ELEPHANT`, and the client needs to register `ElephantEntityRenderer::new` in `MaikClient`.
The drawings must be `assets/maik/textures/entity/elephant/ ' , and the filenames correspond to the captions in bbmodel.
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.
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.
`ItemGroupEvents.modifyEntriesEvent(ItemGroups.SPAWN_EGGS)` is responsible for adding elephant spawn eggs to the column.
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
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.
What students learn
Models, textures, language files, and Java classes must be placed in the correct path for the game to find them.
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.
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.