中级教程

添加 3D 音频

*此内容使用人工智能(Beta)翻译,可能包含错误。若要查看英文页面,请点按 此处

3D 音频 是从 3D 空间中特定位置发出的方向性声音,音量会根据音频发射器和听众之间的距离和方向而增加或减少。这意味着,当听众或发射器在环境中移动时,玩家可以动态地从不同的方向和音量级别听到该音频。

使用 姜饼屋 - 开始 .rbxl 文件作为起点,并以 姜饼屋 - 完整音频 作为参考,本教程向您展示如何为您的游戏添加循环和一次性 3D 音频,包括以下指导:

  • 循环环境声音,在玩家连接到服务器时立即播放。
  • 触发音频以通知玩家关于对他们的游戏至关重要的关键情境事件。
  • 激活音频以在玩家与 3D 对象交互时提供听觉反馈。
  • 播放角色音频片段,吸引并引导玩家前往他们环境中的兴趣点。

如果在过程中遇到困难,您可以使用 姜饼屋 - 完整音频 作为参考,以比较您的进度。

音频对象

要创建方向性音频,了解您将在本教程中使用的音频对象非常重要。3D 音频有六种主要类型的音频对象:

  • AudioPlayer 对象加载并播放 音频文件
  • AudioEmitter 对象是一个 虚拟扬声器,将音频发射到 3D 环境中。
  • AudioListener 对象是一个 虚拟麦克风,从 3D 环境中拾取音频。
  • AudioDeviceOutput 对象是现实世界中的 物理硬件设备,例如扬声器或耳机。
  • AudioDeviceInput 对象是现实世界中的 物理麦克风
  • Wires 将音频流从一个对象传输到另一个对象。

所有这些音频对象共同工作,发出声音,就像它们在现实世界中的对应物一样。让我们通过一个示例来看看这在实践中是如何工作的:一个玩家在玩游戏时佩戴耳机。

  • AudioPlayer1516791621 音频资产 ID 加载到游戏中,用于雨声轨道。
  • AudioEmitter 将雨声轨道音频流发射到 3D 环境中。
  • 一个 Wire 将流从 AudioPlayer 传输到 AudioEmitter,以便流从 3D 扬声器中发出。
  • 角色的子 AudioListener 对象在 3D 环境中监听该声音,并将其反馈到他们的耳机中。
  • AudioDeviceOutput 对象将声音从 AudioListener 传输到玩家的物理扬声器,或者在这种情况下,是他们的耳机。
  • AudioDeviceInput 对象捕获来自现实世界的声音,并将其反馈到游戏中以进行语音聊天。
游戏中的对象表示
现实世界中的对象表示

以下部分将深入探讨并引用这些对象,帮助您学习如何播放循环和一次性 3D 音频。在您回顾这些对象与即将到来的技术时,您可以更准确地预测如何捕获并将声音从游戏传递给玩家,反之亦然。

循环音频

循环 3D 音频,或在玩家连接到服务器时无缝重复的方向音频,是一种常见的声音设计技术,通过使 3D 空间感觉生动和动态来增强氛围。此外,循环 3D 音频保持您的环境声音源一致,例如电视的静电声或瀑布的轰鸣声;如果这些声音突然停止,环境将显得不真实。

为了演示这个概念,请查看以下两个大型瀑布的 3D 音频示例,当未循环的音轨完成时,它们会立即停止。虽然水声最初将玩家沉浸在户外环境中,但突然的听觉变化是刺耳的,与现实世界中瀑布的行为不自然。

同样,示例使用此技术为流动的巧克力瀑布调整音量,根据玩家与音频发射器的距离进行调整。当玩家距离发射器少于 20 个单位时,发射器以全音量发出声音。随着玩家的远离,水声每隔 20 个单位音量减少一次。这模拟了现实世界中声音随着距离源的远离而减小的效果。

