本篇教程的视频

本篇教程的源代码

GitHub地址:

本篇教程目标

  • 搞清楚声音事件和ogg文件是怎么对上的
  • 学会注册声音事件
  • 学会写sounds.json和字幕
  • 学会给方块套自定义音效组,以及在代码里主动播放声音

介绍

Minecraft里的声音分很多种,走路的声音、挖方块的声音、村民嘟囔的声音、唱片里的音乐,全都是「声音事件」加「音频文件」凑出来的

我们当然也能加自己的

在开始之前,得先准备ogg格式的音频

这里有两个坑,先说一下:

  • 不要直接把mp3改后缀名,那样是放不出来的,得用格式工厂这类软件真转成ogg
  • 音频要导出成单声道。立体声的音频在游戏里没有距离衰减,站多远都能听见,听起来会很怪

这一期和 1.21.1 那篇的区别,主要是移除混淆后的类名和方法名,写法上倒是差不多的

另外,26.1 的SoundEvent是个record,里面就两个字段:location(音频位置)和fixedRange(固定的可听距离,可以没有)

所以它有两个工厂方法

createVariableRangeEvent(id)是普通音效,距离随音量走,绝大多数音效都用这个

createFixedRangeEvent(id, range)把可听距离写死,不随音量变化,用在一些特殊场合

这一期我们只用第一个

查看源代码

原版怎么注册声音事件

原版的声音事件都在SoundEvents这个类里

1
2
3
4
5
public static final SoundEvent NETHER_BRICKS_BREAK = register("block.nether_bricks.break");
public static final SoundEvent NETHER_BRICKS_STEP = register("block.nether_bricks.step");
public static final SoundEvent NETHER_BRICKS_PLACE = register("block.nether_bricks.place");
public static final SoundEvent NETHER_BRICKS_HIT = register("block.nether_bricks.hit");
public static final SoundEvent NETHER_BRICKS_FALL = register("block.nether_bricks.fall");

注意括号里的字符串是block.nether_bricks.break这种点分格式,这个是资源路径,和json里的键是对应的

再看看它的注册方法

1
2
3
4
5
6
7
8
9
10
11
private static SoundEvent register(final String id) {
return register(Identifier.withDefaultNamespace(id));
}

private static SoundEvent register(final Identifier id) {
return register(id, id);
}

private static SoundEvent register(final Identifier id, final Identifier soundId) {
return Registry.register(BuiltInRegistries.SOUND_EVENT, id, SoundEvent.createVariableRangeEvent(soundId));
}

常规的,有一堆重载方法,实际上我们只需要关注最后一个就好了

很简单,就是往SOUND_EVENT这个注册表里塞一个SoundEvent

这个方法传了两个id,第一个是注册名,第二个是声音ID。原版为了方便,两个传的一样

SoundType

然后是方块音效组,也就是SoundType

这个类不算注册表,就是个普通的对象,构造函数长这样

1
2
3
4
5
6
7
8
9
public SoundType(
final float volume,
final float pitch,
final SoundEvent breakSound,
final SoundEvent stepSound,
final SoundEvent placeSound,
final SoundEvent hitSound,
final SoundEvent fallSound
)

一共七个参数

前两个是音量和音调

后面五个按顺序是:破坏、踩踏、放置、击打、掉落

掉落那个是指玩家或实体砸在上面的时候,不是方块自己掉下去

原版的下界砖是这样写的

1
2
3
4
5
6
public static final SoundType NETHER_BRICKS = new SoundType(1.0F, 1.0F,
SoundEvents.NETHER_BRICKS_BREAK,
SoundEvents.NETHER_BRICKS_STEP,
SoundEvents.NETHER_BRICKS_PLACE,
SoundEvents.NETHER_BRICKS_HIT,
SoundEvents.NETHER_BRICKS_FALL);

这五个参数的顺序一定要对上,写反了不会报错,但挖方块的时候放的是踩踏声,很难发现

方块那边就是用sound这个方法把音效组挂上去,比如

1
BlockBehaviour.Properties.of().strength(2.0F, 6.0F).sound(SoundType.NETHER_BRICKS)

所以想给方块换音效,思路就是:先攒五个声音事件,拼成一个SoundType,再在方块属性里挂上

动手写

创建 ModSoundEvents

我们新建一个sound包,写ModSoundEvents

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ModSoundEvents {
public static final SoundEvent PROSPECTOR_FOUND_ORE = register("prospector_found_ore");

public static final SoundEvent BLOCK_BREAK = register("block_break");
public static final SoundEvent BLOCK_STEP = register("block_step");
public static final SoundEvent BLOCK_PLACE = register("block_place");
public static final SoundEvent BLOCK_HIT = register("block_hit");
public static final SoundEvent BLOCK_FALL = register("block_fall");

private static SoundEvent register(String name) {
Identifier id = Identifier.fromNamespaceAndPath(TutorialMod.MOD_ID, name);
return Registry.register(BuiltInRegistries.SOUND_EVENT, id, SoundEvent.createVariableRangeEvent(id));
}
}

注册方法和原版一样,只是把命名空间换成我们自己的

这里注册了六个事件,一个给探矿器用,五个给方块音效组用

名字都是小写下划线,没有点号

你有多个方块音效要添加时,也可以使用原版那种block.nether_bricks.break,不过注意要和后面sounds.json里的键一致

音效组

接着在同一类里拼一个音效组

1
2
public static final SoundType ICE_ETHER_SOUND = new SoundType(1.0F, 1.0F,
BLOCK_BREAK, BLOCK_STEP, BLOCK_PLACE, BLOCK_HIT, BLOCK_FALL);

