声音

声音

声音虽然不是必需内容,却能让 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。
  • 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。