NeoForge-1.20.4Mod开发教程之第一个生物

本篇教程参考为Flandre芙兰的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()

从代码逻辑看,它做了两件事:

  1. 先判断当前是不是服务端

  2. 如果是服务端,就查找距离 firstAnimal 10 格内最近的玩家,并给这个玩家添加 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;

leg1leg4 同步摆动

leg2leg3 用加上 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

  • 独立的模型与贴图路径

  • 客户端模型层、渲染器和属性注册

© 版权声明
THE END
喜欢就支持一下吧
点赞5赞赏 分享
评论 抢沙发

    暂无评论内容