本篇教程的视频

本篇教程的源代码

GitHub地址:

本篇教程目标

  • 理清唱片的三层结构:声音事件、歌曲、物品
  • 学会注册能被歌曲引用的声音事件
  • 学会用数据生成写jukebox_song
  • 学会注册唱片物品,以及它的模型和贴图

介绍

上一篇我们讲了声音事件,这一篇讲音乐唱片

从1.21开始,唱片就不是「一个物品加一个音效」这么简单了,它分成了三层

第一层是声音事件,也就是SoundEvent,它决定放什么声音

第二层是歌曲,也就是JukeboxSong,它记录这首歌多长、比较器输出多少红石信号、播放的时候显示什么名字

第三层是物品,也就是唱片本身,它通过jukeboxPlayable指向第二层

三层里,只有第一层和第三层是我们熟悉的代码注册,第二层在数据包里

所以在 1.21.1 里,我们注册完物品之后,还得自己手写一个data/<命名空间>/jukebox_song/xxx.json,不然唱片能放进唱片机、也有字幕,但就是没声音

到了 26.1,这一层也能用数据生成写了,不用手写

和 1.21.1 的差别,主要有这几处

第一,注册歌曲用的参数从Registerable<JukeboxSong>变成了BootstrapContext<JukeboxSong>,和上一期职业、交易那套一样

第二,RegistryEntry.Reference<SoundEvent>现在叫Holder.Reference<SoundEvent>,换个名字而已

第三,JukeboxSong里存的声音也从RegistryEntry变成了Holder

第四,jukebox_song的json我们不用手写了,交给FabricDynamicRegistryProvider

第五,物品模型的写法变了,TEMPLATE_MUSIC_DISC现在是ModelTemplates.MUSIC_DISC,而且模型文件拆成了两段

查看源代码

JukeboxSong

先看歌曲这个类,它是个record

1
2
3
4
5
public record JukeboxSong(
Holder<SoundEvent> soundEvent,
Component description,
float lengthInSeconds,
int comparatorOutput)

四个字段,正好对应唱片机上需要的信息

soundEvent是放哪个声音事件,注意类型是Holder<SoundEvent>,不是SoundEvent本身

description是播放时飘出来的那行描述文本,也就是「正在播放:xxx」

lengthInSeconds是歌曲时长,单位是秒

comparatorOutput是比较器输出的红石信号强度,0到15

时长这里有个细节,lengthInTicks方法会把它换算成tick,换算方法是秒数 × 20向上取整,然后再多留 1 秒的收尾

JukeboxSongs

原版的歌曲都在JukeboxSongs这个接口里

1
ResourceKey<JukeboxSong> THIRTEEN = of("13");

再往下来到bootstrap里的注册

1
2
3
public static void bootstrap(BootstrapContext<JukeboxSong> context) {
register(context, THIRTEEN, SoundEvents.MUSIC_DISC_13, 178, 1);
}

后面的两个数字是时长和比较器信号

再看它内部的注册方法

1
2
3
4
5
6
7
8
9
10
11
12
private static void register(
final BootstrapContext<JukeboxSong> context,
final ResourceKey<JukeboxSong> registryKey,
final Holder.Reference<SoundEvent> soundEvent,
final int lengthInSeconds,
final int comparatorOutput
) {
context.register(
registryKey,
new JukeboxSong(soundEvent, Component.translatable(Util.makeDescriptionId("jukebox_song", registryKey.identifier())), lengthInSeconds, comparatorOutput)
);
}

Util.makeDescriptionId("jukebox_song", id)会拼出一个jukebox_song.<命名空间>.<路径>的翻译键

我们照抄这个方法就行,键名自动按我们自己的命名空间来

物品那边

最后是唱片物品,原版是这么写的

1
2
3
public static final Item MUSIC_DISC_13 = registerItem(
"music_disc_13", new Item.Properties().stacksTo(1).rarity(Rarity.UNCOMMON).jukeboxPlayable(JukeboxSongs.THIRTEEN)
);

三个属性

stacksTo(1)是最大堆叠 1,唱片都是一张一张的

rarity(Rarity.RARE)是稀有度,影响名字颜色,RARE是蓝色

jukeboxPlayable(...)就是告诉游戏「这张唱片对应哪首歌」,参数是歌曲的钥匙

动手写

注册声音事件

唱片的音效和普通音效不太一样,它得是Holder.Reference<SoundEvent>

因为JukeboxSong里存的就是Holder,你直接传SoundEvent是编译不过的

我们在ModSoundEvents里加一个方法

1
2
3
4
private static Holder.Reference<SoundEvent> registerForHolder(String name) {
Identifier id = Identifier.fromNamespaceAndPath(TutorialMod.MOD_ID, name);
return Registry.registerForHolder(BuiltInRegistries.SOUND_EVENT, id, SoundEvent.createVariableRangeEvent(id));
}

和上一篇那个register几乎一样,只是把Registry.register换成了Registry.registerForHolder

registerForHolder返回的Holder.Reference就是这个条目在注册表里的引用,后面歌曲就能存它

然后注册我们这首歌的声音事件

1
public static final Holder.Reference<SoundEvent> MUSIC_DISC_A_MOMENT_APART = registerForHolder("music_disc.a_moment_apart");

这个字符串后面sounds.json里还要用一次,得对上

创建 ModJukeboxSongs

新建一个接口,和原版一样

