游戏音频功能与角色对话
Game-Specific Audio Features & Character Dialog
在 3D 音频渲染管线之上,游戏还要实现分屏、物理驱动音频、动态音乐、角色对话、声音合成和人群声音等专门功能。GEA 重点讲了分屏的多听者问题、顽皮狗《最后生还者》的角色对话系统(逻辑台词、随机不重复、优先级与打断、把对话作为一等实体、排他性、基于规则和事实字典的分支对话、按区域标签的情境台词、对话手势)以及音乐系统(播放列表、紧张度、插入乐、过渡)(GEA4 卷 II 15.6)。
常见的游戏专属音频功能
(卷 II 15.6)
- 分屏支持:多个听者共享一套扬声器;
- 物理驱动音频:碎片、可破坏物、布娃娃的撞击、滑动、滚动、破碎声;
- 动态音乐:随剧情情绪和紧张度实时变化;
- 角色对话:AI 角色彼此交谈、和玩家交谈时显得更真实;
- 声音合成:用正弦、方波、锯齿波等合成;更高级的有乐器合成、基于物理的声音合成(利用物理引擎的接触、动量、力、力矩、形变信息和材质、形状合成撞击、滑动、弯折声)、载具引擎合成、发声道模型语音合成(如免费工具 VocalTractLab)。GEA 指出《神秘海域》三部曲的载具追逐用了动态引擎建模,但严格说不是合成器,而是在多段预录声音之间交叉淡化;
- 人群建模:不能简单叠加大量人声,通常分成背景氛围层加个别发声。
分屏
多个听者在世界里,扬声器只有一套。对每个听者各平移一次再平均混合,结果并不总是合理:A 站在爆炸旁、B 离得很远,玩 B 的人照样听到很响的爆炸。没有完美解,只能混合:部分声音"物理正确",部分声音"糊弄"成最合理的体验(卷 II 15.6.1)。
为什么对话系统复杂
对话传达玩法信息、讲故事、建立情感,还决定玩家对 AI 智能的感知。Bungie 在 GDC 2002 的 Halo 演讲里提到:精英被杀后咕噜兽会逃跑,但测试玩家一直没看懂原因,直到给咕噜兽加了一句类似"头儿死了,快跑!"的台词(卷 II 15.6.2)。系统复杂的原因(卷 II 15.6.2.1):
- 要编目所有可能的台词并给每条唯一 id;
- 每个可识别的角色声音要一致,例如《最后生还者》匹兹堡段的猎人分配到 8 种声音之一,同一场战斗里没有两个猎人声音相同;
- 事先不知道哪个角色会说某句,所以同一句要由多位配音演员录多遍;
- 需要大量变体,从中随机选;
- 语音资产时长长、占内存,很多过场台词全游戏只说一次,所以通常流式播放;
- 角色的"用力声"(搬重物、跳跃、挨打)要和说话声一致,一般也由对话系统处理。
逻辑台词
大多数对话系统在"请求说话"和"选哪个音频片段"之间加一层间接:程序或策划请求逻辑台词,用字符串或更好的哈希字符串 id(见 字符串与唯一标识符)标识;音效设计师给每条逻辑台词填入一个或多个片段,提供不同声音和说法(卷 II 15.6.2.2)。顽皮狗的音效设计师用 Scheme 的自定义语法定义,结构大致如下(GEA 示例的简化):
(define-dialog-line 'line-out-of-ammo
(character 'drake (lines drk-out-of-ammo-01 drk-out-of-ammo-02 drk-out-of-ammo-03))
(character 'elena (lines eln-out-of-ammo-01 eln-out-of-ammo-02))
(character 'pirate-a (lines pira-out-of-ammo-01 pira-out-of-ammo-02))
;; ... pirate-h
)
更好的做法是按角色(并按关卡)拆分成多个文件:设计师互不冲突,内存也能按需加载,例如某段没有海盗就不加载海盗台词。
播放台词
系统按角色的声音 id 查表,再从候选里随机挑一个(卷 II 15.6.2.3)。避免重复:把候选索引放进数组随机打乱,按顺序依次使用;用完后重新打乱,并确保最近播放的那句不会排到新序列的第一位。GEA 说这样能"杜绝所有重复",准确地说是保证在一轮用完之前不重复、跨轮时也不会连播同一句,同时听起来随机(随机数见 伪随机数生成)。
API 要简单到"发出即忘":如果 AI 程序员或策划要绕很多弯才能让角色说话,角色会出奇地沉默。《神秘海域 3》的 C++ 代码在 AI 决策逻辑里直接调用 Npc::PlayDialog():
void Skill::OnEvent(const Event& evt)
{
Npc* pNpc = GetSelf();
switch (evt.GetMessage())
{
case SID("player-seen"):
pNpc->PlayDialog(SID("line-player-seen")); // 说一句台词
pNpc->MoveTo(GetClosestCover()); // 然后找最近的掩体
break;
// ...
}
}
优先级与打断
角色正在说话又被要求说话,或同一帧收到多条请求,用优先级裁决:比较当前台词和本帧请求中的最高优先级,当前的赢就继续,请求的更高(或角色没在说话)就播放新台词,必要时打断(卷 II 15.6.2.4)。打断本身不好做:对同一个人的语音做交叉淡化听起来很怪;理想是先播一个喉塞音,甚至一句表示惊讶或不满的短语。《最后生还者》并没做这些,只是停掉当前台词立即播放新的,大多数时候效果不错。
对话、对话打断与排他性
对话 (conversation) 由逻辑片段 (segment) 组成,每段对应一个角色说的一条逻辑台词,片段用唯一 id 通过 :next-seg 串起来。《神秘海域 4》和《失落的遗产》里,吉普车上的对话可以被打断(如玩家下车探索),回到车上后从断处继续(卷 II 15.6.2.5)。
逐角色应用优先级会出问题:A 问完话等 B 回答时,A "技术上没在说话",一条无关台词就能插进来("嘿,找到什么了吗?""没有,我找了一个小时……""看,有个亮东西!""……什么也没找到。")。《最后生还者》因此把对话作为一等实体:对话开始时系统知道所有参与者都在对话中,即使没轮到他们说;优先级按整个对话比较。同级或更低的插话被拒绝;更高优先级的("小心,他拿枪对着我们!")则打断整个对话的所有参与者(卷 II 15.6.2.6)。
排他性 (exclusivity):每条台词或对话标记为三种之一(卷 II 15.6.2.7):
| 级别 | 行为 | 例子 |
|---|---|---|
| 非排他 | 可以与其他台词重叠 | 两个搜索中的猎人各自嘟囔,互不交谈 |
| 阵营排他 | 打断本阵营所有台词 | 猎人喊"他在这儿!",其他猎人立刻闭嘴;但 Ellie 对 Joel 的耳语不被打断 |
| 全局排他 | 跨阵营打断所有台词 | 所有听得到的角色都应该有反应的情况 |
规则、分支与事实字典
编剧和音效设计师不仅想控制说什么,还想控制走哪条分支的逻辑条件,而不必经过程序员。顽皮狗的系统部分受 Valve 的启发(Elan Ruskin,GDC 2012《Rule Databases for Contextual Dialog and Game Logic》),两者细节不同但核心相似(卷 II 15.6.2.8)。
- 每个片段可以有多个备选,每个备选带一条规则;规则为真时选中。
- 规则由一个或多个判据 (criterion) 组成,多个判据之间是逻辑与;空规则相当于"否则"。例:生命值 < 25 说"流血了……需要医生",< 75 说"麻烦大了",否则说"好险"。
- 分支对话:Ellie 问"你没事吧?",若 Joel 最近没中弹则回答"没事",Ellie 接"把头低下!";若中弹则回答"(喘气) 不太好",Ellie 接"你在流血!"。
- 说话者与听者:片段的
:next-speaker 'listener让当前听者成为下一段的说话者,角色来回切换;判据里('speaker 'shot-recently)总是指当前说话者。对话定义与具体角色无关,同一份定义也能用于 Joel 问 Ellie;对敌人尤其必要,因为战斗闲聊是动态选两个角色触发的,事先不知道是谁。系统最多支持三名听者,但绝大多数对话只有两人。原书问题(前后不一致):同一段先说说话者/听者机制"可扩展到两人或三人对话",紧接着又说"最多支持三名听者"(加上说话者就是四人)。两句无法同时成立,原文没有说明以哪句为准(见卷 II 15.6.2.8 "Speaker and Listener" 末段)。
- 事实字典 (fact dictionary):判据中的
'health、'player-death-count等符号量存在键值表里,值是变体 (variant):像 C/C++ union 能存多种类型,但还记录当前类型,便于校验和转换(如把整数 42 取成浮点 42.0f)。每个角色、每个阵营各有一个字典,另有一个全局单例字典(游戏时长、当前关卡、某任务重试次数等)。 - 判据语法:可以显式指定字典,如
(('self 'health) > 5)、(('global 'seconds-playing) <= 23.5);不指定时按"角色 → 所属阵营 → 全局"的顺序查找,写起来简短,代价是清晰度降低。
下图是 GEA 描述的片段选择过程:对当前片段的各备选按顺序求规则,选中第一个为真的(GEA 示例里规则按从严到宽排列,空规则放最后作为"否则";"按顺序取第一个"是从示例推断的)。
flowchart TD
S["当前片段"] --> A1{"备选 1 规则: 全部判据为真?"}
A1 -->|"是"| P1["播放备选 1 台词"]
A1 -->|"否"| A2{"备选 2 规则"}
A2 -->|"是"| P2["播放备选 2 台词"]
A2 -->|"否"| E["空规则备选 = 否则"]
P1 --> N["按 :next-seg / :next-speaker 进入下一段"]
P2 --> N
E --> N情境对话
敌人要能"聪明地"报出玩家位置:"他在商店里!""他在那辆车后面!"。做法:音效设计师在世界里标注区域,每个区域带具体标签("柜台后""收银机旁")或一般标签("商店里""街上")。若玩家和 NPC 在同一个一般区域,用玩家的具体标签选台词("他在窗户边!");不在同一一般区域时退回玩家的一般标签("他在街上!抓住他!")。系统很简单,难在要录和配置的组合数量巨大,但游戏内效果值得(卷 II 15.6.2.9)。
对话动作
没有肢体语言的台词显得怪异。有些台词属于全身动画(如过场),但很多要在走、跑、开枪时说。《最后生还者》用叠加动画实现手势系统(见 动画混合),可以由 C++ 或脚本显式调用;每条台词还可以关联一段与音频时间线同步的脚本,在关键台词的精确时刻触发手势(卷 II 15.6.2.10)。
音乐系统
音乐系统的职责(卷 II 15.6.3):音乐几乎都太大放不进内存,要流式播放;提供变化;匹配游戏事件;在曲目间无缝过渡;与其他声音合理混合;可以被临时闪避以突出特定声音或对话;允许插入乐 (stinger) 临时打断或叠在当前曲目上;可以暂停和重新开始(不需要每一秒都有管弦乐)。
- 按情绪/紧张度的播放列表:每个列表对应一种紧张度或情绪,含一首或多首曲子,随机或顺序选择;战斗开始结束、动人过场进出时切换列表。有的游戏用一个紧张度递增的"栈":无敌人时平静、接近未察觉的敌人时紧张、首次接触时惊悚、战斗中快节奏。
- 插入乐:短的音乐或音效片段,临时打断当前曲目,或在压低主曲音量时叠加播放,例如首次看到新敌人时一声不祥的"隆隆",玩家死亡时快速切到一小段"死亡音乐"。
- 过渡:不能盲目交叉淡化两首无关的曲子,速度和节拍可能对不上。关键是把握过渡时机:速度不匹配时快速淡化,速度接近时可以长淡化;需要反复试验,连让一首曲子循环得好都需要音效工程师调整。
与 UE5 的对应
UE5 的 Quartz 提供与节拍对齐、精确到采样的调度(可用于在小节或拍上切换音乐),见 音频系统与 MetaSounds。"规则 + 事实"的选择思路与 AI 的决策结构可以对照 行为树与 StateTree(这是对比归纳)。
出处
- Game_Engine_Architecture_4th_Edtion_Volume_2_-_Jason_Gregory.pdf
- Jason Gregory《Game Engine Architecture》第 4 版(2026)卷 II 第 15 章 15.6 节