声音
声音虽然不是必需的,却能让 Mod 更细腻、更有生气。Minecraft 提供多种注册和播放声音的方式,本文将逐一介绍。
术语
Minecraft 声音引擎用以下术语区分不同概念:
- 声音事件:代码中的触发器,用于告诉声音引擎播放某个声音。注册进游戏的对象也是
SoundEvent。 - 声音类别或声音来源:声音的大致分组,可分别调节。声音设置界面的滑块对应这些类别,例如
master、block、player等。代码中由SoundSource枚举表示。 - 声音定义:将一个声音事件映射到一个或多个声音对象,并包含可选元数据。声音定义位于命名空间的
sounds.json文件中。 - 声音对象:包含声音文件位置和可选元数据的 JSON 对象。
- 声音文件:磁盘上的音频文件。Minecraft 仅支持
.ogg音频。
重要警告 受 Minecraft 音频库 OpenAL 的实现限制,声音若需随玩家距离远近而衰减,文件必须是单声道。立体声(多声道)文件不会衰减,始终在玩家位置播放,因此适合环境音和背景音乐。另见 MC-146721。
创建 SoundEvent
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 DeferredHolder<SoundEvent, SoundEvent> MY_SOUND = SOUND_EVENTS.register(
"my_sound", // must match the resource location on the next line
() -> SoundEvent.createVariableRangeEvent(ResourceLocation.fromNamespaceAndPath("examplemod", "my_sound"))
);
// There is a currently unused method to register fixed range (= non-attenuating) events as well:
public static final DeferredHolder<SoundEvent, 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.
() -> SoundEvent.createFixedRangeEvent(ResourceLocation.fromNamespaceAndPath("examplemod", "my_fixed_sound"), 16)
);
}
还需要在 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 的 sounds.json 中定义了 sound_1、sound_2、sound_3 和 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(Player player, double x, double y, double z, Holder<SoundEvent> soundEvent, SoundSource soundSource, float volume, float pitch, long seed)- 客户端:如果传入的玩家是本地玩家,则在指定位置向该玩家播放事件;否则不执行操作。
- 服务端:向除传入玩家外的所有玩家发送数据包,要求客户端在指定位置播放声音事件。
- 用途:可在由客户端发起、两端都会执行的代码中调用。服务端排除发起玩家,避免该玩家听到两次。也可以在服务端发起的代码(例如方块实体)中传入
null玩家,向所有人播放。
playSound(Player player, double x, double y, double z, SoundEvent soundEvent, SoundSource soundSource, float volume, float pitch)- 将
SoundEvent包装为 holder,选择随机种子后转发给playSeededSound。
- 将
playSound(Player player, BlockPos pos, SoundEvent soundEvent, SoundSource soundSource, float volume, float pitch)- 转发给上一方法,
x、y、z分别使用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,会依据玩家距离延迟播放。 - 服务端:不执行操作。
- 用途:在服务端发送的自定义数据包处理中调用。原版使用它播放雷声。
- 客户端:在指定位置向玩家播放声音,不向服务端发送内容。若
ClientLevel
playLocalSound(BlockPos pos, SoundEvent soundEvent, SoundSource soundSource, float volume, float pitch, boolean distanceDelay)- 转发给
Level#playLocalSound,x、y、z分别使用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.
public MySoundDefinitionsProvider(PackOutput output, ExistingFileHelper existingFileHelper) {
// Use your actual mod id instead of "examplemod".
super(output, "examplemod", existingFileHelper);
}
@Override
public void registerSounds() {
// Accepts a Supplier<SoundEvent>, a SoundEvent, or a ResourceLocation 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 ResourceLocation 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 event) {
DataGenerator generator = event.getGenerator();
PackOutput output = generator.getPackOutput();
ExistingFileHelper existingFileHelper = event.getExistingFileHelper();
// other providers here
generator.addProvider(
event.includeClient(),
new MySoundDefinitionsProvider(output, existingFileHelper)
);
}
来源: NeoForged/Documentation(1.21.1)。中文翻译依据提交
4a680cdd4d15。