本篇教程的视频
本篇教程的源代码
GitHub地址:
自定义船类
船在游戏中是属于实体,与告示牌一样,在注册时,他们的注册表是硬编码的
所以这里我们需要写一个自定义的船类
1 2 3 4 5
| public class ModBoatEntity extends Boat { public ModBoatEntity(EntityType<? extends Boat> pEntityType, Level pLevel) { super(pEntityType, pLevel); } }
|
随后我们再写一个构造函数,待会再用
1 2 3 4 5 6 7
| public ModBoatEntity(Level level, double x, double y, double z) { this(, level); this.setPos(x, y, z); this.xo = x; this.yo = y; this.zo = z; }
|
实体数据访问器
接下来我们写一个数据访问器,因为这是实体
1
| private static final EntityDataAccessor<Integer> DATA_ID_TYPE = SynchedEntityData.defineId(Boat.class, EntityDataSerializers.INT);
|
内部类 Type
随后我们写一个内部类,这个是用来定义船的类型的
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
| public enum Type implements StringRepresentable { ICE_ETHER(ModBlocks.ICE_ETHER_PLANKS.get(), "ice_ether");
private final String name; private final Block planks; public static final StringRepresentable.EnumCodec<ModBoatEntity.Type> CODEC = StringRepresentable.fromEnum(ModBoatEntity.Type::values); private static final IntFunction<Type> BY_ID = ByIdMap.continuous(Enum::ordinal, values(), ByIdMap.OutOfBoundsStrategy.ZERO);
private Type(Block pPlanks, String pName) { this.name = pName; this.planks = pPlanks; }
public String getSerializedName() { return this.name; }
public String getName() { return this.name; }
public Block getPlanks() { return this.planks; }
public String toString() { return this.name; }
public static ModBoatEntity.Type byId(int pId) { return BY_ID.apply(pId); }
public static ModBoatEntity.Type byName(String pName) { return CODEC.byName(pName, ICE_ETHER); } }
|
这个内部类原版的Boat类中也有
数据持久化
我们还需要重写一个数据同步和两个数据持久化相关的方法
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
| @Override protected void defineSynchedData() { super.defineSynchedData(); this.entityData.define(DATA_ID_TYPE, Type.ICE_ETHER.ordinal()); }
@Override protected void addAdditionalSaveData(CompoundTag pCompound) { ModBoatEntity.Type type = getModVariant(); pCompound.putString("Type", type.getSerializedName()); }
@Override protected void readAdditionalSaveData(CompoundTag pCompound) { if (pCompound.contains("Type", 8)) { setVariant(ModBoatEntity.Type.byName(pCompound.getString("Type"))); } }
public ModBoatEntity.Type getModVariant() { return ModBoatEntity.Type.byId(this.entityData.get(DATA_ID_TYPE)); }
public void setVariant(ModBoatEntity.Type type) { this.entityData.set(DATA_ID_TYPE, type.ordinal()); }
|
getDropItem
我们还需要重写getDropItem这个方法,让我们的船破坏后有掉落物
1 2 3 4 5 6 7
| @Override public Item getDropItem() { switch (getModVariant()) { } return super.getDropItem(); }
|
当然现在我们尚未注册我们自己的物品,这里就先空着
自定义箱子船类
除了普通的船,还有一个箱子船,我们这里也一并写了
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
| public class ModChestBoatEntity extends ChestBoat { private static final EntityDataAccessor<Integer> DATA_ID_TYPE = SynchedEntityData.defineId(Boat.class, EntityDataSerializers.INT); public ModChestBoatEntity(EntityType<? extends Boat> pEntityType, Level pLevel) { super(pEntityType, pLevel); }
public ModChestBoatEntity(Level level, double x, double y, double z) { this(ModEntities.MOD_CHEST_BOAT.get(), level); this.setPos(x, y, z); this.xo = x; this.yo = y; this.zo = z; }
@Override public Item getDropItem() { switch (getModVariant()) { } return super.getDropItem(); }
@Override protected void defineSynchedData() { super.defineSynchedData(); this.entityData.define(DATA_ID_TYPE, ModBoatEntity.Type.ICE_ETHER.ordinal()); }
@Override protected void addAdditionalSaveData(CompoundTag pCompound) { ModBoatEntity.Type type = getModVariant(); pCompound.putString("Type", type.getSerializedName()); } @Override protected void readAdditionalSaveData(CompoundTag pCompound) { if (pCompound.contains("Type", 8)) { setVariant(ModBoatEntity.Type.byName(pCompound.getString("Type"))); } }
public ModBoatEntity.Type getModVariant() { return ModBoatEntity.Type.byId(this.entityData.get(DATA_ID_TYPE)); }
public void setVariant(ModBoatEntity.Type type) { this.entityData.set(DATA_ID_TYPE, type.ordinal()); } }
|
整体上和船的差不多
注册船
接下来我们先注册这两个船类
1 2 3 4 5 6 7
| public static final RegistryObject<EntityType<ModBoatEntity>> MOD_BOAT = ENTITY_TYPES.register("mod_boat", () -> EntityType.Builder.<ModBoatEntity>of(ModBoatEntity::new, MobCategory.MISC) .sized(1.375f, 0.5625f).build("mod_boat"));
public static final RegistryObject<EntityType<ModChestBoatEntity>> MOD_CHEST_BOAT = ENTITY_TYPES.register("mod_chest_boat", () -> EntityType.Builder.<ModChestBoatEntity>of(ModChestBoatEntity::new, MobCategory.MISC) .sized(1.375f, 0.5625f).build("mod_chest_boat"));
|
记得完善各自类的构造函数
1 2 3 4 5
| this(ModEntities.MOD_BOAT.get(), level);
this(ModEntities.MOD_CHEST_BOAT.get(), level);
|
船的渲染器类
我们需要一个渲染器类,这个类继承自BoatRenderer
1 2 3 4 5 6
| public class ModBoatRenderer extends BoatRenderer {
public ModBoatRenderer(EntityRendererProvider.Context pContext, boolean pChestBoat) { super(pContext, pChestBoat); } }
|
然后我们来创建一个Map
1
| private final Map<ModBoatEntity.Type, Pair<ResourceLocation, ListModel<Boat>>> boatResources;
|
这个是用来保存船的资源,将它的类型和船的资源关联起来
而后需要在构造方法中初始化这个Map
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
| public ModBoatRenderer(EntityRendererProvider.Context pContext, boolean pChestBoat) { super(pContext, pChestBoat); this.boatResources = Stream.of(ModBoatEntity.Type.values()).collect(ImmutableMap.toImmutableMap((type) -> type, (type) -> Pair.of(ResourceLocation.fromNamespaceAndPath(TutorialMod.MOD_ID, getTextureLocation(type, pChestBoat)), this.createBoatModel(pContext, type, pChestBoat)))); }
private static String getTextureLocation(ModBoatEntity.Type pType, boolean pChestBoat) { return pChestBoat ? "textures/entity/chest_boat/" + pType.getName() + ".png" : "textures/entity/boat/" + pType.getName() + ".png"; }
private ListModel<Boat> createBoatModel(EntityRendererProvider.Context pContext, ModBoatEntity.Type pType, boolean pChestBoat) { ModelLayerLocation modellayerlocation = pChestBoat ? createChestBoatModelName(pType) : createBoatModelName(pType); ModelPart modelpart = pContext.bakeLayer(modellayerlocation); return pChestBoat ? new ChestBoatModel(modelpart) : new BoatModel(modelpart); }
public static ModelLayerLocation createBoatModelName(ModBoatEntity.Type pType) { return createLocation("boat/" + pType.getName(), "main"); }
public static ModelLayerLocation createChestBoatModelName(ModBoatEntity.Type pType) { return createLocation("chest_boat/" + pType.getName(), "main"); }
private static ModelLayerLocation createLocation(String pPath, String pModel) { return new ModelLayerLocation(ResourceLocation.fromNamespaceAndPath(TutorialMod.MOD_ID, pPath), pModel); }
|
当然,为了初始化这个Map,我们需要写一堆额外的方法
最后还需要写一个方法
1 2 3 4 5 6 7 8 9
| public Pair<ResourceLocation, ListModel<Boat>> getModelWithLocation(Boat boat) { if(boat instanceof ModBoatEntity modBoat) { return this.boatResources.get(modBoat.getModVariant()); } else if(boat instanceof ModChestBoatEntity modChestBoatEntity) { return this.boatResources.get(modChestBoatEntity.getModVariant()); } else { return null; } }
|
这个方法用于获取船的资源,主要是获取它的贴图
渲染器类注册
我们到主类的ClientModEvents中的registerRenderers方法下注册这个渲染器
1 2
| EntityRenderers.register(ModEntities.MOD_BOAT.get(), pContext -> new ModBoatRenderer(pContext, false)); EntityRenderers.register(ModEntities.MOD_CHEST_BOAT.get(), pContext -> new ModBoatRenderer(pContext, true));
|
自定义船物品
接下来我们还需要写一个船物品
1 2 3 4 5
| public class ModBoatItem extends Item { public ModBoatItem(Properties pProperties) { super(pProperties); } }
|
同样还是因为原版的船物品被硬编码了,所以我们只能自己写一个,来实现放置船的逻辑
这里我们先定义三个字段,并在构造方法中初始化
1 2 3 4 5 6 7 8 9
| private static final Predicate<Entity> ENTITY_PREDICATE = EntitySelector.NO_SPECTATORS.and(Entity::isPickable); private final ModBoatEntity.Type type; private final boolean hasChest;
public ModBoatItem(boolean pHasChest, ModBoatEntity.Type pType, Item.Properties pProperties) { super(pProperties); this.hasChest = pHasChest; this.type = pType; }
|
ENTITY_PREDICATE是一个Predicate,用于判断是否可以放置船
type是船的类型
hasChest是船是否带箱子
然后是重写use方法
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
| @Override public InteractionResultHolder<ItemStack> use(Level pLevel, Player pPlayer, InteractionHand pUsedHand) { ItemStack itemstack = pPlayer.getItemInHand(pUsedHand); HitResult hitresult = getPlayerPOVHitResult(pLevel, pPlayer, ClipContext.Fluid.ANY); if (hitresult.getType() == HitResult.Type.MISS) { return InteractionResultHolder.pass(itemstack); } else { Vec3 vec3 = pPlayer.getViewVector(1.0F); List<Entity> list = pLevel.getEntities(pPlayer, pPlayer.getBoundingBox().expandTowards(vec3.scale(5.0D)).inflate(1.0D), ENTITY_PREDICATE); if (!list.isEmpty()) { Vec3 vec31 = pPlayer.getEyePosition();
for (Entity entity : list) { AABB aabb = entity.getBoundingBox().inflate((double) entity.getPickRadius()); if (aabb.contains(vec31)) { return InteractionResultHolder.pass(itemstack); } } }
if (hitresult.getType() == HitResult.Type.BLOCK) { Boat boat = this.getBoat(pLevel, hitresult); if (boat instanceof ModChestBoatEntity chestBoat) { chestBoat.setVariant(this.type); } else if (boat instanceof ModBoatEntity) { ((ModBoatEntity) boat).setVariant(this.type); } boat.setYRot(pPlayer.getYRot()); if (!pLevel.noCollision(boat, boat.getBoundingBox())) { return InteractionResultHolder.fail(itemstack); } else { if (!pLevel.isClientSide) { pLevel.addFreshEntity(boat); pLevel.gameEvent(pPlayer, GameEvent.ENTITY_PLACE, hitresult.getLocation()); if (!pPlayer.getAbilities().instabuild) { itemstack.shrink(1); } }
pPlayer.awardStat(Stats.ITEM_USED.get(this)); return InteractionResultHolder.sidedSuccess(itemstack, pLevel.isClientSide()); } } else { return InteractionResultHolder.pass(itemstack); } } }
private Boat getBoat(Level level, HitResult hitResult) { return (this.hasChest ? new ModChestBoatEntity(level, hitResult.getLocation().x, hitResult.getLocation().y, hitResult.getLocation().z) : new ModBoatEntity(level, hitResult.getLocation().x, hitResult.getLocation().y, hitResult.getLocation().z)); }
|
没有什么特别的逻辑,照抄原版的方法
注册船物品
1 2 3 4 5
| public static final RegistryObject<Item> ICE_ETHER_BOAT = ITEMS.register("ice_ether_boat", () -> new ModBoatItem(false, ModBoatEntity.Type.ICE_ETHER, new Item.Properties())); public static final RegistryObject<Item> ICE_ETHER_CHEST_BOAT = ITEMS.register("ice_ether_chest_boat", () -> new ModBoatItem(true, ModBoatEntity.Type.ICE_ETHER, new Item.Properties()));
|
这两个一个是普通的船,一个是带箱子的船
不要忘记添加到物品栏
1 2 3 4
| public static final Item ICE_ETHER_BOAT = TerraformBoatItemHelper.registerBoatItem( ModBoats.ICE_ETHER_BOAT, ModBoats.ICE_ETHER_BOAT_KEY, false); public static final Item ICE_ETHER_CHEST_BOAT = TerraformBoatItemHelper.registerBoatItem( ModBoats.ICE_ETHER_CHEST_BOAT, ModBoats.ICE_ETHER_BOAT_KEY, true);
|
这里我们使用TerraformBoatItemHelper.registerBoatItem方法来注册我们的船对应的物品
传入的参数有三个,分别是我们的船的Identifier,我们的船的注册键,以及是否是带箱子的船
然后不要忘记将船加入到物品栏中
1 2
| pOutput.accept(ModItems.ICE_ETHER_BOAT.get()); pOutput.accept(ModItems.ICE_ETHER_CHEST_BOAT.get());
|
模型层设置
接下来我们新建一个ModModelLayers类,来设置我们的船的模型层
1 2 3 4 5 6
| public class ModModelLayers { public static final ModelLayerLocation ICE_ETHER_BOAT_LAYER = new ModelLayerLocation( ResourceLocation.fromNamespaceAndPath(TutorialMod.MOD_ID, "boat/ice_ether"), "main"); public static final ModelLayerLocation ICE_ETHER_CHEST_BOAT_LAYER = new ModelLayerLocation( ResourceLocation.fromNamespaceAndPath(TutorialMod.MOD_ID, "chest_boat/ice_ether"), "main"); }
|
随后在模组主类的ClientModEvents类下添加以下内容
1 2 3 4 5
| @SubscribeEvent public static void registerLayers(EntityRenderersEvent.RegisterLayerDefinitions event) { event.registerLayerDefinition(ModModelLayers.ICE_ETHER_BOAT_LAYER, BoatModel::createBodyModel); event.registerLayerDefinition(ModModelLayers.ICE_ETHER_CHEST_BOAT_LAYER, ChestBoatModel::createBodyModel); }
|
数据文件
语言文件
继续使用数据生成来写各个数据文件
1 2
| add(ModItems.ICE_ETHER_BOAT.get(), "Ice Ether Boat"); add(ModItems.ICE_ETHER_CHEST_BOAT.get(), "Ice Ether Chest Boat");
|
模型文件
1 2
| basicItem(ModItems.ICE_ETHER_BOAT.get()); basicItem(ModItems.ICE_ETHER_CHEST_BOAT.get());
|
模型文件只要写物品对应的模型即可
贴图文件
注意,贴图文件分两种,一个是物品贴图,一个是实体的展开图
物品贴图就和常规的物品一样放就可以
展开图也分两个,一个是普通的船,要放在texures/entity/boat/下,贴图名字与注册键的名字一致,也就是上面我们写的ICE_ETHER_BOAT_KEY,对应ice_ether_boat
另一个是带箱子的船,要放在texures/entity/chest_boat/下,贴图名字同上
测试
跑好数据生成之后,我们就可以进入游戏进行测试了