做模组做到第二十二章,终于要往《我的世界》Java版的世界里塞一个活蹦乱跳的实体了。很多人在这一步被卡住,不是因为代码有多难,而是因为“实体”这个词背后牵扯的东西太多:实体类型、注册表、渲染器、模型、动画、AI、刷怪蛋,每一个拆开都能写一整篇。今天这篇教程我打算按“外观先行”的顺序来,先用 Fabric 模组开发把绯红巨像(Crimson Golem)的外观和动画完整跑通,让它在游戏里能生成、能看见、有呼吸有走路动作,至于追踪玩家和攻击 AI,我们留到下一篇再讲。
这一篇适合已经会用 Fabric 注册物品、懂基本 Java 语法、但第一次碰实体的开发者。建模部分我会用 Blockbench 操作,代码部分基于 1.21.4 + Fabric + Yarn 映射。跟着这篇走完,你会得到一个可以在游戏里/summon tutorial:crimson_golem召唤出来的新生物,并且它自带 idle、walk、attack 三类动画的雏形。看完之后你会发现,实体外观设计这件事情最核心的其实就一句话:Blockbench 里的模型树,就是代码里的 ModelPart 树,也是动画关键帧的绑定目标。搞懂这条主线,其他都是细节。
1. 从零到一:生物实体外观与动画设计的整体思路
1.1 这次要做什么,以及最终效果
先明确目标:我们要创建一个叫crimson_golem的新实体,它是一个约两格高、体宽 1.4 格的类人形生物。实体在游戏里需要满足三个基本诉求:
- 能通过指令生成,并且有正常的阴影和碰撞箱;
- 有独立纹理,不能出现紫黑块或隐形问题;
- 有基础动画:闲置时呼吸晃动,移动时四肢交替摆动。
这个实体本身没有继承原版铁傀儡的逻辑,是一个完全独立的“新种族”。我们先把外观和动画做好,之后再加入 AI 行为。这样做的最大好处是,每一步都能在游戏里看到实质反馈,而不是写了一大堆代码后一启动就崩溃,连资源文件对不对都没法验证。
在下手之前,我还建议你先把assets/tutorial/textures/entity/crimson_golem.png这个路径记在纸上。许多新手卡在紫黑方块上,就是纹理路径和代码里Identifier没对上。实体渲染的所有资源路径,都是以assets/<命名空间>/为根目录,代码里写Identifier.of("tutorial", "textures/entity/crimson_golem.png"),那么纹理文件就必须放在src/main/resources/assets/tutorial/textures/entity/crimson_golem.png,不能多一层目录,也不能改大小写。
1.2 为什么先做“外观与动画”而不是直接写AI
我在很多教程里看到大家喜欢先做实体 AI,再补模型,理由是“AI 才是核心玩法”。但我的经验恰恰相反:先做外观和动画,才是新手入坑实体开发的正确姿势。
原因有三。第一,实体渲染和实体逻辑在代码里虽然是两套系统,但共用同一个EntityType和同一个模型实例,你一旦把 AI 写在前面,就会出现“一个看不见的怪在打你”的情况,根本没法判断是不是 AI 逻辑的问题。第二,模型的部件结构会影响动画,如果你随手建了一个模型树,左腿和右腿合并成一个leg部件,后面做走路动画时,左右腿就无法独立摆动,这时候再回 Blockbench 改模型,反而要重构代码。第三,外观反馈最直观,模型、纹理、动画一旦完成,成就感能支撑你继续写后面相对枯燥的 AI。
所以这一篇的整体设计思路很明确:让模型树和动画命名在建模阶段就定下来,代码阶段只是把这棵树“翻译”给 Minecraft。哪怕后续 AI 逻辑大改,只要 ModelPart 的名字和层级不变,渲染部分基本不用动。
1.3 前置条件与工具清单
既然是系列教程第二十二篇,我默认你已经搭好 Fabric 开发环境。不过还是把关键工具列一下,方便检查:
- JDK 21,同时确认
JAVA_HOME路径正确,命令行执行java -version显示的是 21; - IntelliJ IDEA,社区版就够用;
- Fabric Loom 插件和 Gradle 8.x;
- Blockbench 4.11 或更高版本;
- 已完成一个空白的 Fabric 模组,能正常启动,并注册过至少一个物品或方块。
另外,我强烈建议使用 Yarn 映射而不是 Mojang 官方映射。Yarn 的类名更接近社区习惯,比如PathAwareEntity、MobEntityRenderer,在查 Fabric 社区教程时更容易对应。如果你使用的是 Mojang 映射,类名会变成PathfinderMob之类,思路一样,但代码细节要自己转换。
2. 建模第一步:用 Blockbench 搭出生物模型
2.1 Blockbench 的“Minecraft Entity”预设
Blockbench 是当前做《我的世界》实体模型最主流的工具,没有之一。打开 Blockbench 后,点击“新建”,选择“Minecraft Entity”,版本可以选择 1.21.x。这个预设和我们做方块模型、物品模型的流程完全不同,它会自动加载 Minecraft 实体模型的导出逻辑,包括EntityModelLayer需要的 Java 模型代码和动画定义代码都支持直接生成。
新建时纹理尺寸我建议选 128x128。原版很多小生物用 64x64 就够了,但我们这个生物结构多,头、身体、四肢、可能还要做攻击手臂的细节,64x64 很容易把 UV 挤成一团。256x256 虽然更清晰,但会白白增加显存开销,128x128 是最平衡的选择。
在 Blockbench 中,一个立方体就是模型的最小单位。左侧 Outliner 面板会显示模型树,右侧可以调整方块的位置、大小和旋转轴心。你不需要一次性建得特别精细,先搭出“头、身体、两只手臂、两条腿”这六个主要盒子,让它的轮廓像一个巨人,后面再考虑贴图细节。
2.2 模型树与命名规范
这一小节是整个实体外观设计的灵魂。很多教程会直接跳过命名规范,让你从 Blockbench 导出 Java 模型代码,然后复制进游戏里发现动画完全对不上。核心原因就是模型树的 part 名称和代码里的getChild("xxx")不一致。
我建议把所有部件都放在根节点 root 下,保持平级结构:
root +-- body +-- head +-- left_arm +-- right_arm +-- left_leg +-- right_leg不要用body/head/arm_left这种中文玩家常见的命名习惯,代码里要匹配的ModelPart名称必须和 Blockbench 的节点名完全一致。尤其是左右腿,千万不要合并成一个legs部件。走路动画需要左腿和右腿独立旋转,如果你把它们画在一个立方体里,动画模式下就只能整块转,效果非常僵硬。
轴心点(pivot)也是建模时就要注意的。在 Blockbench 中,选中一个方块后,右键可以编辑 pivot 位置。腿的 pivot 应该放在髋关节位置,也就是腿方块的上边缘,而不是方块中心。否则走路动画播放时,腿会像绕着自己中心旋转,而不是从骨盆处摆动。这个错误在游戏里看起来就是“膝盖扭曲”,很难修,因为动画文件已经绑定了 pivot 坐标。
2.3 UV 展开与纹理绘制技巧
Blockbench 建模完成后,点击左侧的“纹理”标签页,会看到自动展开的 UV 图。实体模型的纹理本质上是一个大 PNG,每个立方体的六个面会被平铺到这个 PNG 上。你可以直接在 Blockbench 里用画笔上色,也可以导出 PNG 后用 Aseprite、Photoshop 精细绘制。
这里有一个实战经验:UV 岛之间一定要留至少 2 像素的边距。Minecraft 的纹理在远处会生成 mipmap,如果两个 UV 岛严丝合缝地贴在一起,mipmap 采样时会把隔壁像素的颜色“渗”进来,导致模型边缘出现细小闪烁。尤其是眼睛、嘴、花纹这类像素区域,与 UV 边缘保持 2px 以上距离能省很多心。
另外,实体模型默认使用的渲染层不支持透明像素。你画贴图时如果用了透明背景,游戏里看到的会是“半透明镂空”的诡异效果。如果你需要眼睛发光或者镂空结构,那要用RenderLayer自行处理,普通实体教程不推荐一上来就碰。
建模完成后,选择 Blockbench 的“文件 -> 导出 -> Java Entity”,会生成一个 Java 模型类。这个类包含了完整的getTexturedModelData()方法,里面所有立方体、UV、pivot 坐标都生成好了。我们后面就是把这个类塞进 Fabric 项目里,省去手写大量模型代码。
3. 把模型变成代码:实体类、注册与渲染器
3.1 实体类与 EntityType 注册
转到 IDE 里,先创建实体本体类。因为我们要让生物有基本的移动能力,我选择继承PathAwareEntity,这样后面加寻路 AI 时不用再换父类。
package com.tutorialmod.entity; import net.minecraft.entity.EntityType; import net.minecraft.entity.mob.PathAwareEntity; import net.minecraft.world.World; public class CrimsonGolem extends PathAwareEntity { public CrimsonGolem(EntityType<? extends PathAwareEntity> entityType, World world) { super(entityType, world); } }接着创建一个ModEntities类,用来注册实体类型。Fabric 的注册方式比原版更简洁,关键是FabricEntityTypeBuilder可以帮我们省掉EntityType.Builder的样板代码:
package com.tutorialmod.entity; 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 final class ModEntities { private ModEntities() {} public static final EntityType<CrimsonGolem> CRIMSON_GOLEM = Registry.register( Registries.ENTITY_TYPE, Identifier.of("tutorial", "crimson_golem"), FabricEntityTypeBuilder.create(SpawnGroup.CREATURE, CrimsonGolem::new) .dimensions(EntityDimensions.fixed(1.4F, 2.8F)) .build() ); public static void initialize() { // 后续在这里加刷怪蛋、生成规则等 } }EntityDimensions.fixed(1.4F, 2.8F)里的 1.4 是实体碰撞体积的宽度直径,2.8 是高度。这个值要和你模型的视觉尺寸匹配。如果模型做得很大,碰撞箱却很小,玩家攻击时就会感觉“打到空气”;相反碰撞箱太大,实体过门时会卡住。我建议先拿 Blockbench 里的模型尺寸粗略估算,后面再微调。
在主入口类里调用ModEntities.initialize():
@Override public void onInitialize() { ModEntities.initialize(); }到这里,游戏已经认识了一个叫tutorial:crimson_golem的实体类型,但你直接/summon的话,只会生成一个没有模型、没有纹理的隐形碰撞箱。接下来是渲染部分。
3.2 渲染器与模型类的骨架
实体渲染完全在客户端侧工作。我们需要两样东西:一个是渲染器Renderer,另一个是模型类Model。渲染器负责告诉 Minecraft “这个实体用什么模型、什么纹理、渲染阴影多大”;模型类负责定义模型结构,并在每一帧计算各个ModelPart的角度。
先写渲染器:
package com.tutorialmod.client.renderer; import com.tutorialmod.client.model.CrimsonGolemModel; import com.tutorialmod.client.render.ModModelLayers; import com.tutorialmod.entity.CrimsonGolem; import net.minecraft.client.render.entity.EntityRendererFactory; import net.minecraft.client.render.entity.MobEntityRenderer; import net.minecraft.util.Identifier; public class CrimsonGolemRenderer extends MobEntityRenderer<CrimsonGolem, CrimsonGolemModel<CrimsonGolem>> { public CrimsonGolemRenderer(EntityRendererFactory.Context context) { super(context, new CrimsonGolemModel<>(context.getPart(ModModelLayers.CRIMSON_GOLEM)), 0.7F); } @Override public Identifier getTexture(CrimsonGolem entity) { return Identifier.of("tutorial", "textures/entity/crimson_golem.png"); } }这里最后那个0.7F是阴影半径,数值越大阴影越宽,视觉上会感觉生物更“庞大”。
然后写模型类。你不需要手写所有立方体的构建代码,把 Blockbench 导出的 Java 文件内容合并进来即可。模型类的骨架长这样:
package com.tutorialmod.client.model; import com.tutorialmod.entity.CrimsonGolem; import net.minecraft.client.model.*; import net.minecraft.client.render.entity.model.EntityModel; import net.minecraft.entity.mob.PathAwareEntity; public class CrimsonGolemModel<T extends PathAwareEntity> extends EntityModel<T> { private final ModelPart root; private final ModelPart head; private final ModelPart body; private final ModelPart leftArm; private final ModelPart rightArm; private final ModelPart leftLeg; private final ModelPart rightLeg; public CrimsonGolemModel(ModelPart root) { this.root = root; this.head = root.getChild("head"); this.body = root.getChild("body"); this.leftArm = root.getChild("left_arm"); this.rightArm = root.getChild("right_arm"); this.leftLeg = root.getChild("left_leg"); this.rightLeg = root.getChild("right_leg"); } public static TexturedModelData getTexturedModelData() { ModelPartData modelPartData = ModelPartData.create(); // 将 Blockbench 导出的 getTexturedModelData 内容粘贴进这里 return TexturedModelData.of(modelPartData, 128, 128); } @Override public void setAngles(T entity, float limbAngle, float limbDistance, float animationProgress, float headYaw, float headPitch) { this.head.yaw = headYaw * ((float) Math.PI / 180F); this.head.pitch = headPitch * ((float) Math.PI / 180F); } }setAngles是每一帧都会调用的方法,参数里的headYaw和headPitch来自游戏对生物头部的实时计算。这里先把这两个值传给 head 部件,模型就能跟随视线转动了。
3.3 模型层注册与纹理路径对应
模型类写好后,还需要告诉EntityModelLayer怎么创建它。创建一个ModModelLayers类:
package com.tutorialmod.client.render; import com.tutorialmod.client.model.CrimsonGolemModel; import com.tutorialmod.client.renderer.CrimsonGolemRenderer; import com.tutorialmod.entity.ModEntities; import net.fabricmc.fabric.api.client.rendering.v1.EntityModelLayerRegistry; import net.fabricmc.fabric.api.client.rendering.v1.EntityRendererRegistry; import net.minecraft.client.render.entity.model.EntityModelLayer; import net.minecraft.util.Identifier; public class ModModelLayers { public static final EntityModelLayer CRIMSON_GOLEM = new EntityModelLayer(Identifier.of("tutorial", "crimson_golem"), "main"); public static void initialize() { EntityModelLayerRegistry.registerModelLayer(CRIMSON_GOLEM, CrimsonGolemModel::getTexturedModelData); EntityRendererRegistry.register(ModEntities.CRIMSON_GOLEM, CrimsonGolemRenderer::new); } }然后在客户端初始化入口调用:
package com.tutorialmod.client; import com.tutorialmod.client.render.ModModelLayers; import net.fabricmc.api.ClientModInitializer; public class TutorialClient implements ClientModInitializer { @Override public void onInitializeClient() { ModModelLayers.initialize(); } }别忘了fabric.mod.json里的entrypoints需要分主入口和客户端入口:
"entrypoints": { "main": ["com.tutorialmod.TutorialMod"], "client": ["com.tutorialmod.client.TutorialClient"] }到这里,你启动游戏,使用/summon tutorial:crimson_golem,应该就能看到一个静态的绯红巨像站在面前了。如果出现紫黑格或者完全隐形,先检查纹理路径是否正确,再看ClientModInitializer是否被加载。
4. 动画设计:从 Blockbench 关键帧到游戏内播放
4.1 动画要拆成哪几类
我们做生物动画,不要把每个动作都堆在一个大动画里。原版 Minecraft 实体动画系统支持多段动画混合,但混合的目标如果互相冲突就会很难看。所以开始做动画前,先规划一下需要的动画拆解:
idle:循环动画,表现呼吸、身体轻微摇晃、头部转动;walk:循环动画,表现四肢交替摆动,和移动速度相关;attack:一次性动画,表现挥动手臂,播放完停在最后一帧或者复位;death:一次性动画,表现倒地,这个可以留给下篇做。
本篇我重点讲idle和walk,attack会给出调用思路。这样例子代码不会太啰嗦。
4.2 在 Blockbench 制作关键帧动画
在 Blockbench 左下角点击“Animate”按钮,就会进入动画模式。动画面板类似常见骨骼动画编辑器。新建一个动画,命名idle,将循环方式设置为Loop。
以idle为例,最简单的呼吸动画:
- 在第 0 tick,
body的 rotation 为(0, 0, 0); - 在第 10 tick,
body的 rotation 为(-1, 0, 0); - 在第 20 tick,
body的 rotation 回到(0, 0, 0)。
这里的 tick 是游戏刻。Minecraft 一秒运行 20 个 tick,所以一个 20 tick 的 idle 动画就是 1 秒一个循环。Blockbench 会自动为两个关键帧之间生成插值。如果你想要更自然的呼吸,可以把 position 和 rotation 同时做微调,并且使用“平滑”插值。
再做walk动画:
- 第 0 tick,
left_legrotation 为(-30, 0, 0),right_legrotation 为(30, 0, 0); - 第 10 tick,
left_legrotation 为(30, 0, 0),right_legrotation 为(-30, 0, 0); - 第 20 tick,回到第 0 tick 的状态。
手臂动画要反向摆动,否则看起来非常不自然。这也是为什么建模时左右臂和左右腿都必须是独立 part。Blockbench 里旋转单位默认是角度,你只需要输入-30和30表示前后摆动即可。
4.3 导出动画 Java 文件并挂接到模型类
动画制作完成后,在 Blockbench 选择“文件 -> 导出 -> Java Entity Animation”,会生成一个 Java 类文件,里面包含多个AnimationDefinition。把这个文件复制到你的项目里,比如命名为CrimsonGolemAnimations.java。
Blockbench 生成的动画代码大致长这样:
public class CrimsonGolemAnimations { public static final AnimationDefinition IDLE = Animation.Builder.create("idle").looping() .addAnimation("body", new AnimationChannel(AnimationChannel.Targets.ROTATION, new Keyframe(0.0F, new Vector3f(0F, 0F, 0F), AnimationChannel.Interpolations.LINEAR) ) ) .build(); public static final AnimationDefinition WALK = Animation.Builder.create("walk").looping() .addAnimation("left_leg", new AnimationChannel(AnimationChannel.Targets.ROTATION, new Keyframe(0.0F, new Vector3f(-30F, 0F, 0F), AnimationChannel.Interpolations.LINEAR) ) ) .build(); }不同版本的 Yarn 映射下,Keyframe构造函数的参数顺序可能不同。粘贴到 IDEA 后如果标红,直接让 IDE 自动修正即可。
挂接动画的核心在模型类的setAngles里。我们需要为每个动画定义一个AnimationState,然后在每一帧更新它们:
private final AnimationState idleAnimationState = new AnimationState(); private final AnimationState walkAnimationState = new AnimationState(); @Override public void setAngles(T entity, float limbAngle, float limbDistance, float animationProgress, float headYaw, float headPitch) { this.head.yaw = headYaw * ((float) Math.PI / 180F); this.head.pitch = headPitch * ((float) Math.PI / 180F); boolean isWalking = entity.getVelocity().horizontalLengthSquared() > 0.01F; if (isWalking) { walkAnimationState.startIfNotRunning(entity.age); } else { walkAnimationState.stop(); } idleAnimationState.startIfNotRunning(entity.age); KeyframeAnimator.animate(this, this.root, idleAnimationState, CrimsonGolemAnimations.IDLE); if (isWalking) { KeyframeAnimator.animate(this, this.root, walkAnimationState, CrimsonGolemAnimations.WALK); } }KeyframeAnimator.animate是 Minecraft 1.21 自带的动画播放工具,在net.minecraft.client.render.entity.animation.KeyframeAnimator里。它的作用是根据动画状态记录的播放时间,计算当前应该在哪些ModelPart上叠加多少角度。这个类对新手很友好,只要你的模型 part 名和动画关键帧 bone 名一致,它就能自动工作。
4.4 动画叠加与常见理解误区
你会发现上面的写法是 if/else,也就是走路时只播 walk,闲置时只播 idle。实际项目里我们经常需要两段动画叠加,比如走路时同时保留胸口的呼吸起伏。KeyframeAnimator.animate支持传入多个AnimationDefinition:
KeyframeAnimator.animate(this, this.root, animationState, CrimsonGolemAnimations.IDLE, CrimsonGolemAnimations.WALK);但这里有个前提:多个动画会以同一AnimationState的播放进度来计算,所以你必须保证这些动画的时间轴设计是兼容的。比如 idle 是 20 tick 循环,walk 也最好做 20 tick 循环,这样叠加后才不会出现呼吸节奏和步伐错位。如果你想深入做动画状态机,建议后续研究原版AnimationState和KeyframeAnimator的源码,这块内容够写三篇教程。
另外,很多人误以为引入动画文件后模型就会自动动起来。实际上游戏每帧调用setAngles,模型只是“被计算角度”。如果没有调用startIfNotRunning,动画状态就永远是停止状态,KeyframeAnimator也不会输出任何变动。
5. 常见问题与排查技巧实录
5.1 实体生成但完全隐形,只有阴影
这种情况第一步先按 F3 + B 打开实体碰撞箱显示。如果能看到一个矩形碰撞箱,说明实体类型和生成逻辑没问题,问题出在客户端渲染链路。
优先检查三点:一是EntityRendererRegistry.register是否真的执行了,ClientModInitializer里的initialize()有没有漏掉;二是ModModelLayers.CRIMSON_GOLEM是否和渲染器里的context.getPart()使用的是同一个常量;三是控制台是否输出类似Tried to get model for unknown layer的报错。只要这三项完整,模型基本能显示出来。
5.2 实体显示紫黑格或透明纹路
紫黑格意味着纹理资源加载失败。常见原因有四个:纹理文件没放在assets/tutorial/textures/entity/目录下;文件名不是crimson_golem.png;fabric.mod.json的"depends"里缺少资源加载相关依赖;或者文件名大小写不对,Linux 下大小写敏感会直接报错。
透明纹路则说明你的 PNG 带 alpha 通道。默认的实体渲染层使用不透明纹理,一旦有透明像素就会出现“半透明窗户”效果。如果你不是刻意做发光/透明效果,建议把 PNG 的 alpha 通道合并到 RGB 里。
5.3 动画不播放或播放时模型扭曲
动画不播放,先检查AnimationState是否被start,再看代码里有没有调用KeyframeAnimator.animate。有时候你调用了,但传入的 animation definition 是空的,那也不会动。
模型扭曲,尤其是四肢乱转,基本是 Blockbench 里的 pivot 位置不对。pivot 决定了部件旋转时绕哪个点转,如果腿的 pivot 在方块中心,走路动画看起来就像腿自己在原地转圈。回到 Blockbench,把 pivot 拖到髋关节位置,重新导出动画文件即可。
还有一种隐蔽问题:Blockbench 导出的动画 part 名是leftLeg,但模型代码里 part 名是left_leg。动画关键帧根据 part 名定位,找不到就静默忽略。所以建模时命名规范一定要统一,我建议全部使用下划线风格。
5.4 服务端崩溃:NoClassDefFoundError
这个报错经常出现在实体渲染代码被服务端加载时。比如不小心在主入口onInitialize里 import 了CrimsonGolemModel,或者把自己的模型类放进了公共 source set 并被服务端类引用。解决办法是把渲染相关类全部放在client包下,并且只在ClientModInitializer中引用。服务端永远不应该知道EntityModelLayerRegistry存在。
5.5 常见问题速查表
| 现象 | 大概率原因 | 快速处理 |
|---|---|---|
| 紫黑格 | 纹理路径/命名空间错误 | 检查assets/tutorial/textures/entity/路径 |
| 隐形但有阴影 | 渲染器或模型层未注册 | 检查ClientModInitializer入口 |
| 动画不播放 | AnimationState 未启动 | 检查startIfNotRunning调用 |
| 四肢乱转 | pivot 轴心点错误 | 在 Blockbench 修正 pivot 后重新导出 |
| 服务端崩溃 | 客户端类被服务端加载 | 把渲染类隔离到 client 包,检查入口 |
| 模型尺寸和碰撞不匹配 | EntityDimensions 设置错误 | 调整EntityDimensions.fixed()数值 |
我个人的习惯是,模型树一旦确定就尽量保持稳定。Blockbench 中的命名直接照抄到 Java 代码里,动画文件名也和实体 id 保持一致,比如crimson_golem.animation.json这种风格。每次改完模型和动画,我都会启动游戏快速验证一次,而不是等做了一大堆再统一测。实体外观与动画设计可以说是模组开发里最依赖“模型树一致性”的环节,只要这条主线不乱,后面加 AI、加刷怪蛋、加交互相应都会顺利很多。