Привет, всем. Это будет завершающая часть серии статей по созданию новой коровы. Мы создали моба, добавили ему новую модель, а теперь пришла пора улучшить базовый ИИ. Конечная цель такова: можно доить если есть молоко, молоко может быть только после еды (определённое время после), ест сама траву (как овцы) и имеет состояние голода, во время которого молоко производиться не будет. На деле тут всё будет очень просто и часть логики мы просто интегрируем от других мобов, а также добавим несколько важных переменных.
Этот гайд начинаем только после этого:
Ну, сейчас все увидите. Погнали.
Java-часть. Пара новых моментов…
Для начала правим класс GSCowEntity.java. Нам нужно добавить пару глобальных переменных. Как и следует из названий, первая – наличие молока, а вторая – голодная ли корова или нет. Сразу говорю, что можно бы взять вторую не как булеву, а как какое-то число, в зависимости от которого и будет определено голодна корова или сыта. Второй вариант позволяет сделать более гибкую систему питания и выработки молока. К примеру: если корова сыта не менее чем на 50%, то она делает молоко, а иначе хочет есть и не делает. Но я специально упростил модель, чтобы не усложнять :D.
1 2 |
private static final DataParameter<Boolean> HAS_MILK = EntityDataManager.createKey(GSCowEntity.class, DataSerializers.BOOLEAN); private static final DataParameter<Boolean> HUNGRY = EntityDataManager.createKey(GSCowEntity.class, DataSerializers.BOOLEAN); |
Это не всё с глобальными, помимо этих двух нам нужен таймер поедания травы, который будет запускать анимацию опускания головы, объект логики поедания травы и переменная, которая будет определять время до готовности молока.
1 2 3 |
private int cowTimer; private EatGrassGoal eatGrassGoal; public int timeUntilMilk = this.rand.nextInt(1000) + 500; |
Обновим goals сущности:
1 2 3 4 5 6 7 8 9 10 11 12 |
protected void registerGoals() { this.eatGrassGoal = new EatGrassGoal(this); this.goalSelector.addGoal(0, new SwimGoal(this)); this.goalSelector.addGoal(1, new PanicGoal(this, 2.0D)); this.goalSelector.addGoal(2, new BreedGoal(this, 1.0D)); this.goalSelector.addGoal(3, new TemptGoal(this, 1.25D, Ingredient.fromItems(Items.WHEAT), false)); this.goalSelector.addGoal(4, new FollowParentGoal(this, 1.25D)); this.goalSelector.addGoal(5, this.eatGrassGoal); this.goalSelector.addGoal(6, new WaterAvoidingRandomWalkingGoal(this, 1.0D)); this.goalSelector.addGoal(7, new LookAtGoal(this, PlayerEntity.class, 6.0F)); this.goalSelector.addGoal(8, new LookRandomlyGoal(this)); } |
Тут мы проинициализировали логику поедания травы, а также добавили её в список целей.
Добавляем функцию updateAITasks()
1 2 3 4 |
protected void updateAITasks() { this.cowTimer = this.eatGrassGoal.getEatingGrassTimer(); super.updateAITasks(); } |
Тут мы синхронизируем внутренний таймер сущности с таймером поедания травы. Именно благодаря этому сущность будет знать когда опускать голову. Иными словами: опускание головы и факт поедания травы не связанны никак, но в зависимости от времени до поедания – срабатывает анимация «питания».
Ещё нам нужно добавить livingTick()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public void livingTick() { if (this.world.isRemote) { //this.glowing = hasMilk(); this.cowTimer = Math.max(0, this.cowTimer - 1); } if (!hasMilk() && !isHungry()) { if (!this.world.isRemote && this.isAlive() && !this.isChild() && --this.timeUntilMilk <= 0) { this.playSound(SoundEvents.ENTITY_COW_AMBIENT, 1.0F, (this.rand.nextFloat() - this.rand.nextFloat()) * 0.2F + 1.0F); setHasMilk(true); } } super.livingTick(); } |
Эта функция будет вызываться каждый тик для живой сущности. Отдельно происходит отсчёт таймера, а отдельно проверка молока на готовность.
С первым думаю всё ясно, а второй рассмотрим подробнее. Условие гласит: если корова не голодна и уже не имеет молоко, то идём к следующим условиям: если она не ребёнок, жива, а также время до молока меньше или равно нулю – говорим му и указываем, что молоко уже есть. Если убрать комментарий со строки this.glowing = hasMilk();, то корова с молоком будет иметь подсветку, тогда как без – нет.
Java-часть. И ещё пара новых моментов…
Далее добавляем такую функцию:
1 2 3 4 5 6 7 8 |
@OnlyIn(Dist.CLIENT) public void handleStatusUpdate(byte id) { if (id == 10) { this.cowTimer = 40; } else { super.handleStatusUpdate(id); } } |
Честно, я имею приблизительное представление о том, что такое id == 10, так как в зависимости от id жители выражают эмоции, а голем может носить цветочек, а если id 40, то это обработка активации тотема бессмертия. Данный код я взял у овцы, так что пока оставим так, все вопросы к ней. Список id и их описаний я не нашёл.
И добавляем пару функций, которые будут участвовать в обработке наклона головы коровы. Я их тоже бессовестно спёр из класса овцы и даже не особо правил, так как анимация меня устраивала. Единственное, что в классе модели нужно будет изменить высоту головы на нашу.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
@OnlyIn(Dist.CLIENT) public float getHeadRotationPointY(float p_70894_1_) { if (this.cowTimer <= 0) { return 0.0F; } else if (this.cowTimer >= 4 && this.cowTimer <= 36) { return 1.0F; } else { return this.cowTimer < 4 ? ((float)this.cowTimer - p_70894_1_) / 4.0F : -((float)(this.cowTimer - 40) - p_70894_1_) / 4.0F; } } @OnlyIn(Dist.CLIENT) public float getHeadRotationAngleX(float p_70890_1_) { if (this.cowTimer > 4 && this.cowTimer <= 36) { float f = ((float)(this.cowTimer - 4) - p_70890_1_) / 32.0F; return ((float)Math.PI / 5F) + 0.21991149F * MathHelper.sin(f * 28.7F); } else { return this.cowTimer > 0 ? ((float)Math.PI / 5F) : this.rotationPitch * ((float)Math.PI / 180F); } } |
Как и я говорил, в зависимости от внутреннего таймера будет запущена отрисовка анимации, но эти функции будут вызваны не тут, а в классе модели.
Теперь можно добавить функции для более удобной работы с состояниями коровы:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public boolean hasMilk() { return this.dataManager.get(HAS_MILK); } public void setHasMilk(boolean hasMilk) { this.dataManager.set(HAS_MILK, hasMilk); } public boolean isHungry() { return this.dataManager.get(HUNGRY); } public void setIsHungry(boolean isHungry) { this.dataManager.set(HUNGRY, isHungry); } |
Теперь функции для регистрации параметров коровы, их считывания и записи:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
protected void registerData() { super.registerData(); this.dataManager.register(HAS_MILK, new Random().nextBoolean()); this.dataManager.register(HUNGRY, new Random().nextBoolean()); } public void writeAdditional(CompoundNBT compound) { super.writeAdditional(compound); compound.putBoolean("hasMilk", this.hasMilk()); compound.putBoolean("isHungry", this.isHungry()); } public void readAdditional(CompoundNBT compound) { super.readAdditional(compound); this.setHasMilk(compound.getBoolean("hasMilk")); this.setIsHungry(compound.getBoolean("isHungry")); } |
Так же нам нужно добавить функцию eatGrassBonus() которая будет вызвана после поедания травы.
1 2 3 4 5 6 7 8 9 10 11 |
public void eatGrassBonus() { if (isHungry()) this.timeUntilMilk = this.rand.nextInt(1000) + 500; this.setIsHungry(false); if (this.isChild()) { this.addGrowth(60); } } |
Я делаю проверку до только потому, что корова может есть несколько раз до готовности молока и без проверки таймер будет сброшен после каждого приёма пищи. А так он будет запущен, как только корова перестала быть голодной и пускай дальше делает, что хочет.
В завершение новая версия функции processInteract(), которая обрабатывает применение предметов на сущность.
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 |
public boolean processInteract(PlayerEntity player, Hand hand) { ItemStack itemstack = player.getHeldItem(hand); if (itemstack.getItem() == Items.BUCKET && !player.abilities.isCreativeMode && !this.isChild() && hasMilk()) { player.playSound(SoundEvents.ENTITY_COW_MILK, 1.0F, 1.0F); itemstack.shrink(1); if (itemstack.isEmpty()) { player.setHeldItem(hand, new ItemStack(Items.MILK_BUCKET)); } else if (!player.inventory.addItemStackToInventory(new ItemStack(Items.MILK_BUCKET))) { player.dropItem(new ItemStack(Items.MILK_BUCKET), false); } setHasMilk(false); setIsHungry(true); return true; } else if (itemstack.getItem() == Items.DEBUG_STICK) { if(!world.isRemote) { player.sendMessage(new StringTextComponent("[TUTORMOD]\nHAS MILK: " + hasMilk() + "\nHUNGRY: " + isHungry() + "\nTIME UNTIL MILK: " + timeUntilMilk)); } return true; } else { return super.processInteract(player, hand); } } |
К условию доения я добавил проверку на наличие молока, а также возможность нажимать на корову палочкой отладки для получения информации о наличии молока, голоде и времени до появления молока.
Полный код нашего чудовища смотрите тут.
Модель
Теперь нужно научить модель отображать новые анимации, а также отображать большое вымя при наличии у коровы молока. Для «живых» анимаций есть специальная функция setLivingAnimations. Одним из параметров выступает наша сущность, что позволит при обновлении учитывать любые изменения моба.
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 |
package mod.astler.tutorial_mod_gs.client.renderer.entity.model; import com.mojang.blaze3d.matrix.MatrixStack; import com.mojang.blaze3d.vertex.IVertexBuilder; import mod.astler.tutorial_mod_gs.entity.passive.GSCowEntity; import net.minecraft.client.renderer.entity.model.EntityModel; import net.minecraft.client.renderer.model.ModelRenderer; import net.minecraft.util.math.MathHelper; public class GSCowModel extends EntityModel<GSCowEntity> { private final ModelRenderer mBody; private final ModelRenderer mRightBack; private final ModelRenderer mLeftBack; private final ModelRenderer mRightFront; private final ModelRenderer mLeftFront; private final ModelRenderer mHead; private final ModelRenderer mNose; private final ModelRenderer mRightEar; private final ModelRenderer mLeftEar; private final ModelRenderer mIDK; private float headRotationAngleX; public GSCowModel() { textureWidth = 64; textureHeight = 64; mBody = new ModelRenderer(this); mBody.setRotationPoint(0.0F, 24.0F, 0.0F); ModelRenderer legs = new ModelRenderer(this); legs.setRotationPoint(0.0F, 0.0F, 0.0F); mBody.addChild(legs); ModelRenderer back = new ModelRenderer(this); back.setRotationPoint(0.0F, 0.0F, 0.0F); legs.addChild(back); mRightBack = new ModelRenderer(this); mRightBack.setRotationPoint(-4.0F, -12.0F, 8.0F); back.addChild(mRightBack); mRightBack.setTextureOffset(0, 38).func_228303_a_(-2.0F, 0.0F, -2.0F, 4.0F, 12.0F, 4.0F, 0.0F, false); mLeftBack = new ModelRenderer(this); mLeftBack.setRotationPoint(4.0F, -12.0F, 8.0F); back.addChild(mLeftBack); mLeftBack.setTextureOffset(0, 38).func_228303_a_(-2.0F, -0.1551F, -1.8511F, 4.0F, 12.0F, 4.0F, 0.0F, false); ModelRenderer front = new ModelRenderer(this); front.setRotationPoint(0.0F, 0.0F, 0.0F); legs.addChild(front); mRightFront = new ModelRenderer(this); mRightFront.setRotationPoint(-4.0F, -12.0F, -6.0F); front.addChild(mRightFront); mRightFront.setTextureOffset(0, 38).func_228303_a_(-2.0F, 0.0F, -2.0F, 4.0F, 12.0F, 4.0F, 0.0F, false); mLeftFront = new ModelRenderer(this); mLeftFront.setRotationPoint(4.0F, -12.0F, -6.0F); front.addChild(mLeftFront); mLeftFront.setTextureOffset(0, 38).func_228303_a_(-2.0F, 0.0F, -2.0F, 4.0F, 12.0F, 4.0F, 0.0F, false); mHead = new ModelRenderer(this); mHead.setRotationPoint(0.0F, -21.0F, -8.0F); mBody.addChild(mHead); mHead.setTextureOffset(0, 22).func_228303_a_(-4.0F, -4.0F, -6.0F, 8.0F, 8.0F, 6.0F, 0.0F, false); mHead.setTextureOffset(0, 39).func_228303_a_(3.0F, -6.0F, -4.0F, 1.0F, 2.0F, 1.0F, 0.0F, false); mHead.setTextureOffset(0, 39).func_228303_a_(-4.0F, -6.0F, -4.0F, 1.0F, 2.0F, 1.0F, 0.0F, false); mNose = new ModelRenderer(this); mNose.setRotationPoint(0.0F, 4.0F, -7.0F); mHead.addChild(mNose); setRotationAngle(mNose, 0.1745F, 0.0F, 0.0F); mNose.setTextureOffset(42, 49).func_228303_a_(-3.0F, -3.0F, 0.0F, 6.0F, 3.0F, 2.0F, 0.0F, false); mRightEar = new ModelRenderer(this); mRightEar.setRotationPoint(-4.0F, -1.0F, -3.0F); mHead.addChild(mRightEar); setRotationAngle(mRightEar, 0.0873F, 0.0F, 0.0873F); mRightEar.setTextureOffset(42, 46).func_228303_a_(-2.0F, -2.0F, -1.0F, 2.0F, 1.0F, 1.0F, 0.0F, false); mRightEar.setTextureOffset(42, 45).func_228303_a_(-3.0F, -1.0F, -1.0F, 3.0F, 2.0F, 1.0F, 0.0F, false); mLeftEar = new ModelRenderer(this); mLeftEar.setRotationPoint(4.0F, -1.0F, -4.0F); mHead.addChild(mLeftEar); setRotationAngle(mLeftEar, 0.0873F, 0.0F, -0.0873F); mLeftEar.setTextureOffset(50, 45).func_228303_a_(0.0F, -1.0F, 0.0F, 3.0F, 2.0F, 1.0F, 0.0F, false); mLeftEar.setTextureOffset(51, 44).func_228303_a_(0.0F, -2.0F, 0.0F, 2.0F, 1.0F, 1.0F, 0.0F, false); ModelRenderer chest = new ModelRenderer(this); chest.setRotationPoint(0.0F, -16.0F, 0.0F); mBody.addChild(chest); chest.setTextureOffset(0, 36).func_228303_a_(-6.0F, -6.0F, -8.0F, 12.0F, 10.0F, 18.0F, 0.0F, false); mIDK = new ModelRenderer(this); mIDK.setRotationPoint(0.0F, 0.0F, 0.0F); mBody.addChild(mIDK); mIDK.setTextureOffset(0, 14).func_228303_a_(-2.0F, -12.0F, 4.0F, 4.0F, 2.0F, 6.0F, 0.0F, false); } public void setRotationAngle(ModelRenderer modelRenderer, float x, float y, float z) { modelRenderer.rotateAngleX = x; modelRenderer.rotateAngleY = y; modelRenderer.rotateAngleZ = z; } @Override public void func_225598_a_(MatrixStack p_225598_1_, IVertexBuilder p_225598_2_, int p_225598_3_, int p_225598_4_, float p_225598_5_, float p_225598_6_, float p_225598_7_, float p_225598_8_) { mBody.func_228308_a_(p_225598_1_, p_225598_2_, p_225598_3_, p_225598_4_); } public void func_225597_a_(GSCowEntity p_225597_1_, float p_225597_2_, float p_225597_3_, float p_225597_4_, float p_225597_5_, float p_225597_6_) { this.mHead.rotateAngleX = this.headRotationAngleX; this.mLeftFront.rotateAngleX = MathHelper.cos(p_225597_2_ * 0.6662F) * 1.4F * p_225597_3_; this.mLeftBack.rotateAngleX = MathHelper.cos(p_225597_2_ * 0.6662F + (float) Math.PI) * 1.4F * p_225597_3_; this.mRightFront.rotateAngleX = MathHelper.cos(p_225597_2_ * 0.6662F + (float) Math.PI) * 1.4F * p_225597_3_; this.mRightBack.rotateAngleX = MathHelper.cos(p_225597_2_ * 0.6662F) * 1.4F * p_225597_3_; } public void setLivingAnimations(GSCowEntity entityIn, float limbSwing, float limbSwingAmount, float partialTick) { super.setLivingAnimations(entityIn, limbSwing, limbSwingAmount, partialTick); this.mHead.rotationPointY = entityIn.getHeadRotationPointY(partialTick) * 9 - 22; this.headRotationAngleX = entityIn.getHeadRotationAngleX(partialTick); mIDK.showModel = entityIn.hasMilk(); } } |
Ну, похоже, что на этом всё. Можно запускать!
Пингбэк: Создание модов для Minecraft 1.15 – GeekStand