中级教程

添加 3D 音频

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

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

使用 Gingerbread House - Start .rbxl 文件作为起点,并以 Gingerbread House - Complete Audio 作为参考,本教程将向你展示如何向你的体验添加循环和一次性 3D 音频,包括以下指导:

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

如果在过程中你遇到困难,可以使用 Gingerbread House - Complete Audio 作为参考,以比较你的进度。

音频对象

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

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

所有这些音频对象协同工作,发出声音,就像它们在现实世界中的对应物一样。让我们通过一个示例来看它是如何工作的:想象一个玩家在笔记本电脑上玩体验时戴着耳机:

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

接下来的部分将深入探讨,并引用这些对象,帮助你学习如何播放循环和一次性 3D 音频。当你回顾这些对象及即将介绍的技术时,你可以更准确地预测如何在体验中捕捉并传递声音给玩家,反之亦然。

循环音频

循环 3D 音频,或在玩家连接到服务器后无缝重复的定向音频,是一种常见的声音设计技术,以通过让 3D 空间感觉活跃且动态来增强氛围。此外,循环 3D 音频保持你的环境声音源一致,例如电视的静态或瀑布的咆哮;如果这些声音突然停止,环境会感觉不真实。

为了演示这个概念,请查看以下两个大瀑布的 3D 音频的表现,它们在未循环的音轨完成时立即停止。尽管水声最初让玩家沉浸在户外环境中,但突如其来的音频变化是刺耳且不自然的,这与现实世界中瀑布的行为不符。

类似地,此示例对流动的巧克力瀑布使用了此技术,并根据玩家与音频发射器之间的距离调整其音量。当玩家距离发射器不到 20 个单位时,发射器以最大音量发出声音。当玩家距离更远时,音量每 20 个单位逐渐减小。这模拟了现实世界中,声音离声源越远,音量越小的效果。

要在示例 Gingerbread House - Complete Audio 位置文件中重现循环 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 空间中将注意力和焦点指向何处至关重要。如果他们没有收到即时的音频反馈,他们可能会错过对他们的游戏非常重要的信息,从而导致不知道该去哪里或接下来该做什么的沮丧感。

为了演示这一点,让我们查看来自 Laser Tag 模板的 3D 音频,该音频由每个玩家的激光枪播放:

  • 每次玩家从他们的激光枪发射时,都会播放深沉的炮声。
  • 每当玩家为激光枪重新装填新弹药时,都会播放点击声和机械的哔声。

这两种声音都通过提醒附近的所有玩家炮火来自哪个方向来提供情境意识,因此他们可以做出明智的决定要么加入乐趣,要么避开潜在的危险。

该示例使用相同的技术向玩家提供关于完成体验主目标的奖励的情境意识。在他们收集所有三个糖果之前,姜饼屋的门打开,以便玩家进入其中的礼物。

因为玩家在收集糖果时没有特定顺序,所以无论他们最后收集哪个糖果,了解门的打开情况都很重要。定向音频使这一点成为可能,以使玩家能够意识到他们的成功以及他们需要去哪里,无论他们与门的相对距离和方向如何。

要在示例 Gingerbread House - Complete Audio 位置文件中重现一次性事件反馈 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 对象互动时,例如打开灯光开关或捡起武器,提供即时反馈非常重要,让他们直观地理解 如何 与对象互动。视觉和听觉反馈的结合加强了玩家行为与环境反应之间的因果关系。

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

  • 玩家播种时会发出轻微的叮当声。
  • 玩家给生长的植物浇水时会发出湿润的溅水声。
  • 玩家收集成熟植物时会发出剪切声。
  • 玩家将卷心菜放入手推车时会发出柔和的闷响。

所有这些声音都强化了玩家与对象在 3D 空间中变化形式的重要互动提示。对于那些视觉障碍的玩家,颜色变化或动画本身难以解读,提供多种感官反馈可以使你的 3D 对象交互对尽可能多的玩家保持可访问和直观。

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

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

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

默认视图
被按下的状态

要在示例 Gingerbread House - Complete Audio 位置文件中重现一次性对象交互 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)提供定向音频有助于指引玩家前往其环境中的要点,并为他们与其他角色的互动增添深度。实际上,在以叙事驱动的体验中,许多设计者会有策略地利用角色对话间接教导玩家有关他们的角色、盟友和敌人的信息,或整个世界的知识。

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

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

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

以空间站作为一个角色,该对话片段为玩家提供有关整体背景的重要上下文。例如,从这句单独的句子中,玩家了解到:

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

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

为了说明,示例使用一个 音量,或在 3D 空间中的隐形区域,触发雪人发出的角色对话,指引玩家收集三颗糖果以打开通往他家的门。因为这是玩家进入体验时看到的第一件事,玩家更有可能触发对话并知道他们需要怎么做才能成功。

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

要在示例 Gingerbread House - Complete Audio 位置文件中重现一次性角色对话 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,这是防止音频反复触发的去防抖模式。有关该去防抖模式的更多信息,请参见 Debounce - 检测碰撞

  5. 进行游戏测试,当你的玩家角色触碰雪人周围的音量时听到说明性的角色对话音频。

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