创建你的第一个实体
实体是游戏中动态、可交互的对象,它们不是地形的一部分(例如方块)。 实体可以移动,并以各种方式与世界交互。 一些示例包括:
Villager、Pig和Goat都是Mob的示例。Mob是最常见的实体类型,表示有生命的生物。Zombie和Skeleton是Monster的示例。Monster是一种敌对Player的Entity变体。Minecart和Boat是VehicleEntity的示例。这类实体具有处理玩家输入的特殊逻辑。
本教程将引导你创建一个自定义的 迷你傀儡。 这个实体会带有有趣的动画。 它将是一个 PathfinderMob,这是大多数具有寻路能力的生物所使用的类,例如 Zombie 和 Villager。
准备你的第一个实体
创建自定义实体的第一步,是定义它的类,并将其注册到游戏中。
我们会为该实体创建 MiniGolemEntity 类,并首先为它设置属性。 属性决定了实体的多项内容,包括最大生命值、移动速度和生物引诱范围。
java
public class MiniGolemEntity extends PathfinderMob {
public MiniGolemEntity(Level world) {
this(ModEntityTypes.MINI_GOLEM, world);
}
public MiniGolemEntity(EntityType<? extends MiniGolemEntity> entityType, Level world) {
super(entityType, world);
}
public static AttributeSupplier.Builder createCubeAttributes() {
return PathfinderMob.createMobAttributes()
.add(Attributes.MAX_HEALTH, 5)
.add(Attributes.TEMPT_RANGE, 10)
.add(Attributes.MOVEMENT_SPEED, 0.3);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
若要注册你的实体,建议创建一个单独的 ModEntityTypes 类,用于注册所有实体类型、设置它们的尺寸,并注册它们的属性。
java
public class ModEntityTypes {
public static final EntityType<MiniGolemEntity> MINI_GOLEM = register(
"mini_golem",
EntityType.Builder.<MiniGolemEntity>of(MiniGolemEntity::new, MobCategory.MISC)
.sized(0.75f, 1.75f)
);
private static <T extends Entity> EntityType<T> register(String name, EntityType.Builder<T> builder) {
ResourceKey<EntityType<?>> key = ResourceKey.create(Registries.ENTITY_TYPE, Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, name));
return Registry.register(BuiltInRegistries.ENTITY_TYPE, key, builder.build(key));
}
public static void registerModEntityTypes() {
ExampleMod.LOGGER.info("Registering EntityTypes for " + ExampleMod.MOD_ID);
}
public static void registerAttributes() {
FabricDefaultAttributeRegistry.register(MINI_GOLEM, MiniGolemEntity.createCubeAttributes());
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
添加目标
目标是用于处理实体目的或行为意图的系统,它会为实体提供一组明确的行为。 目标具有一定的优先级:优先级数值较低的目标,会优先于数值较高的目标执行。
若要为实体添加目标,需要在实体类中创建一个 registerGoals 方法,用于定义该实体的目标。
java
@Override
protected void registerGoals() {
this.goalSelector.addGoal(0, new TemptGoal(this, 1, Ingredient.of(Items.WHEAT), false));
this.goalSelector.addGoal(1, new RandomStrollGoal(this, 1));
this.goalSelector.addGoal(2, new LookAtPlayerGoal(this, Cow.class, 4));
this.goalSelector.addGoal(3, new RandomLookAroundGoal(this));
}1
2
3
4
5
6
7
2
3
4
5
6
7
INFO
TemptGoal- 使实体被手持特定物品的玩家吸引。RandomStrollGoal- 使实体在世界中随机行走或游荡。LookAtPlayerGoal- 尽管名称如此,但它可以接受任意实体。 这里用于注视Cow实体。RandomLookAroundGoal- 使实体随机朝不同方向看。
创建渲染
渲染是指将方块、实体和环境等游戏数据转换为玩家屏幕上可见画面的过程。 这包括决定对象如何被照亮、着色和渲染贴图。
INFO
实体渲染始终在客户端处理。 服务器负责管理实体的逻辑和行为,而客户端负责显示实体的模型、纹理和动画。
渲染包含多个步骤,并会涉及各自对应的类。我们先从 EntityRenderState 类开始。
java
public class MiniGolemEntityRenderState extends LivingEntityRenderState {
}1
2
2
存储在渲染状态中的数据会用于决定实体的视觉表现,包括移动、空闲行为等动画状态。
设置模型
MiniGolemEntityModel 类用于描述实体的形状和组成部分,从而定义实体的外观。 模型通常会在 Blockbench 等第三方工具中创建,而不是手写。 不过,本教程仍会通过一个手写示例来展示其工作方式。
WARNING
Blockbench 支持多种映射(例如 Mojang Mappings、Yarn 等)。 请确保选择与你的开发环境匹配的正确映射。本教程使用 Mojang Mappings。
映射不一致可能会导致集成 Blockbench 生成的代码时出错。
java
public class MiniGolemEntityModel extends EntityModel<MiniGolemEntityRenderState> {
private final ModelPart head;
private final ModelPart leftLeg;
private final ModelPart rightLeg;
//:::dancing_animation
public MiniGolemEntityModel(ModelPart root) {
//:::dancing_animation
super(root);
head = root.getChild(PartNames.HEAD);
leftLeg = root.getChild(PartNames.LEFT_LEG);
rightLeg = root.getChild(PartNames.RIGHT_LEG);
}
//:::dancing_animation1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
MiniGolemEntityModel 类定义了迷你傀儡实体的视觉模型。 它继承自 EntityModel,并指定实体各个身体部件(身体、头部、左腿和右腿)的名称。
java
public static LayerDefinition getTexturedModelData() {
MeshDefinition modelData = new MeshDefinition();
PartDefinition root = modelData.getRoot();
root.addOrReplaceChild(
PartNames.BODY,
CubeListBuilder.create().addBox(
/* x */ -6,
/* y */ -6,
/* z */ -6,
/* width */ 12,
/* height */ 12,
/* depth */ 12
),
PartPose.offset(0, 8, 0)
);
root.addOrReplaceChild(
PartNames.HEAD,
CubeListBuilder.create().texOffs(36, 0).addBox(-3, -6, -3, 6, 6, 6),
PartPose.offset(0, 2, 0)
);
root.addOrReplaceChild(
PartNames.LEFT_LEG,
CubeListBuilder.create().texOffs(48, 12).addBox(-2, 0, -2, 4, 10, 4),
PartPose.offset(-2.5f, 14, 0)
);
root.addOrReplaceChild(
PartNames.RIGHT_LEG,
CubeListBuilder.create().texOffs(48, 12).addBox(-2, 0, -2, 4, 10, 4),
PartPose.offset(2.5f, 14, 0)
);
return LayerDefinition.create(modelData, 64, 32);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
该方法通过将迷你傀儡的身体、头部和腿创建为长方体,设置它们的位置和纹理映射,并返回用于渲染的 LayerDefinition,从而定义迷你傀儡的 3D 模型。
每个部件都会以一个偏移点添加,该偏移点是应用到该部件的所有变换的原点。 模型部件中的其他所有坐标,都是相对于这个偏移点进行测量的。
WARNING
模型中更大的 Y 值对应实体的底部。 这与游戏内坐标相反。
现在,我们需要在客户端包中创建一个 ModEntityModelLayers 类。 这个实体只有一个纹理层,但其他实体可能会使用多个纹理层;可以想想 Player 这类实体的第二皮肤层,或 Spider 的眼睛。
java
public class ModEntityModelLayers {
public static final ModelLayerLocation MINI_GOLEM = createMain("mini_golem");
private static ModelLayerLocation createMain(String name) {
return new ModelLayerLocation(Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, name), "main");
}
public static void registerModelLayers() {
ModelLayerRegistry.registerModelLayer(ModEntityModelLayers.MINI_GOLEM, MiniGolemEntityModel::getTexturedModelData);
}
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
随后,必须在模组的客户端初始化器中初始化这个类。
java
public class ExampleModCustomEntityClient implements ClientModInitializer {
@Override
public void onInitializeClient() {
ModEntityModelLayers.registerModelLayers();
}
}1
2
3
4
5
6
2
3
4
5
6
设置纹理
TIP
纹理尺寸应与 LayerDefinition.create(modelData, 64, 32) 中的值一致:宽 64 像素,高 32 像素。 如果你需要使用不同尺寸的纹理,别忘了同时修改 LayerDefinition.create 中的尺寸,使其匹配。
每个模型部件或方盒都会期望纹理上的某个特定位置存在对应的展开图。 默认情况下,它会期望该位置在 0, 0(左上角),但可以通过调用 CubeListBuilder 中的 texOffs 函数来更改。
作为示例,你可以将以下纹理用于 assets/example-mod/textures/entity/mini_golem.png:
创建渲染器
实体的渲染器使你能够在游戏中看到该实体。 我们会创建一个新的 MiniGolemEntityRenderer 类,用于告诉 Minecraft 该实体应使用哪种纹理、模型和实体渲染状态。
java
public class MiniGolemEntityRenderer extends MobRenderer<MiniGolemEntity, MiniGolemEntityRenderState, MiniGolemEntityModel> {
private static final Identifier TEXTURE = Identifier.fromNamespaceAndPath(ExampleMod.MOD_ID, "textures/entity/mini_golem.png");
public MiniGolemEntityRenderer(EntityRendererProvider.Context context) {
super(context, new MiniGolemEntityModel(context.bakeLayer(ModEntityModelLayers.MINI_GOLEM)), 0.375f); // 0.375 shadow radius
}
@Override
public MiniGolemEntityRenderState createRenderState() {
return new MiniGolemEntityRenderState();
}
@Override
public Identifier getTextureLocation(MiniGolemEntityRenderState state) {
return TEXTURE;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
这里也会设置阴影半径。对于该实体,阴影半径为 0.375f。
随后,必须在模组的客户端初始化器中注册这个渲染器。
java
EntityRenderers.register(ModEntityTypes.MINI_GOLEM, MiniGolemEntityRenderer::new);1
添加行走动画
可以将以下代码添加到 MiniGolemEntityModel 类中,为实体添加行走动画。
java
//:::dancing_animation
@Override
public void setupAnim(MiniGolemEntityRenderState state) {
super.setupAnim(state);
head.xRot = state.xRot * Mth.RAD_TO_DEG;
head.yRot = state.yRot * Mth.RAD_TO_DEG;
float limbSwingAmplitude = state.walkAnimationSpeed;
float limbSwingAnimationProgress = state.walkAnimationPos;
leftLeg.xRot = Mth.cos(limbSwingAnimationProgress * 0.2f + Mth.PI) * 1.4f * limbSwingAmplitude;
rightLeg.xRot = Mth.cos(limbSwingAnimationProgress * 0.2f) * 1.4f * limbSwingAmplitude;
}
//:::dancing_animation
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
首先,将偏航角和俯仰角应用到头部模型部件上。
然后,将行走动画应用到腿部模型部件。 我们使用 cos 函数创建基础的腿部摆动效果,再对余弦波进行变换,以获得正确的摆动速度和摆动幅度。
- 公式中的
0.2常量控制余弦波的频率(腿摆动的速度)。 值越大,频率越高。 - 公式中的
1.4常量控制余弦波的振幅(腿摆动的幅度)。 值越大,振幅越大。 limbSwingAmplitude变量也会像1.4常量一样影响振幅。 该变量会根据实体速度变化,因此实体移动得越快,腿部摆动越明显;实体移动较慢或静止时,腿部摆动会减弱或完全停止。- 左腿公式中的
Mth.PI常量会将余弦波平移半个相位,使左腿与右腿朝相反方向摆动。
你可以将它们绘制到图表上,观察其效果:


蓝色曲线表示左腿,棕色曲线表示右腿。 水平 x 轴表示时间,y 轴表示腿部肢体的角度。
你可以随意在 Desmos 上调整这些常量,看看它们会如何影响曲线。
现在回到游戏中,你已经具备了使用 /summon example-mod:mini_golem 生成该实体所需的一切!

为实体添加数据
若要在实体上存储数据,通常的做法是在实体类中直接添加字段。
有时,你需要将服务端实体中的数据同步到客户端实体。 关于客户端-服务端架构的更多信息,请参阅网络通信页面。 为此,我们可以通过定义 EntityDataAccessor 来使用 synched data [原文如此]。
在本例中,我们希望实体每隔一段时间跳舞一次,因此需要创建一个会在客户端之间同步的跳舞状态,以便之后为其播放动画。 不过,跳舞冷却时间不需要与客户端同步,因为动画由服务器触发。
java
private static final EntityDataAccessor<Boolean> DANCING = SynchedEntityData.defineId(MiniGolemEntity.class, EntityDataSerializers.BOOLEAN);
private int dancingTimeLeft;
@Override
protected void defineSynchedData(SynchedEntityData.Builder builder) {
super.defineSynchedData(builder);
builder.define(DANCING, false);
}
public boolean isDancing() {
return entityData.get(DANCING);
}
private void setDancing(boolean dancing) {
entityData.set(DANCING, dancing);
}
@Override
public void tick() {
super.tick();
if (!level().isClientSide()) {
if (isDancing()) {
if (dancingTimeLeft-- <= 0) {
setDancing(false);
}
} else {
if (this.random.nextInt(1000) == 0) {
setDancing(true);
dancingTimeLeft = 100 + this.random.nextInt(100);
}
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
如你所见,我们添加了一个 tick 方法来控制跳舞状态。
将数据存储到 NBT
对于需要在游戏关闭后仍然保存的持久数据,我们会在 MiniGolemEntity 中重写 addAdditionalSaveData 和 readAdditionalSaveData 方法。 我们可以用它们来存储跳舞动画剩余的时间。
java
@Override
protected void addAdditionalSaveData(ValueOutput valueOutput) {
super.addAdditionalSaveData(valueOutput);
valueOutput.putInt("dancing_time_left", dancingTimeLeft);
}
@Override
protected void readAdditionalSaveData(ValueInput valueInput) {
super.readAdditionalSaveData(valueInput);
dancingTimeLeft = valueInput.getInt("dancing_time_left").orElse(0);
setDancing(dancingTimeLeft > 0);
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
现在,每当实体被加载时,它都会恢复到先前留下的状态。
添加动画
为实体添加动画的第一步,是在实体类中添加动画状态。 我们会创建一个动画状态,用于让实体跳舞。
java
public final AnimationState dancingAnimationState = new AnimationState();
@Override
public void onSyncedDataUpdated(EntityDataAccessor<?> data) {
super.onSyncedDataUpdated(data);
if (data == DANCING) {
dancingAnimationState.animateWhen(isDancing(), this.tickCount);
}
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
我们重写了 onSyncedDataUpdated 方法。 每当同步数据在服务器或客户端更新时,该方法都会被调用。 这里的 if 语句会检查被更新的同步数据是否为跳舞同步数据。
现在,我们继续处理动画本身。 我们会创建 MiniGolemAnimations 类,并添加一个 AnimationDefinition,用于定义动画应如何应用到实体上。
java
public class MiniGolemAnimations {
public static final AnimationDefinition DANCING = AnimationDefinition.Builder.withLength(1)
.looping()
.addAnimation(PartNames.HEAD, new AnimationChannel(
AnimationChannel.Targets.ROTATION,
new Keyframe(0, KeyframeAnimations.degreeVec(0, 0, 0), AnimationChannel.Interpolations.LINEAR),
new Keyframe(0.2f, KeyframeAnimations.degreeVec(0, 0, 45), AnimationChannel.Interpolations.LINEAR),
new Keyframe(0.4f, KeyframeAnimations.degreeVec(0, 0, 0), AnimationChannel.Interpolations.LINEAR),
new Keyframe(0.6f, KeyframeAnimations.degreeVec(0, 0, 0), AnimationChannel.Interpolations.LINEAR),
new Keyframe(0.8f, KeyframeAnimations.degreeVec(0, 0, -45), AnimationChannel.Interpolations.LINEAR),
new Keyframe(1, KeyframeAnimations.degreeVec(0, 0, 0), AnimationChannel.Interpolations.LINEAR)
))
.addAnimation(PartNames.LEFT_LEG, new AnimationChannel(
AnimationChannel.Targets.ROTATION,
new Keyframe(0, KeyframeAnimations.degreeVec(0, 0, 0), AnimationChannel.Interpolations.LINEAR),
new Keyframe(0.2f, KeyframeAnimations.degreeVec(0, 0, 45), AnimationChannel.Interpolations.LINEAR),
new Keyframe(0.4f, KeyframeAnimations.degreeVec(0, 0, 0), AnimationChannel.Interpolations.LINEAR)
))
.addAnimation(PartNames.RIGHT_LEG, new AnimationChannel(
AnimationChannel.Targets.ROTATION,
new Keyframe(0.5f, KeyframeAnimations.degreeVec(0, 0, 0), AnimationChannel.Interpolations.LINEAR),
new Keyframe(0.7f, KeyframeAnimations.degreeVec(0, 0, -45), AnimationChannel.Interpolations.LINEAR),
new Keyframe(0.9f, KeyframeAnimations.degreeVec(0, 0, 0), AnimationChannel.Interpolations.LINEAR)
))
.build();
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
这里包含了不少内容,请注意以下关键点:
withLength(1)会让动画持续 1 秒。looping()会让动画反复循环播放。- 随后是一系列
addAnimation调用,它们会添加针对各个模型部件的独立动画。 这里,我们为头部、左腿和右腿分别设置了不同的动画。- 每个动画都会作用于该模型部件的特定属性。在本例中,我们每次修改的都是模型部件的旋转。
- 一个动画由一组关键帧组成。 当动画时间(经过的秒数)等于某个关键帧的时间时,我们所作用的属性值就会等于该关键帧中指定的值(在本例中即旋转值)。
- 当动画时间位于两个关键帧之间时,该属性值会在相邻两个关键帧之间进行插值(混合)。
- 我们使用了线性插值,这是最简单的插值方式,会使属性值(在本例中是模型部件的旋转)以恒定速率从一个关键帧变化到下一个关键帧。 原版还提供了 Catmull-Rom 样条插值,可在关键帧之间产生更平滑的过渡。
- 模组开发者也可以创建自定义插值类型。
最后,让我们将动画接入模型:
java
private final KeyframeAnimation dancing;
public MiniGolemEntityModel(ModelPart root) {
// ...
this.dancing = MiniGolemAnimations.DANCING.bake(root);
//:::model1
}
@Override
public void setupAnim(MiniGolemEntityRenderState state) {
super.setupAnim(state);
//:::model_animation
if (state.dancingAnimationState.isStarted()) {
this.dancing.apply(state.dancingAnimationState, state.ageInTicks);
} else {
// ... the leg swing animation code from before
}
//:::model_animation
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
当动画正在播放时,我们会应用该动画;否则,则使用原来的腿部动画代码。
添加刷怪蛋
若要为迷你傀儡实体添加刷怪蛋,请参阅关于创建刷怪蛋的完整文章。







