本篇教程的视频

本篇教程的源代码

Github地址:TutorialMod-Villager-1.21

介绍

前面我们已经学习了如何自定义交易,那么假设你还不满足,希望有更多职业的村民,那么就继续这里的教程

在这篇教程中,我们将学习如何自定义村民,包括新增村民职业、交易等

村民的职业是通过工作站点来定义的,一开始生成他们的时候并没有职业,也不能交易

但如果有相关的工作站点,那么村民就会根据工作站点来转换职业(除了绿袍),更换其自身的贴图,同时也与玩家可以进行交易

查看源代码

首先村民要定义一个职业,再根据其职业内容来定义交易

所以我们先来查看VillagerProfession这个类,这个类是定义了原版的村民职业

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static final VillagerProfession NONE = register("none", PointOfInterestType.NONE, IS_ACQUIRABLE_JOB_SITE, null);
public static final VillagerProfession ARMORER = register("armorer", PointOfInterestTypes.ARMORER, SoundEvents.ENTITY_VILLAGER_WORK_ARMORER);
public static final VillagerProfession BUTCHER = register("butcher", PointOfInterestTypes.BUTCHER, SoundEvents.ENTITY_VILLAGER_WORK_BUTCHER);
public static final VillagerProfession CARTOGRAPHER = register(
"cartographer", PointOfInterestTypes.CARTOGRAPHER, SoundEvents.ENTITY_VILLAGER_WORK_CARTOGRAPHER
);
public static final VillagerProfession CLERIC = register("cleric", PointOfInterestTypes.CLERIC, SoundEvents.ENTITY_VILLAGER_WORK_CLERIC);
public static final VillagerProfession FARMER = register(
"farmer",
PointOfInterestTypes.FARMER,
ImmutableSet.of(Items.WHEAT, Items.WHEAT_SEEDS, Items.BEETROOT_SEEDS, Items.BONE_MEAL),
ImmutableSet.of(Blocks.FARMLAND),
SoundEvents.ENTITY_VILLAGER_WORK_FARMER
);
...

我们可以看到这些注册语句,当然不同的职业注册的方法也不尽相同,不过其基本的参数是注册名工作站点声音

不过我们可以看到农民的注册语句多了两个参数,这两个参数分别是可收集物品辅助工作站点,因为农民得种地,同时又会收集成熟的农作物

其他职业的注册也是类似的

这里我们方便起见,就以盔甲商 ARMORER为例,我们来看看它的注册方法

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
private static VillagerProfession register(String id, RegistryKey<PointOfInterestType> heldWorkstation, @Nullable SoundEvent workSound) {
return register(id, entry -> entry.matchesKey(heldWorkstation), entry -> entry.matchesKey(heldWorkstation), workSound);
}

private static VillagerProfession register(
String id,
Predicate<RegistryEntry<PointOfInterestType>> heldWorkstation,
Predicate<RegistryEntry<PointOfInterestType>> acquirableWorkstation,
@Nullable SoundEvent workSound
) {
return register(id, heldWorkstation, acquirableWorkstation, ImmutableSet.of(), ImmutableSet.of(), workSound);
}

private static VillagerProfession register(
String id,
RegistryKey<PointOfInterestType> heldWorkstation,
ImmutableSet<Item> gatherableItems,
ImmutableSet<Block> secondaryJobSites,
@Nullable SoundEvent workSound
) {
return register(id, entry -> entry.matchesKey(heldWorkstation), entry -> entry.matchesKey(heldWorkstation), gatherableItems, secondaryJobSites, workSound);
}

private static VillagerProfession register(
String id,
Predicate<RegistryEntry<PointOfInterestType>> heldWorkstation,
Predicate<RegistryEntry<PointOfInterestType>> acquirableWorkstation,
ImmutableSet<Item> gatherableItems,
ImmutableSet<Block> secondaryJobSites,
@Nullable SoundEvent workSound
) {
return Registry.register(
Registries.VILLAGER_PROFESSION,
Identifier.ofVanilla(id),
new VillagerProfession(id, heldWorkstation, acquirableWorkstation, gatherableItems, secondaryJobSites, workSound)
);
}

当然,它的注册方法也是和Items的一样,层层叠叠的,最后的那个才是我们要用的,上面这一堆都可以进行整合

接下来我们再看看盔甲商的工作站点PointOfInterestTypes.ARMORER

