本篇教程参考为的NeoForge1.20.4Mod开发的视频,这篇教程是我边学边总结的,与其说是教程更像我的学习笔记,如有不清楚的地方,大家可以在评论区提问。在我力所能及的范围内,我都会解答的。
一个生物实体至少包含以下几个部分:
-
实体类,决定这个生物本身的AI目标和属性
-
自定义 Goal,补充它自己的特殊行为
-
渲染类,决定客户端如何渲染这个实体
-
模型类,定义这个实体的模型结构和动画
-
客户端事件注册,把模型层、渲染器和属性注册到游戏里
我们先从实体类开始
创建FirstAnimal类
public class FirstAnimal extends Animal {
public FirstAnimal(EntityType<? extends FirstAnimal> entityType, Level level) {
super(entityType, level);
}
@Override
protected void registerGoals() {
this.goalSelector.addGoal(0, new FloatGoal(this));
this.goalSelector.addGoal(1, new PanicGoal(this, 2.0D));
this.goalSelector.addGoal(5, new WaterAvoidingRandomStrollGoal(this, 1.0D));
this.goalSelector.addGoal(6, new LookAtPlayerGoal(this, Player.class, 6.0F));
this.goalSelector.addGoal(7, new RandomLookAroundGoal(this));
this.goalSelector.addGoal(8, new MyGoal(this));
}
@Override
public @Nullable AgeableMob getBreedOffspring(ServerLevel level, AgeableMob otherParent) {
return null;
}
public static AttributeSupplier.Builder initAttributes() {
return Mob.createMobAttributes()
.add(Attributes.MAX_HEALTH, 10.0D)//生命值
.add(Attributes.MOVEMENT_SPEED, 0.2D);//移动速度
}
}
这里 FirstAnimal 直接继承的是 Animal
说明这个实体走的是动物实体这一套逻辑
registerGoals() 里给它注册了 6 个目标:
-
FloatGoal:在水里时可以上浮 -
PanicGoal(this, 2.0D):恐慌逃跑,速度是2.0D -
WaterAvoidingRandomStrollGoal(this, 1.0D):平时会随机散步,尽量避开水 -
LookAtPlayerGoal(this, Player.class, 6.0F):会看向 6 格内的玩家 -
RandomLookAroundGoal:随机转头 -
MyGoal(this):自定义 Goal
getBreedOffspring(...) 现在直接返回了 null
这意味着从当前这份代码来看,它虽然继承了 Animal,但这里并没有提供繁殖后代的实现
创建MyGoal类
public class MyGoal extends Goal {
public final FirstAnimal firstAnimal;
public MyGoal(FirstAnimal firstAnimal) {
this.firstAnimal = firstAnimal;
}
@Override
public boolean canUse() {
Level level = this.firstAnimal.level();
if (!level.isClientSide) {
Player nearestPlayer = level.getNearestPlayer(this.firstAnimal, 10);
if (nearestPlayer != null) {
nearestPlayer.addEffect(new MobEffectInstance(MobEffects.HUNGER, 100, 0, false, false));
}
}
return true;
}
}
这个 MyGoal 比较简单,它继承 Goal,只重写了 canUse()
从代码逻辑看,它做了两件事:
-
先判断当前是不是服务端
-
如果是服务端,就查找距离
firstAnimal10 格内最近的玩家,并给这个玩家添加HUNGER效果
效果参数是:
-
持续时间
100 -
等级
0 -
ambient = false -
visible = false
需要注意的是,这里写效果的地方是在 canUse() 里,而且最后直接 return true
这个 Goal 的“可用性判断”和“施加效果”是写在一起的
创建FirstAnimalRenderer类
public class FirstAnimalRenderer extends MobRenderer<FirstAnimal, FirstAnimalModel> {
public FirstAnimalRenderer(EntityRendererProvider.Context context) {
super(context, new FirstAnimalModel(context.bakeLayer(FirstAnimalModel.LAYER_LOCATION)), 1f);
}
@Override
public @NotNull ResourceLocation getTextureLocation(FirstAnimal entity) {
return new ResourceLocation(ExampleMod.MODID, "textures/entity/first_animal.png");
}
}
这里的渲染器继承的是 MobRenderer<FirstAnimal, FirstAnimalModel>
说明这个实体走的是标准生物渲染流程
构造方法里做了两件事:
-
用
context.bakeLayer(FirstAnimalModel.LAYER_LOCATION)烘焙模型层 -
创建
FirstAnimalModel
第三个参数是 1f,从 MobRenderer 的构造方式来看,这里传入的是阴影大小
创建FirstAnimalModel类
这个类上面已经有注释,它是由 Blockbench 导出的模型代码
public class FirstAnimalModel extends EntityModel<FirstAnimal> {
public static final ModelLayerLocation LAYER_LOCATION = new ModelLayerLocation(new ResourceLocation(ExampleMod.MODID, "first_animal"), "main");
private final ModelPart leg4;
private final ModelPart leg3;
private final ModelPart leg2;
private final ModelPart leg1;
private final ModelPart body;
private final ModelPart head;
public FirstAnimalModel(ModelPart root) {
this.leg4 = root.getChild("leg4");
this.leg3 = root.getChild("leg3");
this.leg2 = root.getChild("leg2");
this.leg1 = root.getChild("leg1");
this.body = root.getChild("body");
this.head = root.getChild("head");
}
public static LayerDefinition createBodyLayer() {
MeshDefinition meshdefinition = new MeshDefinition();
PartDefinition partdefinition = meshdefinition.getRoot();
PartDefinition leg4 = partdefinition.addOrReplaceChild("leg4", CubeListBuilder.create().texOffs(28, 28).addBox(-2.0F, 0.0F, -1.0F, 4.0F, 12.0F, 4.0F, new CubeDeformation(0.0F)), PartPose.offset(4.0F, 12.0F, -6.0F));
PartDefinition leg3 = partdefinition.addOrReplaceChild("leg3", CubeListBuilder.create().texOffs(0, 42).addBox(-2.0F, 0.0F, -1.0F, 4.0F, 12.0F, 4.0F, new CubeDeformation(0.0F)), PartPose.offset(-4.0F, 12.0F, -6.0F));
PartDefinition leg2 = partdefinition.addOrReplaceChild("leg2", CubeListBuilder.create().texOffs(44, 0).addBox(-2.0F, 0.0F, -2.0F, 4.0F, 12.0F, 4.0F, new CubeDeformation(0.0F)), PartPose.offset(4.0F, 12.0F, 7.0F));
PartDefinition leg1 = partdefinition.addOrReplaceChild("leg1", CubeListBuilder.create().texOffs(16, 44).addBox(-2.0F, 0.0F, -2.0F, 4.0F, 12.0F, 4.0F, new CubeDeformation(0.0F)), PartPose.offset(-4.0F, 12.0F, 7.0F));
PartDefinition body = partdefinition.addOrReplaceChild("body", CubeListBuilder.create().texOffs(0, 0).addBox(-6.0F, -10.0F, -7.0F, 12.0F, 18.0F, 10.0F, new CubeDeformation(0.0F))
.texOffs(44, 16).addBox(-2.0F, 2.0F, -8.0F, 4.0F, 6.0F, 1.0F, new CubeDeformation(0.0F)), PartPose.offsetAndRotation(0.0F, 5.0F, 2.0F, 1.5708F, 0.0F, 0.0F));
PartDefinition head = partdefinition.addOrReplaceChild("head", CubeListBuilder.create().texOffs(0, 28).addBox(-4.0F, -4.0F, -6.0F, 8.0F, 8.0F, 6.0F, new CubeDeformation(0.0F))
.texOffs(44, 23).addBox(4.0F, -5.0F, -4.0F, 1.0F, 3.0F, 1.0F, new CubeDeformation(0.0F))
.texOffs(44, 27).addBox(-5.0F, -5.0F, -4.0F, 1.0F, 3.0F, 1.0F, new CubeDeformation(0.0F)), PartPose.offset(0.0F, 4.0F, -8.0F));
return LayerDefinition.create(meshdefinition, 64, 64);
}
@Override
public void renderToBuffer(PoseStack poseStack, VertexConsumer vertexConsumer, int packedLight, int packedOverlay, float red, float green, float blue, float alpha) {
leg4.render(poseStack, vertexConsumer, packedLight, packedOverlay, red, green, blue, alpha);
leg3.render(poseStack, vertexConsumer, packedLight, packedOverlay, red, green, blue, alpha);
leg2.render(poseStack, vertexConsumer, packedLight, packedOverlay, red, green, blue, alpha);
leg1.render(poseStack, vertexConsumer, packedLight, packedOverlay, red, green, blue, alpha);
body.render(poseStack, vertexConsumer, packedLight, packedOverlay, red, green, blue, alpha);
head.render(poseStack, vertexConsumer, packedLight, packedOverlay, red, green, blue, alpha);
}
@Override
public void setupAnim(FirstAnimal entity, float limbSwing, float limbSwingAmount, float ageInTicks, float netHeadYaw, float headPitch) {
this.head.xRot = headPitch * ((float)Math.PI / 180F);
this.head.yRot = netHeadYaw * ((float)Math.PI / 180F);
float swing = Mth.cos(limbSwing * 0.6662F) * 1.4F * limbSwingAmount;
this.leg1.xRot = swing;
this.leg4.xRot = swing;
this.leg2.xRot = Mth.cos(limbSwing * 0.6662F + (float)Math.PI) * 1.4F * limbSwingAmount;
this.leg3.xRot = Mth.cos(limbSwing * 0.6662F + (float)Math.PI) * 1.4F * limbSwingAmount;
}
}
setupAnim() 里做了两组动画:
第一组是头部转动:
this.head.xRot = headPitch * ((float)Math.PI / 180F);
this.head.yRot = netHeadYaw * ((float)Math.PI / 180F);
这里把传入的头部角度从角度值换算成弧度,然后赋给 head
第二组是四条腿的摆动:
float swing = Mth.cos(limbSwing * 0.6662F) * 1.4F * limbSwingAmount;
this.leg1.xRot = swing;
this.leg4.xRot = swing;
this.leg2.xRot = Mth.cos(limbSwing * 0.6662F + (float)Math.PI) * 1.4F * limbSwingAmount;
this.leg3.xRot = Mth.cos(limbSwing * 0.6662F + (float)Math.PI) * 1.4F * limbSwingAmount;
leg1 和 leg4 同步摆动
leg2 和 leg3 用加上 PI 的余弦值,所以和前一组腿是反向摆动
这就是比较典型的四足生物行走动画写法
在ClientEventHandler中注册first_animal相关内容
模型层注册
@SubscribeEvent
public static void registerEntityLayers(EntityRenderersEvent.RegisterLayerDefinitions event) {
event.registerLayerDefinition(FirstAnimalModel.LAYER_LOCATION, FirstAnimalModel::createBodyLayer);
event.registerLayerDefinition(FlyingSwordModel.LAYER_LOCATION, FlyingSwordModel::createBodyLayer);
}
其中和 first_animal 有关的是这一句:
event.registerLayerDefinition(FirstAnimalModel.LAYER_LOCATION, FirstAnimalModel::createBodyLayer);
它的作用就是把 FirstAnimalModel 的 LayerDefinition 注册给客户端
这样渲染器里调用 context.bakeLayer(FirstAnimalModel.LAYER_LOCATION) 时,才能真正烘焙出 ModelPart
渲染器注册
@SubscribeEvent
public static void onClientEvent(FMLClientSetupEvent event){
event.enqueueWork(() -> {
BlockEntityRenderers.register(ModBlockEntities.RUBY_FRAME_BLOCK_ENTITY.get(),RubyFrameBlockEntityRender::new);
EntityRenderers.register(ModEntityTypes.FLYING_SWORD_ENTITY.get(), FlyingSwordEntityRenderer::new);
EntityRenderers.register(ModEntityTypes.FIRST_ANIMAL.get(), FirstAnimalRenderer::new);
});
}
其中和 first_animal 有关的是这一句:
EntityRenderers.register(ModEntityTypes.FIRST_ANIMAL.get(), FirstAnimalRenderer::new);
它把 FIRST_ANIMAL 这个实体类型和 FirstAnimalRenderer 绑定起来
属性注册
@SubscribeEvent
public static void setupAttributes(EntityAttributeCreationEvent event) {
event.put(ModEntityTypes.FIRST_ANIMAL.get(), FirstAnimal.initAttributes().build());
}
这里调用的就是前面 FirstAnimal 里的 initAttributes()
也就是把:
-
MAX_HEALTH = 10.0D -
MOVEMENT_SPEED = 0.2D
真正注册给 FIRST_ANIMAL
这样一来,first_animal 这一套最基本的服务端逻辑和客户端渲染逻辑就串起来了
它已经具备了:
-
基础动物实体行为
-
一个自定义近距离施加饥饿效果的 Goal
-
独立的模型与贴图路径
-








暂无评论内容