本篇教程的视频

本篇教程的源代码

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
// ModBoatEntity
this(ModEntities.MOD_BOAT.get(), level);

// ModChestBoatEntity
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/下,贴图名字同上

测试

跑好数据生成之后,我们就可以进入游戏进行测试了