因为SoundType不是注册表里的东西,所以不用注册,直接new就行

初始化方法

1
2
3
public static void register() {
TutorialMod.LOGGER.info("Registering Mod Sound Events for " + TutorialMod.MOD_ID);
}

这个方法里没有正经逻辑,就打了行日志

然后在主类里调用

1
ModSoundEvents.register();

放在方块注册后面就行

给方块用上

找ModBlocks里的ICE_ETHER_BLOCK,在属性后面加上sound

1
2
3
public static final Block ICE_ETHER_BLOCK = register("ice_ether_block", Block::new,
BlockBehaviour.Properties.of().strength(1.0f, 3.0f).requiresCorrectToolForDrops()
.sound(ModSoundEvents.ICE_ETHER_SOUND), true);

这样破坏、放置、走上去、打一下、摔在上面,放的都会是我们的声音

其他方块想换也可以照这样加,只要它们共用同一个音效组就行

在代码里播放声音

有些声音不是方块自己触发的,得我们手动播

比如之前写的探矿器,扫到矿石的时候可以响一声

1
2
3
4
5
if (found) {
level.playSound(null, pos, ModSoundEvents.PROSPECTOR_FOUND_ORE, SoundSource.BLOCKS, 1.0f, 1.0f);
} else {
player.sendSystemMessage(Component.literal("No ore found!"));
}

playSound的参数是:发出声音的实体(可以填null)、位置、声音事件、声音类别、音量、音调

这里声音类别用SoundSource.BLOCKS

填null的意思是「不是某个实体发出来的」,声音就从坐标那里放出来

level在服务端调用没问题,原版会自己同步给附近的玩家,不用我们操心

数据文件

sounds.json

声音事件注册好了,但游戏还不知道该播哪个文件

这个对应关系写在assets/tutorial/sounds.json里

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
{
"prospector_found_ore": {
"subtitle": "subtitles.tutorial.prospector_found_ore",
"sounds": [
"tutorial:1"
]
},
"block_break": {
"subtitle": "subtitles.tutorial.block_break",
"sounds": [
"tutorial:block_break"
]
},
"block_step": {
"subtitle": "subtitles.tutorial.block_step",
"sounds": [
"tutorial:block_step"
]
},
"block_place": {
"subtitle": "subtitles.tutorial.block_place",
"sounds": [
"tutorial:block_place"
]
},
"block_hit": {
"subtitle": "subtitles.tutorial.block_hit",
"sounds": [
"tutorial:block_hit"
]
},
"block_fall": {
"subtitle": "subtitles.tutorial.block_fall",
"sounds": [
"tutorial:block_fall"
]
}
}

外层这个键,就是我们注册时用的名字,必须一模一样

subtitle是字幕的翻译键,开了「显示字幕」之后,屏幕右下角就会显示这个

sounds里写的是音频文件的位置,格式是命名空间:文件名,不带.ogg

这里有个容易搞错的地方:文件的名字和事件的名字不需要一样

比如探矿器那条,事件叫prospector_found_ore,但音频文件叫1.ogg,所以写的是tutorial:1

sounds.json只是负责「事件 → 文件」的映射,两边怎么起名都行,只要能对上

音频文件放在assets/tutorial/sounds/下面,就是我们的block_break.ogg这些

路径和sounds.json里的写法是对应的:写tutorial:block_break,就找sounds/block_break.ogg

语言文件

字幕也是要翻译的,不然开字幕的时候显示的是一串键名

1
2
3
4
5
6
translationBuilder.add("subtitles.tutorial.prospector_found_ore", "Prospector found ore");
translationBuilder.add("subtitles.tutorial.block_break", "Block breaks");
translationBuilder.add("subtitles.tutorial.block_step", "Footsteps");
translationBuilder.add("subtitles.tutorial.block_place", "Block placed");
translationBuilder.add("subtitles.tutorial.block_hit", "Block hit");
translationBuilder.add("subtitles.tutorial.block_fall", "Something fell");

中文那份放到ModZhCnLangProvider里

1
2
3
4
5
6
translationBuilder.add("subtitles.tutorial.prospector_found_ore", "探矿器发现了矿石");
translationBuilder.add("subtitles.tutorial.block_break", "方块被破坏");
translationBuilder.add("subtitles.tutorial.block_step", "脚步声");
translationBuilder.add("subtitles.tutorial.block_place", "方块被放置");
translationBuilder.add("subtitles.tutorial.block_hit", "方块被击打");
translationBuilder.add("subtitles.tutorial.block_fall", "有东西掉下来了");

这个键和sounds.json里的subtitle必须一致

原版用的格式是subtitles.<命名空间>.<名字>,我们照这个来

测试

跑一遍数据生成,把语言文件生成出来,然后进游戏进行测试

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

  • 完全没有声音:先看sounds.json的位置对不对,必须是assets/<命名空间>/sounds.json,不是assets/<命名空间>/sounds/sounds.json
  • 声音事件不认:注册时的名字和sounds.json外层的键不一致
  • 音频找不到:sounds里写的是命名空间:文件名,不带.ogg,文件本身必须放在sounds/目录下
  • 字幕显示成一串键名:语言文件没加,或者subtitle和语言文件里的键不一致
  • 站着不动也能听到:音频导出成立体声了,重新导出成单声道
  • 挖方块的声音不对:SoundType五个参数的顺序错了,顺序是破坏、踩踏、放置、击打、掉落
  • 改了音频没反应:客户端有缓存,F3 + T重载资源包,或者把音频文件名改一下
  • 把mp3改了后缀名放进去:不行,必须真转成ogg