声音
声音虽然不是必需内容,却能让 Mod 更有细节和活力。Minecraft 提供了多种注册及播放声音的方式,本页将逐一介绍。
术语
Minecraft 声音引擎使用以下术语区分不同对象:
- 声音事件:代码中的触发器,通知声音引擎播放某种声音。注册到游戏中的对象也是
SoundEvent。 - 声音类别或声音来源:对声音的粗略分组,可分别调整。声音设置中的滑块对应
master、block、player等类别,代码中由SoundSource枚举表示。 - 声音定义:把声音事件映射到一个或多个声音对象,并包含可选元数据,位于命名空间的
sounds.json文件中。 - 声音对象:包含声音文件位置及可选元数据的 JSON 对象。
- 声音文件:磁盘上的实际声音文件。Minecraft 仅支持
.ogg。
重要警告 受 OpenAL(Minecraft 使用的音频库)实现限制,若希望音量随玩家距离远近衰减,声音文件必须是单声道。立体声或多声道文件不会衰减,始终在玩家位置播放,因此适合环境音和背景音乐。另见 MC-146721。
创建 SoundEvent
声音事件属于注册对象,必须是单例,并通过 DeferredRegister 注册:
public class MySoundsClass {
// Assuming that your mod id is examplemod
public static final DeferredRegister<SoundEvent> SOUND_EVENTS =
DeferredRegister.create(BuiltInRegistries.SOUND_EVENT, "examplemod");
// All vanilla sounds use variable range events.
public static final Holder<SoundEvent> MY_SOUND = SOUND_EVENTS.register(
"my_sound",
// Takes in the registry name
SoundEvent::createVariableRangeEvent
);
// There is a currently unused method to register fixed range (= non-attenuating) events as well:
public static final Holder<SoundEvent> MY_FIXED_SOUND = SOUND_EVENTS.register(
"my_fixed_sound",
// 16 is the default range of sounds. Be aware that due to OpenAL limitations,
// values above 16 have no effect and will be capped to 16.
registryName -> SoundEvent.createFixedRangeEvent(registryName, 16f)
);
}
不要忘记在 Mod 构造函数中,将注册器接入 Mod 事件总线:
public ExampleMod(IEventBus modBus) {
MySoundsClass.SOUND_EVENTS.register(modBus);
// other things here
}
这样就创建了声音事件。
sounds.json
另见 Minecraft Wiki 的 sounds.json 页面。
要将声音事件连接到实际文件,需要创建声音定义。同一命名空间的全部定义都保存在根目录下唯一的 sounds.json 中,也称声音定义文件。每个定义将声音事件 ID(例如 my_sound)映射到 JSON 声音对象。此处 ID 不含命名空间,因为文件所在位置已经决定了命名空间。示例如下:
{
// Sound definition for the sound event "examplemod:my_sound"
"my_sound": {
// List of sound objects. If this contains more than one element, an element will be chosen randomly.
"sounds": [
// Only name is required, all other properties are optional.
{
// Location of the sound file, relative to the namespace's sounds folder.
// This example references a sound at assets/examplemod/sounds/sound_1.ogg.
"name": "examplemod:sound_1",
// May be "file" or "event". "file" causes the name to refer to a sound file.
// "event" causes the name to refer to another sound event. Defaults to "file".
"type": "file",
// The volume this sound will be played at. Must be between 0.0 and 1.0 (default).
"volume": 0.8,
// The pitch value the sound will be played at.
// Must be between 0.0 and 2.0. Defaults to 1.0.
"pitch": 1.1,
// Weight of this sound when choosing a sound from the sounds list. Defaults to 1.
"weight": 3,
// If true, the sound will be streamed from the file instead of loaded all at once.
// Recommended for sound files that are more than a few seconds long. Defaults to false.
"stream": true,
// Manual override for the attenuation distance. Defaults to 16. Ignored by fixed range sound events.
"attenuation_distance": 8,
// If true, the sound will be loaded into memory on pack load, instead of when the sound is played.
// Vanilla uses this for underwater ambience sounds. Defaults to false.
"preload": true
},
// Shortcut for { "name": "examplemod:sound_2" }
"examplemod:sound_2"
]
},
"my_fixed_sound": {
// Optional. If true, replaces sounds from other resource packs instead of adding to them.
// See the Merging chapter below for more information.
"replace": true,
// The translation key of the subtitle displayed when this sound event is triggered.
"subtitle": "examplemod.my_fixed_sound",
"sounds": [
"examplemod:sound_1",
"examplemod:sound_2"
]
}
}
合并
与多数资源文件不同,sounds.json 不会直接覆盖低优先级资源包的值,而是先合并,再作为一份完整文件解释。假设 RP1 和 RP2 两个资源包分别定义了 sound_1 至 sound_4,并且 RP2 位于 RP1 下方。
RP1 中的 sounds.json:
{
"sound_1": {
"sounds": [
"sound_1"
]
},
"sound_2": {
"replace": true,
"sounds": [
"sound_2"
]
},
"sound_3": {
"sounds": [
"sound_3"
]
},
"sound_4": {
"replace": true,
"sounds": [
"sound_4"
]
}
}
RP2 中的 sounds.json:
{
"sound_1": {
"sounds": [
"sound_5"
]
},
"sound_2": {
"sounds": [
"sound_6"
]
},
"sound_3": {
"replace": true,
"sounds": [
"sound_7"
]
},
"sound_4": {
"replace": true,
"sounds": [
"sound_8"
]
}
}
游戏最终用来加载声音的合并结果如下。这个结果只存在于内存,不会写入文件:
{
"sound_1": {
// replace false and false: add from lower pack, then from upper pack
"sounds": [
"sound_5",
"sound_1"
]
},
"sound_2": {
// replace true in upper pack and false in lower pack: add from upper pack only
"sounds": [
"sound_2"
]
},
"sound_3": {
// replace false in upper pack and true in lower pack: add from lower pack, then from upper pack
// Would still discard values from a third resource pack sitting below RP2
"sounds": [
"sound_7",
"sound_3"
]
},
"sound_4": {
// replace true and true: add from upper pack only
"sounds": [
"sound_8"
]
}
}
播放声音
Minecraft 提供多种播放方法,有时不容易判断该用哪一种。它们接受 SoundEvent,既可以是自定义事件,也可以是 SoundEvents 中的原版事件。下面所说的客户端与服务端分别指逻辑客户端和逻辑服务端。
Level
playSeededSound(Entity entity, double x, double y, double z, Holder<SoundEvent> soundEvent, SoundSource soundSource, float volume, float pitch, long seed)- 客户端:只有传入玩家是本地玩家时,才在指定位置向该玩家播放,否则不执行操作。
- 服务端:向除传入玩家外的所有玩家发送数据包,通知客户端在指定位置播放声音。
- 用途:从客户端发起、两侧都会运行的代码调用。服务端排除发起玩家,可以避免该玩家听到两次声音。也可从服务端发起的代码(例如方块实体)中传入
null玩家,让所有人听见。
playSound(Entity entity, double x, double y, double z, SoundEvent soundEvent, SoundSource soundSource, float volume, float pitch)- 随机选择种子,将声音事件包装为 holder 后转调
playSeededSound。
- 随机选择种子,将声音事件包装为 holder 后转调
playSound(Entity entity, BlockPos pos, SoundEvent soundEvent, SoundSource soundSource, float volume, float pitch)- 转调上一方法,坐标分别为
pos.getX() + 0.5、pos.getY() + 0.5和pos.getZ() + 0.5。
- 转调上一方法,坐标分别为
playLocalSound(double x, double y, double z, SoundEvent soundEvent, SoundSource soundSource, float volume, float pitch, boolean distanceDelay)- 客户端:在指定位置向玩家播放,不向服务端发送信息。
distanceDelay为true时,根据玩家距离延迟播放。 - 服务端:不执行操作。
- 用途:从服务端发送的自定义数据包中调用,原版用于雷声。
- 客户端:在指定位置向玩家播放,不向服务端发送信息。
playPlayerSound(SoundEvent soundEvent, SoundSource soundSource, float volume, float pitch)- 客户端:播放绑定到玩家位置的声音,不向服务端发送信息。
- 服务端:不执行操作。
- 用途:原版用于方块环境音。
ClientLevel
playLocalSound(BlockPos pos, SoundEvent soundEvent, SoundSource soundSource, float volume, float pitch, boolean distanceDelay)- 转调
Level#playLocalSound,坐标分别为pos.getX() + 0.5、pos.getY() + 0.5和pos.getZ() + 0.5。
- 转调
Entity
playSound(SoundEvent soundEvent, float volume, float pitch)- 转调
Level#playSound,玩家参数为null,声音类别为Entity#getSoundSource,坐标为实体位置,其余参数原样传入。
- 转调
Player
playSound(SoundEvent soundEvent, float volume, float pitch),重写Entity的方法。- 转调
Level#playSound,玩家为this,类别为SoundSource.PLAYER,坐标为玩家位置,其余参数原样传入。因此其两侧行为与Level#playSound对应:- 客户端:在给定位置向当前客户端玩家播放。
- 服务端:向该位置附近除调用对象所代表玩家以外的其他人播放。
- 转调
数据生成
声音文件本身不能生成,但可以生成 sounds.json。继承 SoundDefinitionsProvider 并重写 registerSounds():
public class MySoundDefinitionsProvider extends SoundDefinitionsProvider {
// Parameters can be obtained from `GatherDataEvent.Client`.
public MySoundDefinitionsProvider(PackOutput output) {
// Use your actual mod id instead of "examplemod".
super(output, "examplemod");
}
@Override
public void registerSounds() {
// Accepts a Holder<SoundEvent>, a SoundEvent, or a Identifier as the first parameter.
add(MySoundsClass.MY_SOUND, SoundDefinition.definition()
// Add sound objects to the sound definition. Parameter is a vararg.
.with(
// Accepts either a string or a Identifier as the first parameter.
// The second parameter can be either SOUND or EVENT, and can be omitted if the former.
// A type of SOUND refer to the "file" type mentioned previously
sound("examplemod:sound_1", SoundDefinition.SoundType.SOUND)
// Sets the volume. Also has a double counterpart.
.volume(0.8f)
// Sets the pitch. Also has a double counterpart.
.pitch(1.2f)
// Sets the weight.
.weight(2)
// Sets the attenuation distance.
.attenuationDistance(8)
// Enables streaming.
// Also has a parameterless overload that defers to stream(true).
.stream(true)
// Enables preloading.
// Also has a parameterless overload that defers to preload(true).
.preload(true),
// The shortest we can get.
sound("examplemod:sound_2")
)
// Sets the subtitle.
.subtitle("sound.examplemod.sound_1")
// Enables replacing.
.replace(true)
);
}
}
与其他数据提供器一样,别忘了通过事件注册:
@SubscribeEvent // on the mod event bus
public static void gatherData(GatherDataEvent.Client event) {
event.createProvider(MySoundDefinitionsProvider::new);
}
来源: NeoForged/Documentation(26.1)。中文翻译依据提交
4a680cdd4d15。