Привет! Думал подождать выхода forge для 1.16, но как-то скучно стало вечером и я решил улучшить… корову. А вообще из этого вполне можно сделать урок по созданию моба. Так я подумал, вот, делаю :D. Но чтобы не грузить в этот раз лишним – будем использовать код сущности без всего лишнего и со стандартной моделью. Впрочем, финальный результат тоже уже готов.
Рекомендую пройти предыдущие уроки для большего понимания.
Финальный результат: Моя корова уже имеет свою, отличную от оригинальной модель (хотя можно было просто редактировать старую, но я хотел попробовать сам). Сама суть новой коровы – исправление логической ошибки оригинальной. Я считаю, что странно это, доить корову бесконечно и без пауз. А потому мою можно доить только когда молоко «готово». После этого ей нужна еда и время. Ест она сама, траву и блоки с травой (взят код и анимация овцы), а также добавлен таймер готовности молока (как с яйцами у кур, но тут только с момента сытости).
Также корова сохраняет состояние голода и готовности молока при выходе из игры, что логично.
Окей. Значит решили, урок в первую очередь о добавлении новых мобов, а не их логике или моделях, а потому сегодня максимальное внимание базовым вещам, а остальное разберём подробнее уже позже. Погнали!
JAVA-часть
В первую очередь нам нужно создать класс сущности. Как обычно, я стараюсь соответствовать оригинальной игре в именах файлов и папок. У меня класс получает почетное имя: GSCowEntity.
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 |
package mod.astler.tutorial_mod_gs.entity.passive; import mod.astler.tutorial_mod_gs.entity.ModEntityTypes; import net.minecraft.block.BlockState; import net.minecraft.entity.*; import net.minecraft.entity.ai.goal.*; import net.minecraft.entity.passive.AnimalEntity; import net.minecraft.entity.player.PlayerEntity; import net.minecraft.item.ItemStack; import net.minecraft.item.Items; import net.minecraft.item.crafting.Ingredient; import net.minecraft.util.DamageSource; import net.minecraft.util.Hand; import net.minecraft.util.SoundEvent; import net.minecraft.util.SoundEvents; import net.minecraft.util.math.BlockPos; import net.minecraft.world.World; public class GSCowEntity extends AnimalEntity { public GSCowEntity(EntityType<? extends GSCowEntity> type, World worldIn) { super(type, worldIn); } protected void registerGoals() { 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(6, new WaterAvoidingRandomWalkingGoal(this, 1.0D)); this.goalSelector.addGoal(7, new LookAtGoal(this, PlayerEntity.class, 6.0F)); this.goalSelector.addGoal(8, new LookRandomlyGoal(this)); } protected void registerAttributes() { super.registerAttributes(); this.getAttribute(SharedMonsterAttributes.MAX_HEALTH).setBaseValue(20.0D); this.getAttribute(SharedMonsterAttributes.MOVEMENT_SPEED).setBaseValue(0.25F); } protected SoundEvent getAmbientSound() { return SoundEvents.ENTITY_COW_AMBIENT; } protected SoundEvent getHurtSound(DamageSource damageSourceIn) { return SoundEvents.ENTITY_COW_HURT; } protected SoundEvent getDeathSound() { return SoundEvents.ENTITY_COW_DEATH; } protected void playStepSound(BlockPos pos, BlockState blockIn) { this.playSound(SoundEvents.ENTITY_COW_STEP, 0.15F, 1.0F); } protected float getSoundVolume() { return 0.4F; } public boolean processInteract(PlayerEntity player, Hand hand) { ItemStack itemstack = player.getHeldItem(hand); if (itemstack.getItem() == Items.BUCKET && !player.abilities.isCreativeMode && !this.isChild()) { 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); } return true; } else { return super.processInteract(player, hand); } } public GSCowEntity createChild(AgeableEntity ageableEntity) { return ModEntityTypes.GS_COW.get().create(this.world); } protected float getStandingEyeHeight(Pose poseIn, EntitySize sizeIn) { return this.isChild() ? sizeIn.height * 0.95F : 1.3F; } } |
По сути дела, тут мы имеем копию обычной коровы. Но! Мы же будем потом её улучшать, так что всё ок.
Думаю, что из названий вполне очевидно, что за что отвечает. Тут у нас указаны звуки (шагов, смерти), громкость, высота глаз коровы и детёныша, атрибуты, отвечающие за скорость и здоровье, а также goals, т.е. цели, которые и создают своего рода модель поведения.
В данном случае мы в первую очередь плаваем, далее паникуем, если на то есть основания, после неё идёт логика размножения, преследования за тем, у кого пшеница, преследование родителя, маршрут с обходом воды, посмотреть на игрока или на случайную цель.
Так как в приоритетах указаны они последовательно, то даже паникуя она не утонет, т.к. сначала проверит необходимость плыть, а только потом уже начнёт панику.
Теперь нужно зарегистрировать сущность. Как вы могли заметить в структурах, блоках или предметах – регистрация аналогична. Это же работает и тут.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
package mod.astler.tutorial_mod_gs.entity; import mod.astler.tutorial_mod_gs.TutorialGSMod; import mod.astler.tutorial_mod_gs.entity.passive.GSCowEntity; import net.minecraft.entity.EntityClassification; import net.minecraft.entity.EntityType; import net.minecraft.util.ResourceLocation; import net.minecraftforge.fml.RegistryObject; import net.minecraftforge.registries.DeferredRegister; import net.minecraftforge.registries.ForgeRegistries; public class ModEntityTypes { public static final DeferredRegister<EntityType<?>> ENTITY_TYPES = new DeferredRegister<>(ForgeRegistries.ENTITIES, TutorialGSMod.MODID); public static final RegistryObject<EntityType<GSCowEntity>> GS_COW = ENTITY_TYPES .register("gs_cow", () -> EntityType.Builder.<GSCowEntity>create(GSCowEntity::new, EntityClassification.CREATURE) .size(0.9f, 1.49f) .build(new ResourceLocation(TutorialGSMod.MODID, "gs_cow").toString())); } |
Ну, может немного сложнее, но всё так же нужно указывать класс, имя, ресурсы. Также тут мы указываем тип и размеры сущности. В остальном вроде ничего нового.
Регистрируем ModEntityTypes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
package mod.astler.tutorial_mod_gs; import mod.astler.tutorial_mod_gs.entity.ModEntityTypes; import net.minecraftforge.eventbus.api.IEventBus; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @Mod(TutorialGSMod.MODID) @Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD) public class TutorialGSMod { public static final Logger LOGGER = LogManager.getLogger(); public static final String MODID = "tutorial_mod_gs"; public TutorialGSMod() { IEventBus bus = FMLJavaModLoadingContext.get().getModEventBus(); ModEntityTypes.ENTITY_TYPES.register(bus); } } |
Рендер моба в мире
И переходим к интересной части. Дело в том, что в отличие от блоков или предметов, сущностям нужно создать ещё и класс, который будет отвечать за рендер модели, а ещё тут мы укажем текстуру. Её можно прописать прямо, как это я сделал, а можно добавить немного логики или вариативности, как у жителей, лошадей, кроликов и т.д. Ну, вы поняли. Этим займёмся позже.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
package mod.astler.tutorial_mod_gs.client.entity.render; import mod.astler.tutorial_mod_gs.TutorialGSMod; import mod.astler.tutorial_mod_gs.entity.passive.GSCowEntity; import net.minecraft.client.renderer.entity.EntityRendererManager; import net.minecraft.client.renderer.entity.MobRenderer; import net.minecraft.client.renderer.entity.model.CowModel; import net.minecraft.util.ResourceLocation; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.api.distmarker.OnlyIn; @OnlyIn(Dist.CLIENT) public class GSCowRenderer extends MobRenderer<GSCowEntity, CowModel<GSCowEntity>> { private static final ResourceLocation COW_TEXTURES = new ResourceLocation(TutorialGSMod.MODID, "textures/entity/cow/gs_cow.png"); public GSCowRenderer(EntityRendererManager renderManagerIn) { super(renderManagerIn, new CowModel<>(), 0.7F); } public ResourceLocation getEntityTexture(GSCowEntity entity) { return COW_TEXTURES; } } |
Как видите, вот путь к моей текстуре, а также указана модель обычной коровы из игры.
И теперь нам нужно связать сущность с её классом рендера, а также давайте добавим новую корову во все биомы. Для этого создадим ещё один файл ClientEventBusSubscriber:
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 |
package mod.astler.tutorial_mod_gs.client; import mod.astler.tutorial_mod_gs.TutorialGSMod; import mod.astler.tutorial_mod_gs.client.entity.render.GSCowRenderer; import mod.astler.tutorial_mod_gs.entity.ModEntityTypes; import net.minecraft.entity.EntityClassification; import net.minecraft.world.biome.Biome; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.client.registry.RenderingRegistry; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; import net.minecraftforge.registries.ForgeRegistries; @Mod.EventBusSubscriber(modid = TutorialGSMod.MODID, bus = Mod.EventBusSubscriber.Bus.MOD, value = Dist.CLIENT) public class ClientEventBusSubscriber { @SubscribeEvent public static void clientSetup(FMLClientSetupEvent event) { RenderingRegistry.registerEntityRenderingHandler(ModEntityTypes.GS_COW.get(), GSCowRenderer::new); } @SubscribeEvent public static void onInitBiomesGen(FMLCommonSetupEvent event) { for (Biome biome : ForgeRegistries.BIOMES) { biome.getSpawns(EntityClassification.CREATURE).add(new Biome.SpawnListEntry(ModEntityTypes.GS_COW.get(), 50, 3, 8)); } } } |
Где 3 и 8 это минимальное и максимальное количество сущностей при спавне.
50 – вес, если судить по имени в коде. На деле же это значение влияет на частоту появления. Куры, Эндермены и Свиньи имеют вес 10, а позже и я такой укажу. Зомби, скелеты и пауки имеют вес 100. Если переводить в проценты, то монстры с весом равным 100 будут иметь шанс на появление 19%, а с шансом 1 – всего 0.2%. Животные с весом 20 будут иметь шанс на появление 33%, а для 10 это значение равно 20%.
Т.е. уяснили, что вес – это шанс спавна. Чем больше это значение – тем больше будет в мире ваших мобов. Но при этом один и тот же вес для разных типов существ (животные, монстры и т.д.) будет давать разный шанс спавна.
Ну, вроде с этим всё…Теперь текстура. Т.к. у нас модель обычной коровы, то и текстуру нужно брать соответствующую. В следующий раз мы будем делать модель с другой текстурой, так что пока можно просто немного перекрасить существующую.
Вот вам скрин с двумя коровами, у которых абсолютно одинаковый функционал, но при этом левая уже наша собственная. Классно же!
На это вроде всё, мобов оказалось добавлять куда проще, чем я это помнил. Дальше интереснее, ведь впереди нас ждёт модель для моба, а так же целый список изменений в логике нашей, улучшенной коровы. Так что далеко не уходите :D.
Пингбэк: [1.15.2] Добавляем новую модель для моба – GeekStand
Пингбэк: Создание модов для Minecraft 1.15 – GeekStand
Будет ли обновление до 1.16? И не могли бы вы, пожалуйста, создать более подробный курс с рассмотрением, например, приручения и других голов, как их создавать?
Обновлю обязательно, но немного позже
На 1.16 всё тоже самое
Не спавнится этот моб, повторил всё один в один… Уже сто раз перепроверил.
Потом решил сделать яйцо и заспавнить вручную, игра крашнулась, с отсылкой на ошибка при рендере.