要在示例 姜饼屋 - 完整音频 场所文件中重现循环 3D 音频:

  1. 启用一个默认监听器,该监听器附加到您的玩家角色。

    1. 资源管理器 窗口中,选择 SoundService
    2. 属性 窗口中,将 DefaultListenerLocation 设置为 Character。当您运行游戏时,引擎会自动:
      • 在每个玩家角色的 Humanoid.RootPart 下创建一个 AudioListener,以便您可以根据游戏中声音源的位置和音量在现实世界的扬声器中听到声音的变化。
      • SoundService 下创建一个 AudioDeviceOutput
  2. 资源管理器 窗口中,导航到 WorkspaceWaterfallAudioObject,然后:

    1. 插入一个 AudioPlayer 对象以创建瀑布的音频源。
    2. 插入一个 AudioEmitter 对象以从 WaterfallAudioObject 发射位置流。
    3. 插入一个 Wire 对象以将流从音频播放器传输到音频发射器。
  3. 选择 AudioPlayer,然后在 属性 窗口中,

    1. AssetID 设置为 rbxassetid://1516791621 以播放雨声音轨。
    2. 启用 Looping 以便音频无缝重复。
  4. 选择 AudioEmitter,然后在 属性 窗口中,将 DistanceAttenuation 设置为 {0: 1}, {20: 0.8}, {40: 0.4}, {80: 0},以便声音在距离音频发射器每隔 20 个单位时逐渐减小音量。

  5. 选择 Wire,然后在 属性 窗口中,

    1. SourceInstance 设置为您的新 AudioPlayer,以指定您希望电线从此特定音频播放器传输音频。
    2. TargetInstance 设置为您的新 AudioEmitter,以指定您希望电线将音频传输到此特定音频发射器。
  6. 返回 资源管理器 窗口,在 WaterfallAudioObject 中插入一个 Script,将其重命名为 LoopWaterfallMusic,将其 RunContext 属性设置为 Client,然后将以下代码粘贴到脚本中:

    local audioPlayer = script.Parent
    audioPlayer:Play()

    该脚本首先声明一个变量来表示脚本的父级 AudioPlayer。然后,脚本设置音频源在玩家加入游戏的瞬间播放,直到他们退出游戏。

  7. 进行游戏测试,以便在您的角色靠近瀑布时听到循环的巧克力雨声。当您旋转角色的头部以查看不同方向时,声音会根据发射器在 3D 空间中的位置在现实世界的扬声器中动态变化。

一次性音频

一次性 3D 音频,或在特定时间和位置播放一次的方向音频,除非玩家再次触发它,为玩家提供有关其行为、环境和周围任何角色的上下文。在您的游戏中使用这种类型的听觉反馈至关重要,因为它允许玩家做出战略决策,例如避免即将到来的敌人或拾取有用的物品。

以下部分提供了常见游戏场景的实现细节,在这些场景中,玩家需要及时的方向反馈,包括情境游戏事件、对象交互和非可玩角色对话。

事件反馈

当玩家在其环境中触发关键情境事件时,例如解锁新的游戏区域或引发敌人开火,了解他们需要在 3D 空间中将注意力和焦点指向何处至关重要。如果他们没有立即收到听觉反馈,他们可能会错过对其游戏至关重要的信息,从而导致不知道该去哪里或接下来该做什么的挫败感。

为了演示这一点,让我们回顾一下 激光标记 模板中每个玩家的发射器播放的一次性 3D 音频:

  • 每次玩家从发射器发射时,都会播放一个深沉的爆炸声。
  • 每次玩家用新一轮子弹重新装填发射器时,都会播放一次点击声和机器人哔声。

这两种声音通过提醒所有附近的玩家爆炸声的方向来提供情境意识,以便他们可以做出明智的决定,选择加入乐趣或避免潜在的危险。

该示例使用相同的技术为玩家提供关于完成游戏主要目标的奖励的情境意识。在他们收集到所有三个糖果后,姜饼屋的门打开,允许玩家进入里面的礼物。

由于玩家收集糖果的顺序没有特定要求,因此无论他们最后收集哪个糖果,玩家都需要意识到门的打开。位置音频使这一点成为可能,以便玩家意识到他们的成功以及他们接下来需要去的地方,无论他们与门的相对距离和方向如何。