这个是在PointOfInterestTypes这个类中定义的

1
2
3
4
5
6
public static final RegistryKey<PointOfInterestType> ARMORER = of("armorer");
...
private static RegistryKey<PointOfInterestType> of(String id) {
return RegistryKey.of(RegistryKeys.POINT_OF_INTEREST_TYPE, Identifier.ofVanilla(id));
}
...

这个是盔甲商的工作站点注册的语句及其注册方法

但是你是否发现了一个问题,它对应的方块呢?盔甲商对应的工作站应该是高炉

然鹅,上面的两个语句都没有提到任何的方块,这是为什么呢?

我们再往下翻,我们可以看到下面的一个语句

1
2
3
4
5
6
7
8
9
10
public static PointOfInterestType registerAndGetDefault(Registry<PointOfInterestType> registry) {
register(registry, ARMORER, getStatesOfBlock(Blocks.BLAST_FURNACE), 1, 1);
...
}

private static PointOfInterestType register(
Registry<PointOfInterestType> registry, RegistryKey<PointOfInterestType> key, Set<BlockState> states, int ticketCount, int searchDistance
) {
...
}

registerAndGetDefault这个方法中,我们可以看到ARMORER对应的工作站点是BLAST_FURNACE(高炉),工作站点的具体方块是在这里定义的

后面的两个数字分别是容纳数量搜索距离(单位当然不是1格方块,具体得挖底层)

容纳数量基本上是1,即一个工作站只能对应一个村民

我们也可以在这个方法中看到其他的一些工作站(也可以叫兴趣点)

1
register(registry, MEETING, getStatesOfBlock(Blocks.BELL), 32, 6);

这是村民集会的地点,也就是村庄里的钟,它最高可以容纳32人

1
register(registry, BEEHIVE, getStatesOfBlock(Blocks.BEEHIVE), 0, 1);

当然也有其他生物的,比如蜂巢,不过他们的容纳数量为0,因为这不是给人使的,给蜜蜂的

不过,最要紧的是它的注册方法是私有的,我们无法直接调用,但是Fabric提供了API,我们可以直接使用

注册职业

创建ModVillagers类

1
2
3
public class ModVillagers {

}

首先我们创建一个ModVillagers类,这个类用来存放我们自定义的职业

随后我们将上面的注册方法进行整合,结合我们自己的命名空间来

1
2
3
4
5
private static VillagerProfession register(String id, RegistryKey<PointOfInterestType> heldWorkstation, @Nullable SoundEvent workSound) {
return Registry.register(Registries.VILLAGER_PROFESSION, Identifier.of(TutorialMod.MOD_ID, id),
new VillagerProfession(id, entry -> entry.matchesKey(heldWorkstation), entry -> entry.matchesKey(heldWorkstation),
ImmutableSet.of(), ImmutableSet.of(), workSound));
}

当然,这里是默认让村民不会收集物品,也不会有辅助工作站点,如果你想让村民有这些功能,可以自行修改

那么还有一个是工作站点对应方块的注册

1
2
3
private static PointOfInterestType registerPointOfInterestType(String id, Block block) {
return PointOfInterestHelper.register(Identifier.of(TutorialMod.MOD_ID, id), 1, 1, block);
}

这里的PointOfInterestHelperFabric提供的API,我们直接使用它来注册我们的站点对应的方块

不过它的结构和原版的还是有区别的,这里少了RegistryKey<PointOfInterestType>这个类型的参数

创建ModPointOfInterestTypes类

最后在注册村民职业之前,我们首先还需要注册工作站点

1
2
3
4
5
6
public class ModPointOfInterestTypes {
public static final RegistryKey<PointOfInterestType> ICE_ETHER_KEY = of("ice_ether_poi");
private static RegistryKey<PointOfInterestType> of(String id) {
return RegistryKey.of(RegistryKeys.POINT_OF_INTEREST_TYPE, Identifier.of(TutorialMod.MOD_ID, id));
}
}

这里我们创建一个ModPointOfInterestTypes类,这个类用来存放我们自定义的工作站点

这里的注册方法也是和原版一样的,不过记得要改命名空间

而后我们注册一个工作站点ICE_ETHER_KEY,另外对应的方块我们放到ModVillagers类中注册

最后,也不要忘了初始化的方法

1
2
3
public static void registerModVillagers() {

}

并在模组主类中调用这个方法

