本篇教程的视频

本篇教程的源代码

GitHub地址:TutorialMod-Villagers-1.20.1

本篇教程目标

  • 理解原版村民的注册
  • 学会为模组添加自定义村民

查看源代码

那么这里我们也还是来看看源代码

村民职业注册

原版的村民是在VillagerProfession中注册的

1
2
3
4
5
6
7
8
9
10
public record VillagerProfession(
String id,
Predicate<RegistryEntry<PointOfInterestType>> heldWorkstation,
Predicate<RegistryEntry<PointOfInterestType>> acquirableWorkstation,
ImmutableSet<Item> gatherableItems,
ImmutableSet<Block> secondaryJobSites,
@Nullable SoundEvent workSound
) {
...
}

它是一个record类,我们再来看看里面的东西

1
2
3
4
5
6
7
8
9
10
public static final VillagerProfession ARMORER = register("armorer", PointOfInterestTypes.ARMORER, SoundEvents.ENTITY_VILLAGER_WORK_ARMORER);
...
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
);
...

可以看到,register是它的注册方法,不过也一样的,也是有好几个重载方法的

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,
new Identifier(id),
new VillagerProfession(id, heldWorkstation, acquirableWorkstation, gatherableItems, secondaryJobSites, workSound)
);
}

不过最终调用的,其实就是最后一个方法中的Registry.register,这个和我们之前注册方块或者物品类似

接下来我们看看其中的参数

id,注册的id,也就是村民的名称

heldWorkstation,村民的工作站,比如铁匠对应的是铁砧图书管理员对应讲台等等

acquirableWorkstation,村民可以获取的工作站,一般与上面的那个一样

gatherableItems,村民可以收集的物品,比如农民可以收集小麦、小麦种子等等

secondaryJobSites,村民的次要工作地点,比如农民可以在耕地上进行工作,除了农民其他职业的也没有第二个工作站点了

workSound,村民工作的声音

村民工作站注册

那么接下来我们看看村民的工作站是怎么注册的,不难发现,它与PointOfInterestTypes这个类有关

这个工作站的话,也可以称为兴趣点,因为这个不是村民独有的

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

这是它其中的一个注册语句和注册方法,这个是注册键

那么问题来了,对应的方块呢?接着往下翻,翻到一个registerAndGetDefault的方法

1
2
3
4
5
public static PointOfInterestType registerAndGetDefault(Registry<PointOfInterestType> registry) {
...
register(registry, FARMER, getStatesOfBlock(Blocks.COMPOSTER), 1, 1);
...
}

那么在这里我们就能够发现注册键对应的方块了

这里的register方法是下面这个

1
2
3
4
5
6
7
8
private static PointOfInterestType register(
Registry<PointOfInterestType> registry, RegistryKey<PointOfInterestType> key, Set<BlockState> states, int ticketCount, int searchDistance
) {
PointOfInterestType pointOfInterestType = new PointOfInterestType(states, ticketCount, searchDistance);
Registry.register(registry, key, pointOfInterestType);
registerStates(registry.entryOf(key), states);
return pointOfInterestType;
}

不过呢,这个方法是private的,我们自己写的时候得另寻他法

接下来我们看看它的参数

registry,注册表

key,注册键,也就是上面注册的那个注册键

states,注册键对应的方块,比如这里的农民对应的就是堆肥箱

ticketCount,这个工作站能够容纳的村民数量,一般而言,每一个工作站只能容纳一个村民

searchDistance,村民搜索这个工作站的距离,比如这里的农民就是1,这个单位估计是一个区块

不过,我们也可以看到其他的那些工作站或者兴趣点,比如蜂巢等等,而像蜂巢这种并不是给人使用的兴趣点,
它的ticketCount0,毕竟它是给蜜蜂用的

好了,村民注册相关的内容就这一些,村民的交易内容也是按照我们之前的教程来写就好了

注册村民

注册工作站注册键

这里我们先创建一个ModPointOfInterestTypes类,用于注册这些兴趣点的注册键

1
2
3
public class ModPointOfInterestTypes {
...
}