要在示例 姜饼屋 - 完整音频 场所文件中重现一次性事件反馈 3D 音频:

  1. 资源管理器 窗口中,导航到 WorkspaceDoor,然后:

    1. 插入一个 AudioPlayer 对象以创建音量的音频源。
    2. 插入一个 AudioEmitter 对象以从 Door 发射位置流。
    3. 插入一个 Wire 对象以将流从音频播放器传输到音频发射器。
  2. 选择 AudioPlayer,然后在 属性 窗口中,将 AssetID 设置为 rbxassetid://5930776613 以播放滑动金属门的音轨。

  3. 选择 Wire,然后在 属性 窗口中,

    1. SourceInstance 设置为您的新 AudioPlayer,以指定您希望电线从此特定音频播放器传输音频。
    2. TargetInstance 设置为您的新 AudioEmitter,以指定您希望电线将音频传输到此特定音频发射器。
  4. 返回 资源管理器 窗口,导航到 ServerScriptService,然后插入一个 Script,将其重命名为 GumdropService,将其 RunContext 属性设置为 Server,然后将以下代码粘贴到脚本中:

    -- 初始化变量
    local Workspace = game:GetService("Workspace")
    local Players = game:GetService("Players")
    local ServerStorage = game:GetService("ServerStorage")
    local TweenService = game:GetService("TweenService")
    -- 模块
    local Leaderboard = require(ServerStorage.Leaderboard)
    local PlayerData = require(ServerStorage.PlayerData)
    -- 变量
    local gumdropsFolder = Workspace.Gumdrops
    local gumdrops = gumdropsFolder:GetChildren()
    local GUMDROP_KEY_NAME = PlayerData.GUMDROP_KEY_NAME
    local GUMDROP_AMOUNT_TO_ADD = 1
    local function updatePlayerGumdrops(player, updateFunction)
    -- 更新糖果表
    local newGumdropAmount = PlayerData.updateValue(player, GUMDROP_KEY_NAME, updateFunction)
    -- 更新糖果排行榜
    Leaderboard.setStat(player, GUMDROP_KEY_NAME, newGumdropAmount)
    -- 检查玩家是否收集了三个糖果
    if newGumdropAmount >= 3 then
    -- 当玩家收集三个糖果时播放门事件音频
    local audioPlayer = Workspace.Door.AudioPlayer
    audioPlayer:Play()
    -- 动画门向下移动
    local doorPart = Workspace.Door
    local tweenInfo = TweenInfo.new(2, Enum.EasingStyle.Linear)
    local tween = TweenService:Create(doorPart, tweenInfo, {Position = doorPart.Position + Vector3.new(0, -15, 0)})
    tween:Play()
    end
    end
    -- 定义事件处理程序
    local function onGumdropTouched(otherPart, gumdrop)
    if gumdrop:GetAttribute("Enabled") then
    local character = otherPart.Parent
    local player = Players:GetPlayerFromCharacter(character)
    if player then
    -- 玩家触碰了糖果
    local audioPlayer = gumdrop.AudioPlayer
    audioPlayer:Play()
    gumdrop.Transparency = 1
    gumdrop:SetAttribute("Enabled", false)
    updatePlayerGumdrops(player, function(oldGumdropAmount)
    oldGumdropAmount = oldGumdropAmount or 0
    return oldGumdropAmount + GUMDROP_AMOUNT_TO_ADD
    end)
    print("玩家收集了糖果")
    end
    end
    end
    -- 设置事件监听器
    for _, gumdrop in gumdrops do
    gumdrop:SetAttribute("Enabled", true)
    gumdrop.Touched:Connect(function(otherPart)
    onGumdropTouched(otherPart, gumdrop)
    end)
    end

    该脚本首先初始化 WorkspacePlayersServerStorageTweenService 服务,以便可以引用它们的子项和功能。然后,它在 ServerStorage 中要求 LeaderboardPlayerData 模块;这些模块负责在屏幕右上角创建和更新一个排行榜,跟踪玩家在环境中收集的糖果数量。

    脚本的 updatePlayerGumdrops 函数是触发事件反馈 3D 音频的主要工作发生的地方,它接受两个参数:

    • player - 收集糖果的玩家。
    • updateFunction - 更新玩家收集的糖果数量的回调函数。

    当玩家与糖果发生碰撞时,脚本:

    • 通过调用 PlayerData.updateValue 函数获取玩家的新糖果收集数量值。
    • 通过调用 Leaderboard.setStat 函数使用此新数量更新排行榜。
    • 检查数量是否大于或等于 3

    当该值大于或等于 3 时,脚本:

    • 从音频播放器播放 3D 音轨到音频发射器。
    • 将门线性移动 15 个单位到其当前位置下方。

    脚本的其余部分主要负责检测与糖果发生碰撞的任何物体是否为玩家,以便触发收集反馈的非位置声音。有关此部分脚本的更多信息,请参见 添加 2D 音频 - 游戏反馈

  5. 进行游戏测试,以便在您收集到环境中的所有三个糖果后听到滑动门的声音。当您旋转相机时,声音会根据发射器在 3D 空间中的位置在现实世界的扬声器中动态变化。

