Minecraft是一款没有上限的游戏,你甚至可以在里面实现魂Like游戏中的酷炫动作:我的世界 类魂式战斗模组
今天我们尝试在模组中添加一个能够做各种动作的生物实体
1.首先,为了实现这些效果,我们需要首先使用到一个模组:geckolib(下载地址)
找到项目的build.gradle
文件,在repositories
和dependencies
中添加依赖。
repositories {
//添加这个
maven { url 'https://dl.cloudsmith.io/public/geckolib3/geckolib/maven/' }
}
dependencies {
minecraft 'net.minecraftforge:forge:1.18.2-40.1.0'
//添加这个
implementation fg.deobf('software.bernie.geckolib:geckolib-1.18-forge:3.0.18')
}
之后我们重新构建gradle项目
构建好了项目后在项目的Main类中添加一句geckolib的初始化语句:
Main.java
public Main() {
IEventBus bus = FMLJavaModLoadingContext.get().getModEventBus();
ItemInit.ITEMS.register(bus);
//添加GeckoLib初始化函数
GeckoLib.initialize();
MinecraftForge.EVENT_BUS.register(this);
}
2.之后,与之前的教程一样,我们需要在blockbench中制作一个模组中的生物实体:
进入软件后我们要找到一个插件按钮,然后再搜索栏中输入GeckoLib Animation Utils
,并下载这个插件
将我们制作好的生物实体进行模型转换工作,找到Convert Project
,之后选择Geckolib Animated Model
在这之后,你会发现你的生物实体栏多了一个Animate栏
,点击进去:
具体动作制作的视频:Blockbench动画制作
在制作好所有的动画后我们导出模型和动画json文件。
3.模型制作完成,接下来需要制作生物实体类,我们的生物的动画事件类似于状态机,所以要对生物实体的状态进行枚举。
EntityDund.java
package com.joy187.re8joymod.entity;
import java.util.Random;
import java.util.function.Predicate;
import javax.annotation.Nullable;
import net.minecraft.core.BlockPos;
import net.minecraft.sounds.SoundEvent;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.util.Mth;
import net.minecraft.world.Difficulty;
import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.world.effect.MobEffects;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.ai.attributes.AttributeSupplier;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.entity.ai.goal.BreakDoorGoal;
import net.minecraft.world.entity.ai.goal.Goal;
import net.minecraft.world.entity.ai.goal.LookAtPlayerGoal;
import net.minecraft.world.entity.ai.goal.MeleeAttackGoal;
import net.minecraft.world.entity.ai.goal.MoveThroughVillageGoal;
import net.minecraft.world.entity.ai.goal.RandomLookAroundGoal;
import net.minecraft.world.entity.ai.goal.WaterAvoidingRandomStrollGoal;
import net.minecraft.world.entity.ai.goal.target.HurtByTargetGoal;
import net.minecraft.world.entity.ai.goal.target.NearestAttackableTargetGoal;
import net.minecraft.world.entity.ai.navigation.GroundPathNavigation;
import net.minecraft.world.entity.ai.util.GoalUtils;
import net.minecraft.world.entity.animal.IronGolem;
import net.minecraft.world.entity.animal.Turtle;
import net.minecraft.world.entity.monster.Monster;
import net.minecraft.world.entity.monster.ZombifiedPiglin;
import net.minecraft.world.entity.npc.AbstractVillager;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.Vec3;
import software.bernie.geckolib3.core.IAnimatable;
import software.bernie.geckolib3.core.PlayState;
import software.bernie.geckolib3.core.builder.AnimationBuilder;
import software.bernie.geckolib3.core.controller.AnimationController;
import software.bernie.geckolib3.core.event.predicate.AnimationEvent;
import software.bernie.geckolib3.core.manager.AnimationData;
import software.bernie.geckolib3.core.manager.AnimationFactory;
import net.minecraft.network.syncher.EntityDataAccessor;
import net.minecraft.network.syncher.EntityDataSerializers;
import net.minecraft.network.syncher.SynchedEntityData;
//我们的生物类要实现一个IAnimatable的接口
public class EntityDund extends Monster implements IAnimatable{
//定义一个动画事件处理器
private AnimationFactory factory = new AnimationFactory(this);
private boolean canBreakDoors;
public boolean playAttackAnimation = false;
public int attackTick=1;
//枚举攻击状态
public static final EntityDataAccessor<Integer> STATE = SynchedEntityData.defineId(EntityDund.class,
EntityDataSerializers.INT);
private final BreakDoorGoal breakDoorGoal = new BreakDoorGoal(this, DOOR_BREAKING_PREDICATE);
private static final Predicate<Difficulty> DOOR_BREAKING_PREDICATE = (mode) -> {
return mode == Difficulty.HARD || mode == Difficulty.NORMAL;
};
public EntityDund(EntityType<? extends Monster> type, Level worldIn) {
super(type, worldIn);
this.xpReward = 10;
}
protected void registerGoals() {
this.goalSelector.addGoal(8, new LookAtPlayerGoal(this, Player.class, 8.0F));
this.goalSelector.addGoal(8, new RandomLookAroundGoal(this));
this.addBehaviourGoals();
}
protected void addBehaviourGoals() {
//将我们的攻击AI进行注册
this.goalSelector.addGoal(1, new EntityDund.AttackGoal(this));
this.goalSelector.addGoal(2, new DundAttackGoal(this, 1.0D, false));
this.goalSelector.addGoal(6, new MoveThroughVillageGoal(this, 1.0D, true, 4, this::canBreakDoors));
this.goalSelector.addGoal(7, new WaterAvoidingRandomStrollGoal(this, 1.0D));
this.targetSelector.addGoal(1, (new HurtByTargetGoal(this)).setAlertOthers(ZombifiedPiglin.class));
this.targetSelector.addGoal(2, new NearestAttackableTargetGoal<>(this, Player.class, true));
this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, AbstractVillager.class, false));
this.targetSelector.addGoal(3, new NearestAttackableTargetGoal<>(this, IronGolem.class, true));
//this.targetSelector.addGoal(5, new NearestAttackableTargetGoal<>(this, Turtle.class, 10, true, false, Turtle.BABY_ON_LAND_SELECTOR));
}
public static AttributeSupplier.Builder prepareAttributes() {
return Monster.createMonsterAttributes().add(Attributes.MAX_HEALTH, 300.0D).
add(Attributes.MOVEMENT_SPEED, 0.32D).
add(Attributes.ATTACK_DAMAGE, 5.5D);
}
public boolean doHurtTarget(Entity entityIn) {
if(!super.doHurtTarget(entityIn))
{
this.playAttackAnimation=false;
return false;
}
else{
if(entityIn instanceof LivingEntity)
{
float f = this.level.getCurrentDifficultyAt(this.blockPosition()).getEffectiveDifficulty();
((LivingEntity)entityIn).addEffect(new MobEffectInstance(MobEffects.WITHER, 100 * (int)f,0,true,true));
}
return true;
}
}
//该状态播放器控制生物平时的各种动作
private <E extends IAnimatable> PlayState predicate(AnimationEvent<E> event) {
//生物在移动,就播放移动的动画,路径和第2步的动作字符串对应
if (event.isMoving()) {
event.getController().setAnimation(new AnimationBuilder().addAnimation("animation.dund.walk", true));
return PlayState.CONTINUE;
}
//不动的话就播放闲置时的动画
event.getController().setAnimation(new AnimationBuilder().addAnimation("animation.dund.idle", true));
return PlayState.CONTINUE;
}
//该状态播放器控制生物攻击时的各种动作
private <E extends IAnimatable> PlayState predicate1(AnimationEvent<E> event) {
//如果生物攻击状态为1并且没有死亡,就执行这个攻击动画
if (this.entityData.get(STATE) == 1 && !(this.dead || this.getHealth() < 0.01 || this.isDeadOrDying())) {
event.getController().setAnimation(new AnimationBuilder().addAnimation("animation.dund.attack", true));
return PlayState.CONTINUE;
}
//如果生物攻击状态为2并且没有死亡,就执行这个攻击动画
// if (this.entityData.get(STATE) == 2 && !(this.dead || this.getHealth() < 0.01 || this.isDeadOrDying())) {
// event.getController().setAnimation(new AnimationBuilder().addAnimation("fire", true));
// return PlayState.CONTINUE;
// }
//如果生物攻击状态为3并且没有死亡,就执行这个攻击动画
//...
//播完停止
return PlayState.STOP;
}
//将我们之前的所有动画控制器进行注册
@Override
public void registerControllers(AnimationData data) {
data.addAnimationController(new AnimationController(this, "controller",
0, this::predicate));
data.addAnimationController(new AnimationController(this, "controller1",
0, this::predicate1));
}
@Override
public AnimationFactory getFactory() {
// TODO Auto-generated method stub
return this.factory;
}
//生物实体声音
@Nullable
protected SoundEvent getAmbientSound() {
return SoundEvents.RAVAGER_AMBIENT;
}
protected SoundEvent getHurtSound(DamageSource source) {
return SoundEvents.RAVAGER_HURT;
}
protected SoundEvent getDeathSound() {
return SoundEvents.RAVAGER_DEATH;
}
protected void playStepSound(BlockPos pos, BlockState blockstate) {
this.playSound(SoundEvents.RAVAGER_STEP, 0.15F, 1.0F);
}
static class DundAttackGoal extends MeleeAttackGoal {
private final EntityDund zombie;
public DundAttackGoal(EntityDund entity, double p_i46803_2_, boolean p_i46803_4_) {
super(entity, p_i46803_2_, p_i46803_4_);
this.zombie=entity;
}
}
public boolean canBreakDoors() {
return this.canBreakDoors;
}
protected boolean supportsBreakDoorGoal() {
return true;
}
//怪物能不能破门而入
public void setCanBreakDoors(boolean p_34337_) {
if (this.supportsBreakDoorGoal() && GoalUtils.hasGroundPathNavigation(this)) {
if (this.canBreakDoors != p_34337_) {
this.canBreakDoors = p_34337_;
((GroundPathNavigation)this.getNavigation()).setCanOpenDoors(p_34337_);
if (p_34337_) {
this.goalSelector.addGoal(1, this.breakDoorGoal);
} else {
this.goalSelector.removeGoal(this.breakDoorGoal);
}
}
} else if (this.canBreakDoors) {
this.goalSelector.removeGoal(this.breakDoorGoal);
this.canBreakDoors = false;
}
}
public int getAttckingState() {
return this.entityData.get(STATE);
}
public void setAttackingState(int time) {
this.entityData.set(STATE, time);
}
@Override
protected void defineSynchedData() {
super.defineSynchedData();
this.entityData.define(STATE, 0);
}
//我们生物的AI,决定使用哪种攻击方式
static class AttackGoal extends Goal {
private final EntityDund parentEntity;
//攻击计时器
protected int attackTimer = 0;
public AttackGoal(EntityDund mob) {
this.parentEntity = mob;
}
public boolean canUse() {
return this.parentEntity.getTarget() != null;
}
public void start() {
super.start();
this.parentEntity.setAggressive(true);
}
@Override
public void stop() {
super.stop();
this.parentEntity.setAggressive(false);
this.parentEntity.setAttackingState(0);
this.attackTimer = -1;
}
public void tick() {
LivingEntity livingentity = this.parentEntity.getTarget();
if (this.parentEntity.hasLineOfSight(livingentity)) {
Level world = this.parentEntity.level;
++this.attackTimer;
Random rand = new Random();
Vec3 vector3d = this.parentEntity.getViewVector(1.0F);
double d0 = Math.min(livingentity.getY(), livingentity.getY());
double d1 = Math.max(livingentity.getY(), livingentity.getY()) + 1.0D;
double d2 = livingentity.getX() - (this.parentEntity.getX() + vector3d.x * 2.0D);
double d3 = livingentity.getY(0.5D) - (0.5D + this.parentEntity.getY(0.5D));
double d4 = livingentity.getZ() - (this.parentEntity.getZ() + vector3d.z * 2.0D);
float f = (float) Mth.atan2(livingentity.getZ() - parentEntity.getZ(),
livingentity.getX() - parentEntity.getX());
this.parentEntity.getNavigation().moveTo(livingentity, 1.5D);
if (this.attackTimer == 15) {
// 将该生物攻击状态设置为2
this.parentEntity.setAttackingState(2);
}
else{
// 将该生物攻击状态设置为1
this.parentEntity.setAttackingState(1);
}
//计时器到头后,将攻击状态设置为0,同时计时器清0
if (this.attackTimer == 30) {
this.parentEntity.setAttackingState(0);
this.attackTimer = -5;
}
}
else if (this.attackTimer > 0) {
--this.attackTimer;
}
this.parentEntity.lookAt(livingentity, 30.0F, 30.0F);
}
}
}
4.新建生物实体模型文件ModelDund类
ModelDund.java
package com.joy187.re8joymod.entity.model;
import com.joy187.re8joymod.Main;
import com.joy187.re8joymod.entity.EntityDund;
import com.joy187.re8joymod.entity.render.RenderDund;
import net.minecraft.client.model.geom.ModelLayerLocation;
import net.minecraft.resources.ResourceLocation;
import software.bernie.geckolib3.model.AnimatedGeoModel;
//我们的模型类要继承AnimatedGeoModel<T>类
public class ModelDund extends AnimatedGeoModel<EntityDund>{
public static final ModelLayerLocation LAYER_LOCATION = new ModelLayerLocation(new ResourceLocation(Main.MOD_ID, "dund"), "main");
////我们生物模型实体的路径
@Override
public ResourceLocation getModelLocation(EntityDund object) {
return new ResourceLocation(Main.MOD_ID, "geo/dund.geo.json");
}
//我们生物皮肤材质的路径
@Override
public ResourceLocation getTextureLocation(EntityDund object) {
//return RenderDund.LOCATION_BY_VARIANT.get(object.getVariant());
return new ResourceLocation(Main.MOD_ID, "textures/entity/dund.png");
}
//我们生物皮肤动画的路径
@Override
public ResourceLocation getAnimationFileLocation(EntityDund animatable) {
return new ResourceLocation(Main.MOD_ID, "animations/dund.animation.json");
}
}
5.模型部分结束,开始着手渲染类的编写。新建RenderDund类。
RenderDund.java
package com.joy187.re8joymod.entity.render;
import com.joy187.re8joymod.Main;
import com.joy187.re8joymod.entity.EntityDund;
import com.joy187.re8joymod.entity.model.ModelDund;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.blaze3d.vertex.VertexConsumer;
import net.minecraft.client.renderer.MultiBufferSource;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.entity.EntityRendererProvider;
import net.minecraft.client.renderer.entity.EntityRendererProvider.Context;
import net.minecraft.resources.ResourceLocation;
import software.bernie.geckolib3.model.AnimatedGeoModel;
import software.bernie.geckolib3.renderers.geo.GeoEntityRenderer;
//我们的渲染类要继承GeoEntityRenderer<T>
public class RenderDund extends GeoEntityRenderer<EntityDund>{
public RenderDund(EntityRendererProvider.Context renderManager) {
super(renderManager, new ModelDund());
//我们生物的阴影大小
this.shadowRadius = 0.5f;
}
@Override
public ResourceLocation getTextureLocation(EntityDund instance) {
//return LOCATION_BY_VARIANT.get(instance.getVariant());
return new ResourceLocation(Main.MOD_ID, "textures/entity/dund.png");
}
@Override
public RenderType getRenderType(EntityDund animatable, float partialTicks, PoseStack stack,
MultiBufferSource renderTypeBuffer, VertexConsumer vertexBuilder, int packedLightIn,
ResourceLocation textureLocation) {
//如果是幼年动物,就把其按比例缩小
// if(animatable.isBaby()) {
// stack.scale(0.4F, 0.4F, 0.4F);
// } else {
// stack.scale(0.8F, 0.8F, 0.8F);
// }
//默认原来模型大小
stack.scale(1F, 1F, 1F);
return super.getRenderType(animatable, partialTicks, stack, renderTypeBuffer, vertexBuilder, packedLightIn, textureLocation);
}
}
6.在EntityInit.java中
添加我们的生物信息:
public static final RegistryObject<EntityType<EntityDund>> DUND1 = ENTITY_TYPES.register("dund",
() -> EntityType.Builder.of(EntityDund::new, MobCategory.MONSTER).sized(1f,1.8f).setTrackingRange(30)
.build(new ResourceLocation(Main.MOD_ID, "dund").toString()));
在ClientModEventSubscriber.java
中添加我们的模型渲染、属性注册语句:
@Mod.EventBusSubscriber(modid = Main.MOD_ID, value = Dist.CLIENT, bus = Mod.EventBusSubscriber.Bus.MOD)
public class ClientModEventSubscriber
{
@SubscribeEvent
public static void onRegisterRenderer(EntityRenderersEvent.RegisterRenderers event) {
//添加渲染注册语句
event.registerEntityRenderer(EntityInit.DUND1.get(), RenderDund::new);
//event.registerEntityRenderer(EntityInit.DETONATOR_ENTITY.get(), RenderDetonatorEntity::new);
}
@SubscribeEvent
public static void onAttributeCreate(EntityAttributeCreationEvent event) {
//添加属性注册语句
event.put(EntityInit.DUND1.get(), EntityDund.prepareAttributes().build());
}
}
7.生物实体部分结束,接下来我们要给生物制作一个刷怪蛋:
在ItemInit类中添加我们的刷怪蛋物品:
public static final RegistryObject<Item> DUND1_SPAWN_EGG = ITEMS.register("dund_spawn_egg",
() -> new ForgeSpawnEggItem(EntityInit.DUND1, 3093009, 65536, new Item.Properties().tab(Main.TUTORIAL_TAB)));
7.代码部分结束,来到资源包制作环节
在resources\assets\你的modid
中的lang包中的en_us.json
添加刷怪蛋和生物实体英文名称:
"item.re8joymod.dund_spawn_egg": "Mob Spawn Egg",
"entity.re8joymod.dund": "Mob",
在models\item
包中添加刷怪蛋模型文件:
dund_spawn_egg.json
{
"parent": "item/template_spawn_egg"
}
在textures\entity
中添加生物实体的皮肤贴图
新建一个geo包和animation包,把第二步中的模型和动画文件分别放进去
8.保存所有文件 -> 进行测试:
生物待机动画
生物攻击动画
大佬牛逼!!