1
ModVillagers.registerModVillagers();

注册职业和工作站点对应的方块

接下来我们利用前面的两个方法来注册相关内容

1
2
3
4
public static final VillagerProfession ICE_ETHER_MASTER = register("ice_ether_master",
ModPointOfInterestTypes.ICE_ETHER_KEY, SoundEvents.ENTITY_VILLAGER_WORK_ARMORER);

public static final PointOfInterestType ICE_ETHER_POI = registerPointOfInterestType("ice_ether_poi", ModBlocks.ICE_ETHER_BLOCK);

这里我们注册了一个ICE_ETHER_MASTER职业和ICE_ETHER_POI工作站点

其中ICE_ETHER_POI对应的方块是ICE_ETHER_BLOCK

值得一提的是,这里的ICE_ETHER_POI和前面ModPointOfInterestTypes类中的ICE_ETHER_KEY注册名一定一定是要一致的,不然无法注册成功(源代码中是直接调用的,也就没有这个问题)

编写交易内容

这里我们新增了一个村民的职业,但是他还没有能够交易的内容,我们还得定义它

1
2
3
4
5
6
7
8
TradeOfferHelper.registerVillagerOffers(ModVillagers.ICE_ETHER_MASTER, 1, factories -> {
factories.add(new TradeOffers.SellItemFactory(ModItems.ICE_ETHER, 2, 9, 12, 2, 0.5f));
factories.add(new TradeOffers.BuyItemFactory(ModItems.RAW_ICE_ETHER, 2, 9, 12, 2));
});
TradeOfferHelper.registerVillagerOffers(ModVillagers.ICE_ETHER_MASTER, 2, factories -> {
factories.add(new TradeOffers.SellItemFactory(ModBlocks.ICE_ETHER_BLOCK.asItem(), 4, 16, 12, 4, 0.5f));
factories.add(new TradeOffers.BuyItemFactory(ModBlocks.RAW_ICE_ETHER_BLOCK.asItem(), 4, 16, 12, 4));
});

方法是和前一篇教程一样的,这里我们先写两个等级的,简单按照你自己的想法写一下即可

数据文件

语言文件

1
translationBuilder.add("entity.minecraft.villager.ice_ether_master", "Ice Ether Master");

这里我们写一下村民的语言文件,这个是显示在交易的GUI上的村民的名字

PointTag

我们假设其他文件都搞定了,但是我们进入游戏生成一个村民,即便把ICE_ETHER_BLOCK放他边上,他也没有转换职业

因为我们还得写一个PointOfInterestTypeTag

这个东西既不是物品的标签ItemTag,也不是方块的标签BlockTag,而是独属于工作站(兴趣点)的标签,Minecraft其实还有各种各样的标签,可以好好研究研究

在图文教程的后面,我们还将补充画的相关内容,这个时候也有一个标签得写

那废话不多说,这里来写这个标签的数据生成类

我们创建ModPointTagProvider,继承TagProvider<PointOfInterestType>

1
2
3
4
5
6
7
8
9
10
public class ModPointTagProvider extends TagProvider<PointOfInterestType> {
public ModPointTagProvider(DataOutput output, CompletableFuture<RegistryWrapper.WrapperLookup> registryLookupFuture) {
super(output, RegistryKeys.POINT_OF_INTEREST_TYPE, registryLookupFuture);
}

@Override
protected void configure(RegistryWrapper.WrapperLookup lookup) {

}
}

这里的super函数直接改写好了,改成RegistryKeys.POINT_OF_INTEREST_TYPE,因为我们这里要生成的标签就是它这个类型的

而后我们就可以在configure里写我们的Tag了

1
2
getOrCreateTagBuilder(PointOfInterestTypeTags.ACQUIRABLE_JOB_SITE)
.addOptional(Identifier.of(TutorialMod.MOD_ID, "ice_ether_poi"));

这里我们要加的不是方块,也不是物品,而是Identifier这个类型的参数

这个名字就是我们前面注册用的名字,一定一定要一致,否则还是无效的

材质文件

村民属于实体,不过他们的职业对应的材质是他们的第二层贴图,我们将对应的材质放在textures/entity/villager/profession下,不要搞错位置

我们去看原版的那些材质的时候,其实也可以发现,在profession下的贴图并不包括整个村民的贴图

那么在此之后,我们就可以进入游戏去测试了