对象交互

当玩家与其环境中的 3D 对象交互时,例如打开灯开关或拾取武器,提供即时反馈非常重要,以便他们直观地理解 如何 与对象交互。视觉和听觉反馈的结合强化了玩家的行为与环境反应之间的因果关系。

为了扩展这个概念,让我们回顾以下来自 植物 示例的一个一次性 3D 音频,用于种植卷心菜的用户流程:

  • 当玩家种下种子时,会播放轻微的叮当声。
  • 当玩家给他们的植物浇水时,会播放湿润的溅水声。
  • 当玩家收集成熟的植物时,会播放剪切声。
  • 当玩家将卷心菜放入手推车时,会播放轻微的砰声。

所有这些声音都强化了玩家与对象在 3D 空间中变化形式的 接近提示 关键交互。对于视觉障碍的玩家来说,颜色变化或动画更难以单独辨别,提供这些多种形式的感官反馈有助于使您的 3D 对象交互对尽可能多的玩家保持可访问和直观。

为了给出如何配置对象交互与多种形式的感官反馈的不同示例,该示例在玩家踩到姜饼屋内的 3D 薄荷糖按钮时提供视觉和听觉反馈。当玩家未与按钮交互时,它看起来像一个典型的薄荷糖,但当他们踩到按钮时,示例:

  • 播放庆祝的音乐音轨。
  • 将按钮的侧面染成绿色。
  • 将按钮向下移动到地面中。

从这里,您可以将此交互连接到各种独特的游戏行为,例如解锁物品或触发特殊能力。

默认视图
按下状态