1
2
3
4
5
6
7
public interface ModJukeboxSongs {
ResourceKey<JukeboxSong> A_MOMENT_APART = of("a_moment_apart");

private static ResourceKey<JukeboxSong> of(String id) {
return ResourceKey.create(Registries.JUKEBOX_SONG, Identifier.fromNamespaceAndPath(TutorialMod.MOD_ID, id));
}
}

of里的字符串就是歌曲的id,一会儿生成出来的json文件名也是它

接着把原版那个register搬过来

1
2
3
4
5
6
7
8
9
10
private static void register(
BootstrapContext<JukeboxSong> context, ResourceKey<JukeboxSong> key,
Holder.Reference<SoundEvent> soundEvent, int lengthInSeconds, int comparatorOutput
) {
context.register(key, new JukeboxSong(
soundEvent,
Component.translatable(Util.makeDescriptionId("jukebox_song", key.identifier())),
lengthInSeconds, comparatorOutput
));
}

Util在net.minecraft.util包下,别导错了

最后写bootstrap

1
2
3
static void bootstrap(BootstrapContext<JukeboxSong> context) {
register(context, A_MOMENT_APART, ModSoundEvents.MUSIC_DISC_A_MOMENT_APART, 235, 15);
}

三个数字里,235是时长、15是比较器信号

比较器信号我给的是满格 15,你想让不同的唱片输出电压不一样,可以自己分配

注册唱片物品

1
2
3
public static final Item A_MOMENT_APART_MUSIC_DISC = registerItem("a_moment_apart_music_disc",
Item::new, new Item.Properties().stacksTo(1).rarity(Rarity.RARE)
.jukeboxPlayable(ModJukeboxSongs.A_MOMENT_APART));

jukeboxPlayable传的是歌曲的钥匙,这样游戏才知道这张唱片该放哪首歌

别忘了加进物品栏

1
output.accept(ModItems.A_MOMENT_APART_MUSIC_DISC);

数据生成

注册歌曲

歌曲是数据包注册表,所以要在buildRegistry里挂上

1
2
3
4
5
@Override
public void buildRegistry(RegistrySetBuilder registryBuilder) {
...
registryBuilder.add(Registries.JUKEBOX_SONG, ModJukeboxSongs::bootstrap);
}

这一行不加,数据生成不会报错,但data/tutorial/jukebox_song/下面是空的

输出歌曲文件

然后在动态注册表生成器里多写一行

1
2
3
4
5
@Override
protected void configure(HolderLookup.Provider registries, Entries entries) {
...
entries.addAll(registries.lookupOrThrow(Registries.JUKEBOX_SONG));
}

模型

唱片的模型有个专门的模板

1
itemModelGenerators.generateFlatItem(ModItems.A_MOMENT_APART_MUSIC_DISC, ModelTemplates.MUSIC_DISC);

1.21.1 那时候叫TEMPLATE_MUSIC_DISC,现在叫ModelTemplates.MUSIC_DISC

数据文件

生成的歌曲文件

跑完数据生成,src/main/generated/data/tutorial/jukebox_song/下面会多出一个a_moment_apart.json

1
2
3
4
5
6
7
8
{
"comparator_output": 15,
"description": {
"translate": "jukebox_song.tutorial.a_moment_apart"
},
"length_in_seconds": 235.0,
"sound_event": "tutorial:music_disc.a_moment_apart"
}

四个字段和我们bootstrap里写的一一对应

sound_event就是我们前面注册的那个声音事件,注意是带点号的那个名字

sounds.json

歌曲指向声音事件,声音事件还得指向音频文件

在上一篇的sounds.json里加一段

1
2
3
4
5
6
7
{
"music_disc.a_moment_apart": {
"sounds": [
"tutorial:a_moment_apart"
]
}
}

和普通音效不同的是,这里不写subtitle

因为唱片播放的时候不显示字幕,显示的是歌曲的description,也就是「正在播放:xxx」

语言文件

两条翻译,一条是物品名,一条是歌曲名

1
2
translationBuilder.add(ModItems.A_MOMENT_APART_MUSIC_DISC, "A Moment Apart");
translationBuilder.add("jukebox_song.tutorial.a_moment_apart", "A Moment Apart");

中文那份也一样

第二条的键必须和生成的json里description.translate一致,不然放唱片的时候会显示成一串键名

贴图

最后是贴图,放在

1
assets/tutorial/textures/item/a_moment_apart_music_disc.png

测试

跑完数据生成后,我们就可以启动游戏进行测试了

常见问题(大肥鱼总结版)

  • 唱片能放进唱片机但没声音:sounds.json里少了music_disc.xxx那一条,或者音频文件没放在sounds/下
  • 显示「正在播放:jukebox_song.tutorial.xxx」:语言文件少了jukebox_song.tutorial.xxx这条翻译
  • 物品名显示成item.tutorial.xxx:物品名没加翻译
  • 唱片机不认这张唱片:jukeboxPlayable的钥匙和bootstrap里注册的钥匙不是同一个
  • 唱片机里放不进去:物品少了jukeboxPlayable,或者jukebox_song的数据没生成出来
  • 唱到一半就停:length_in_seconds写得比音频短,改大一点
  • 放完了还在「播放」:那是正常的,游戏会多留 1 秒的收尾时间
  • 比较器没信号:comparator_output写的 0
  • 唱片模型是紫黑方块:贴图路径不对,或者items/下的模型定义没生成
  • 数据生成没有jukebox_song目录:buildRegistry里漏了Registries.JUKEBOX_SONG