然后我们搬一下原版的注册方法,改一下命名空间

1
2
3
private static RegistryKey<PointOfInterestType> of(String id) {
return RegistryKey.of(RegistryKeys.POINT_OF_INTEREST_TYPE, new Identifier(TutorialMod.MOD_ID, id));
}

最后就可以注册兴趣点的注册键了

1
public static final RegistryKey<PointOfInterestType> ICE_ETHER_KEY = of("ice_ether_poi");

为了加以区分,这里加上了poi,指代这个兴趣点

注册村民职业

接下来我们创建一个ModVillagers类,用于注册村民职业

1
2
3
public class ModVillagers {
...
}

随后我们按照原版的注册方法来定义我们自己的注册方法

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

这里的两个ImmutableSet.of(),指的就是村民可以收集的物品和第二个工作站点,因为这里我们不需要,所以就让它直接空着好了

不要忘了将命名空间改成我们模组的

随后我们就可以注册村民职业了

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

第二个参数的兴趣点注册键就是上面注册好的

村民工作的声音我们直接用原版的就好了

那么还有一个,就是兴趣点注册键对应的方块我们还没写,由于原版是private的方法,
所以这里我们也还是使用Fabric提供的API来实现

先写一个注册方法

1
2
3
public static PointOfInterestType registerPoints(String name, Block block) {
return PointOfInterestHelper.register(new Identifier(TutorialMod.MOD_ID, name), 1, 1, block);
}

这里我们使用了PointOfInterestHelper.register方法,这个方法就是用来注册兴趣点注册键对应的方块的

第一个参数是注册键名称,第二个参数是村民数量,第三个参数是搜索距离,第四个参数就是方块

村民数量和搜索距离就与原版的一致,直接设置为1

而后,我们就可以来写对应的方块了

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

注意这里的注册键名称,它要和我们注册的注册键名字一致,不然对应不上

最后,还有一个用于初始化的方法

1
2
3
public static void registerModVillagers() {

}

同样不要忘记在主类调用

1
ModVillagers.registerModVillagers();

注册村民交易

那么接下来就是村民交易

写法和前面一篇一样,这里就不再赘述了

1
2
3
4
5
6
7
8
TradeOfferHelper.registerVillagerOffers(ModVillagers.ICE_ETHER_MASTER, 1, factories -> {
factories.add(new TradeOffers.BuyForOneEmeraldFactory(ModItems.ICE_ETHER, 2, 12, 5));
factories.add(new TradeOffers.SellItemFactory(ModItems.FIRE_ETHER_AXE.getDefaultStack(), 1, 1, 5, 2, 0.5f));
});
TradeOfferHelper.registerVillagerOffers(ModVillagers.ICE_ETHER_MASTER, 2, factories -> {
factories.add(new TradeOffers.BuyForOneEmeraldFactory(ModItems.FIRE_ETHER, 2, 12, 5));
factories.add(new TradeOffers.SellItemFactory(ModBlocks.ICE_ETHER_BLOCK.asItem().getDefaultStack(), 1, 12, 5, 2, 0.5f));
});

同样也是用TradeOfferHelper来新增村民交易

数据生成

语言文件

这个语言文件写的是在交易的GUI上,显示的村民职业的名字

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

注意它的翻译键是entity.minecraft.villager. + 村民职业的id

兴趣点标签

兴趣点或者说工作站,它也是有标签的,我们得写一下

但是兴趣点标签与以往的物品或者方块的标签不一样,它们是另一种标签,所以这里我们要新建一个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) {

}
}

然后我们就得在configure里面写标签了

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

不过,万变不离其宗的还是使用getOrCreateTagBuilder

这里使用addOptional加入我们的命名空间和兴趣点的注册键名称,名字不要搞错

最后也不要忘了在数据生成类中调用这个类

1
pack.addProvider(ModPointTagProvider::new);

贴图

这个贴图也是展开图,要放在textures/entity/villager/profession目录下

文件名是注册的村民职业名称id,不要写错了

测试

那么最后我们就可以进入游戏进行测试了