要在示例 姜饼屋 - 完整音频 场所文件中重现一次性对象交互 3D 音频:

  1. 资源管理器 窗口中,导航到 Workspace3DAudioButton,然后:

    1. 插入一个 AudioPlayer 对象以创建按钮的音频源。
    2. 插入一个 AudioEmitter 对象以从 3DAudioButton 发射位置流。
    3. 插入一个 Wire 对象以将流从音频播放器传输到音频发射器。
  2. 选择 AudioPlayer,然后在 属性 窗口中,将 AssetID 设置为 rbxassetid://1846248593 以播放欢快的庆祝音轨。

  3. 选择 Wire,然后在 属性 窗口中,

    1. SourceInstance 设置为您的新 AudioPlayer,以指定您希望电线从此特定音频播放器传输音频。
    2. TargetInstance 设置为您的新 AudioEmitter,以指定您希望电线将音频传输到此特定音频发射器。
  4. 返回 资源管理器 窗口,在 3DAudioButton 中插入一个 Script,将其重命名为 PlayAudioWhenPressed,然后将以下代码粘贴到脚本中:

    local TweenService = game:GetService("TweenService")
    local buttonModel = script.Parent.Parent
    local buttonPart = buttonModel.ButtonPart
    local buttonPressedAudioPlayer = buttonModel.ButtonPressedAudioPlayer
    local tweenInfo = TweenInfo.new(.2, Enum.EasingStyle.Exponential)
    local buttonTweenByIsPressed = {
    -- 按下
    [true] = TweenService:Create(buttonPart, tweenInfo, {
    Size = buttonPart.Size / Vector3.new(2, 1, 1),
    Color = Color3.fromRGB(75, 151, 75),
    }),
    -- 默认
    [false] = TweenService:Create(buttonPart, tweenInfo, {
    Size = buttonPart.Size,
    Color = Color3.fromRGB(196, 40, 28),
    }),
    }
    local function onIsPlayingChanged()
    local isPlaying = buttonPressedAudioPlayer.IsPlaying
    local tween = buttonTweenByIsPressed[isPlaying]
    tween:Play()
    end
    onIsPlayingChanged()
    buttonPressedAudioPlayer:GetPropertyChangedSignal("IsPlaying"):Connect(onIsPlayingChanged)
    buttonPressedAudioPlayer.Ended:Connect(onIsPlayingChanged)
    buttonPart.Touched:Connect(function(_hit)
    buttonPressedAudioPlayer:Play()
    end)

    该脚本首先获取:

    • TweenService,以便可以动画化从地面突出的按钮部分。
    • 脚本的父级 3DAudioButton 模型。
    • 从地面突出的按钮部分。
    • 相关的音频播放器,包含您的庆祝声音音轨。

    然后,脚本定义:

    • 一个 TweenInfo 对象,指定按钮的动画将以指数动画样式播放。
    • 两个表示按钮按下或未按下状态的动画。
      • true 按下状态将按钮稍微向下移动到地面中,并将部分的侧面染成绿色。
      • false 未按下状态将按钮移动回其原始位置,并去除之前的染色。

    剩余的脚本是对象交互反馈的主要工作发生的地方,因此让我们回顾一下 onIsPlayingChanged 函数和事件监听器如何协同工作:

    1. buttonPart.Touched 监听玩家触碰按钮,然后调用 Play() 函数以开始播放与音频播放器关联的音频。此过程将 AudioPlayer.IsPlaying 属性从 false 切换为 true
    2. buttonPressedAudioPlayer:GetPropertyChangedSignal("IsPlaying") 监听音频播放器的 IsPlaying 属性变化,然后调用 onIsPlayingChanged 函数。
    3. onIsPlayingChanged 函数利用此信息触发动画,改变其在 3D 空间中的视觉外观。
    4. 为了防止玩家在快速跳跃按钮时意外重新启动音频,buttonPressedAudioPlayer.Ended 监听音频播放器完成播放,然后再次调用 onIsPlayingChanged 函数。

    重要的是要注意,onIsPlayingChanged 事件仅在从 false 变为 true 时触发,这意味着在从 true 变为 false 时不会触发。这是预期的行为,因为在从服务器到客户端复制属性的时机上存在复杂性。因此,在此示例中提供并监听 Ended 事件以涵盖这两种情况。

  5. 进行游戏测试,以便在您的角色触碰姜饼屋中的 3D 按钮时听到庆祝声音。当您远离按钮时,声音的音量会降低。

角色对话

为您的非可玩角色 (NPC) 提供方向音频有助于引导玩家前往他们环境中的兴趣点,并为他们与其他角色的互动增添深度。事实上,在叙事驱动的游戏中,许多设计师战略性地使用角色对话间接教导玩家关于他们的角色、盟友和敌人角色或世界本身的信息。

这种技术的常见示例包括:

  • 对话风格 来设定游戏的基调。
  • 玩笑 来教导玩家关于角色关系的信息。
  • 敌人对话 来坦白动机或他们与玩家的关系。
  • 玩家角色大声说出他们的想法 来轻轻引导玩家该做什么,例如治疗自己、移动到另一个位置或寻找物品。
  • 盟友角色与玩家角色交谈 来揭示游戏世界的细节,如其历史、文化和社会问题。

为了演示这在实践中可能是什么样子,让我们回顾以下来自 超越黑暗 展示的一个一次性 3D 音频片段,该片段在玩家处于空间站的主大厅区域时定期播放。

使用空间站作为角色,这段对话片段为玩家提供了关于整体环境的重要上下文和背景。例如,从这句话中,玩家了解到:

  • 他们在外太空,具体来说是在名为 Kerr-Newman 深空中继 14 的空间站上。
  • 他们的环境是未来主义和友好的。
  • 他们是访客,可能很快就会离开。

这些细节共同将玩家沉浸在他们的环境中,并为他们的任务增添了一种紧迫感。然而,如果玩家不知道他们的主要任务是什么,您也可以使用角色对话来告知玩家您希望他们在游戏中做什么。

为了说明,示例使用一个 音量,或 3D 空间中的不可见区域,触发雪人角色的对话,引导玩家收集三个糖果以打开他家的门。作为玩家加入游戏时看到的第一件事,玩家更有可能触发对话并知道他们需要做什么才能成功。

雪人周围的音量使用碰撞反馈在玩家进入 3D 区域时播放音频。

要在示例 姜饼屋 - 完整音频 场所文件中重现一次性角色对话 3D 音频:

  1. 资源管理器 窗口中,导航到 WorkspaceDialogueVolume,然后:

    1. 插入一个 AudioPlayer 对象以创建音量的音频源。
    2. 插入一个 AudioEmitter 对象以从 DialogueVolume 发射位置流。
    3. 插入一个 Wire 对象以将流从音频播放器传输到音频发射器。
  2. 选择 AudioPlayer,然后在 属性 窗口中,将 AssetID 设置为 rbxassetid://92917410841704 以播放游戏目标的说明音轨。

  3. 选择 Wire,然后在 属性 窗口中,

    1. SourceInstance 设置为您的新 AudioPlayer,以指定您希望电线从此特定音频播放器传输音频。
    2. TargetInstance 设置为您的新 AudioEmitter,以指定您希望电线将音频传输到此特定音频发射器。
  4. 返回 资源管理器 窗口,导航到 StarterPlayerStarterCharacterScripts,插入一个 LocalScript,将其重命名为 PlayAudioWhenInVolume,并将以下代码粘贴到本地脚本中:

    local Workspace = game:GetService("Workspace")
    local Players = game:GetService("Players")
    local humanoid = script.Parent:WaitForChild("Humanoid")
    local volumeDetector = Workspace.DialogueVolume
    local trigger = humanoid:WaitForChild("Animator")
    local debounce = false
    local localPlayer = Players.LocalPlayer
    volumeDetector.Touched:Connect(function(hit)
    if debounce then
    return
    end
    local hitCharacter = hit:FindFirstAncestorWhichIsA("Model")
    local hitPlayer = Players:GetPlayerFromCharacter(hitCharacter)
    if hitPlayer ~= localPlayer then
    return
    end
    debounce = true
    local audioPlayer = Workspace.DialogueVolume.AudioPlayer
    audioPlayer:Play()
    audioPlayer.Ended:Wait()
    debounce = false
    end)

    该脚本首先获取 Workspace 和 Players 服务,以便可以引用它们的子项和功能。对于每个加载或重生回到游戏中的玩家角色,脚本等待:

    • 角色的 HumanoidAnimator 对象。
    • 工作区中名为 DialogueVolume 的音量对象。

    当任何物体与音量发生碰撞时,Touched 事件处理函数获取第一个祖先是 Model 的对象,如果碰撞的 BasePart 是角色模型的后代,则应该是角色。如果是,函数将:

    • 将 debounce 设置为 true
    • 播放并等待音频结束。
    • 将 debounce 设置回 false

    将 debounce 从 false 变为 true,然后在音频播放结束后再次变为 false 是一种防抖模式,防止音频在玩家不断与音量碰撞时重复触发。有关此防抖模式的更多信息,请参见 防抖 - 检测碰撞

  5. 进行游戏测试,以便在您的角色触碰雪人周围的音量时听到说明性角色对话。

©2026 Roblox Corporation、Roblox、Roblox 标志及 Powering Imagination 是我们在美国及其他国家或地区的注册与未注册商标。