第 16 课
语音解说功能开发(上)
本课将在背景音乐系统上增加作品语音解说。你会导入 21 段音频,建立两个 Audio Source,制作不会阻挡移动的作品触发区,扩展 AudioManager 的声音决策,并使用碰撞体计数适配具有多个碰撞体的虚拟现实装置。
为作品加入自动语音解说
建立背景音乐与作品解说双播放器架构,让学生进入作品感应区时播放解说,并由 AudioManager 统一协调视频、解说和背景音乐。
学完你将能够
学习目标
- 说明背景音乐与作品解说为什么需要两个 Audio Source。
- 根据作品介绍表为作品选择正确的解说音频。
- 建立 AudioSource 父对象和两个子播放器的清楚结构。
- 创建不会阻挡学生移动的 Box Collider 触发区域。
- 说明 Is Trigger、Collider 和物理层在触发事件中的作用。
- 区分背景音乐播放意图与播放器实际状态。
- 根据 backgroundMusicShouldPlay 和 introIsActive 判断 BGM 状态。
- 使用作品候选列表处理相邻感应区重叠时的解说切换。
- 解释解说自然结束后如何恢复背景音乐。
- 说明 Update 为什么每帧检查解说状态,并读懂 ??= 的条件赋值。
- 使用新的 AudioManager 接口更新 VideoController。
- 读懂碰撞体计数和后置递增的执行顺序。
- 使用 Mathf.Max 防止碰撞体数量变成负数。
- 为圣彼得作品绑定音频并制作可复用预制体。
- 根据运行现象定位声音引用、触发事件和状态冲突。
开始操作前
准备工作
- 确认第 15 课的背景音乐能够自动并循环播放。
- 确认 Singleton.cs、AudioManager.cs 和 VideoController.cs 可以正常编译。
- 确认视频控制按钮可以开始和停止视频。
- 确认 XR Origin 可以在场景中正常移动并具有碰撞结构。
- 确认 Main Camera 上保留唯一启用的 Audio Listener。
- 保存 MainScene,并打开 Project、Hierarchy、Inspector、Scene、Game 和 Console 窗口。
本课学习路线
按照学习模块逐步完成
每个模块都包含理解、跟随操作和独立实践。让交互脚本提交请求,由 AudioManager 决定结果
理解双播放器与统一声音决策
背景音乐一直循环,作品解说按学生位置动态切换,两者具有完全不同的播放规则。使用两个播放器可以保持职责清楚,再由 AudioManager 统一处理声音优先级。背景音乐与作品解说的差异
两个声音内容和播放条件不同,不应反复在同一个 Audio Source 中替换。
左右滑动表格,可查看完整内容
| 对比项 | 背景音乐 | 作品解说 |
|---|---|---|
| 音频内容 | 固定为 BGM | 根据当前作品动态切换 |
| 进入场景时 | 自动播放 | 不自动播放 |
| 播放到结尾 | 循环 | 不循环 |
| 播放条件 | 没有更重要声音占用 | 学生进入作品感应区 |
| 暂停后 | 从原位置继续 | 再次进入同一作品时继续 |
从上到下阅读:VideoController 与 ArtWorkIntroController 只提交状态请求;AudioManager 保存状态并统一决定 BGM;两个 Audio Source 分别输出背景音乐和作品解说。
手机上可左右滑动;点击图片可以查看完整图声音架构的三层职责
请求、决策和播放分开后,一个功能结束时不会错误覆盖另一个功能的声音需求。
左右滑动表格,可查看完整内容
| 层级 | 对象 | 职责 |
|---|---|---|
| 请求来源 | VideoController | 说明视频是否允许 BGM 播放 |
| 请求来源 | ArtWorkIntroController | 说明进入或离开哪件作品的区域 |
| 统一决策 | AudioManager | 综合视频与解说状态 |
| 声音输出 | BGAudioSource | 实际播放背景音乐 |
| 声音输出 | IntroAudioSource | 实际播放当前作品解说 |
| 声音接收 | Audio Listener | 从学生视角接收最终声音 |
解说结束不等于背景音乐一定恢复
如果视频仍在播放,作品解说结束只代表一个暂停原因已经解除,视频对 BGM 的限制仍然存在。
因此作品触发脚本不直接恢复背景音乐。它只更新解说状态,再由 AudioManager 同时判断视频是否允许、解说是否活跃。
说明一条完整声音链路
对照架构图,从进入作品区域一直说到学生听到解说。
- 指出 ArtWorkIntroController 发出的请求。
- 指出 AudioManager 保存和判断的状态。
- 指出 IntroAudioSource 负责实际播放。
- 指出 Main Camera 上的 Audio Listener 负责接收。
- 说明解说结束后为什么还要判断视频状态。
先识别作品,再绑定同名解说
导入作品解说并确认对应关系
场景对象名称不一定能说明作品内容。绑定前应先根据作品画面确认标题,再使用作品介绍表找到对应音频,避免把场景序号当作音频顺序。下载 21 段作品解说音频
解压后把 ArtworkIntroAudio 文件夹整体放入 Assets/_Audio。每段音频都以作品名称命名。
- 包含 21 段 MP3 解说
- 保留 ArtworkIntroAudio 文件夹结构
- 用于全部作品的触发播放
下载作品介绍表
表格包含作品标题、介绍内容和解说音频文件名。本课主要使用标题与解说音频两列。
- 用于识别作品与音频对应关系
- 不要根据 Painting7 等场景编号猜测
- 外文完整标题与简化音频名已经对应
试听圣彼得解说
这是本课第一个完整配置的作品音频,可以先试听声音是否清楚,再导入 Unity。
- 对应作品:圣彼得
- 用于第一个作品感应区
- 后续绑定到 Intro Clip
把 ArtworkIntroAudio 放入 _Audio
- 01
解压 ArtworkIntroAudio.zip。
- 02
把整个 ArtworkIntroAudio 文件夹拖入 Assets/_Audio。
- 03
等待 Unity 完成导入。
- 04
展开文件夹,确认能够看到 21 段 MP3。
- 05
随机选择几段音频,在 Inspector 底部试听。
Unity 物体、作品与解说音频对应表
先按展厅找到 Unity 物体,再核对作品名称和音频文件名。
左右滑动表格,可查看完整内容
| 展厅 | Unity 物品名 | 作品名称 | 对应介绍音频 |
|---|---|---|---|
| RoomA | MexicoJug | 小罐子 | 小罐子.mp3 |
| RoomA | TheThinker | 思想者 | 思想者.mp3 |
| RoomA | BustJanMatejko | 扬·马特伊科半身像 | 扬·马特伊科半身像.mp3 |
| RoomA | AmoghasiddhiBuddha | 不空成就佛 | 不空成就佛.mp3 |
| RoomA | LoewenfeldRaBust | 女性半身像 | 女性半身像.mp3 |
| RoomA | FangjiaWineVessel | 方斝酒器 | 方斝酒器.mp3 |
| RoomA | RosehipVase | 玫瑰果酱药剂瓶 | 玫瑰果酱药剂瓶.mp3 |
| RoomA | AphroditeMilos | 米洛的维纳斯 | 米洛的维纳斯.mp3 |
| RoomB | Painting0 | 蒙娜丽莎 | 蒙娜丽莎.mp3 |
| RoomB | Painting1 | 持灯老妇 | 持灯老妇.mp3 |
| RoomB | Painting2 | 微笑少年 | 微笑少年.mp3 |
| RoomB | Painting3 | 基督与使徒 | 基督与使徒.mp3 |
| RoomB | Painting4 | 圣母子 | 圣母子.mp3 |
| RoomB | Painting5 | 圣母与天使 | 圣母与天使.mp3 |
| RoomB | Painting6 | 自画像 | 自画像.mp3 |
| RoomB | Painting7 | 圣彼得 | 圣彼得.mp3 |
| RoomB | Painting8 | 画室中的艺术家 | 画室中的艺术家.mp3 |
| RoomB | Painting9 | 冬日河畔景色 | 冬日河畔景色.mp3 |
| RoomB | Painting10 | 乡村风景 | 乡村风景.mp3 |
| RoomB | Painting11 | 湖畔水鸟 | 湖畔水鸟.mp3 |
| RoomB | Painting12 | 英国皇家战舰 | 英国皇家战舰.mp3 |
按展厅和 Unity 物体名查找
对应表已经给出 RoomA、RoomB 中每个 Unity 物体的正式绑定关系。配置时先确认当前展厅,再在 Hierarchy(层级面板)中找到对应物体。
找到物体后仍要对照画面确认作品名称,最后绑定同一行的音频文件。这样可以同时避免选错场景对象和选错解说。
完成三组物体对应练习
在 Hierarchy(层级面板)中找到 Unity 物体,再根据表格确认作品与音频。
- RoomA 的 MexicoJug 对应小罐子和小罐子.mp3。
- RoomB 的 Painting7 对应圣彼得和圣彼得.mp3。
- RoomB 的 Painting12 对应英国皇家战舰和英国皇家战舰.mp3。
- 试听三段音频,确认文件可以正常播放。
先记录声音请求,再统一改变播放器
理解两个播放器引用与两个状态
新的 AudioManager 不再只切换一个 BGM 开关。它同时记录视频是否允许 BGM、作品解说是否活跃,再根据两个原因决定最终播放状态。下载本课完整 AudioManager.cs
本课需要用这份脚本替换第 15 课的 AudioManager.cs。下面六段主线代码按页面顺序连接后,与下载文件完全一致。
- 同时管理背景音乐与作品解说
- 根据两个状态统一决定 BGM
- 解说自然结束后自动更新状态
- 感应区重叠时保留仍可播放的作品
using System.Collections.Generic;
using UnityEngine;
public class AudioManager : Singleton<AudioManager>
{
[SerializeField] private AudioSource musicSource;
[SerializeField] private AudioSource introSource;
private bool backgroundMusicShouldPlay = true;
private bool introIsActive;
private readonly List<AudioClip> activeIntroClips = new();
protected override void Awake()
{
base.Awake();
musicSource ??= GetComponent<AudioSource>();
}
private void Update()
{
if (introIsActive && (introSource == null || !introSource.isPlaying))
{
if (introSource != null)
{
activeIntroClips.Remove(introSource.clip);
}
introIsActive = false;
ApplyBackgroundMusicState();
}
}musicSource 与 introSource 分别指向两个播放器;backgroundMusicShouldPlay 记录播放意图;introIsActive 记录解说是否占用声音;activeIntroClips 保存仍满足播放条件的作品音频。
本课第一次把持续检查写进 Update
第 14 课已经认识一帧和 Update:只要 AudioManager 组件保持启用,Unity 就会在每一帧调用一次 Update。这里确实需要持续观察 IntroAudioSource,因为 Audio Source 没有像 Video Player 那样直接提供本课所需的播放结束事件。
每帧检查的内容很少:只有程序记录解说活跃、但播放器已经不再播放时,才移除已经播完的音频,把 introIsActive 改为 false,并重新决定背景音乐。Update 不会每帧重新播放音频,也不会反复重置播放位置。
帧率会随设备性能变化,因此不能通过“数多少帧”计算固定秒数。本课只在每帧读取当前状态,不把帧数当作计时器。
按运行顺序读懂 AudioManager 的开头
Awake 只负责初始化;Update 在之后的每一帧负责发现解说自然结束。
左右滑动表格,可查看完整内容
| 阶段 | 关键代码 | 运行结果 |
|---|---|---|
| 组件初始化 | base.Awake() | 先执行第 15 课完成的单例登记 |
| 组件初始化 | musicSource ??= GetComponent<AudioSource>() | 只有 musicSource 为空时,才从当前对象取得 Audio Source |
| 每一帧 | introIsActive && (...) | 只有程序认为解说活跃时,才继续确认播放器状态 |
| 发现自然结束 | Remove 当前音频,再把 introIsActive 设为 false | 移除已经播完的候选,并清除声音占用记录 |
| 重新决策 | ApplyBackgroundMusicState() | 根据视频请求和解说状态决定 BGM 是否恢复 |
??= 表示为空时才赋值
??= 表示“左边为空时才赋值”。两种写法执行结果相同,先读完整写法,再记住简写。
左右滑动表格,可查看完整内容
| 写法 | 等价含义 | 什么时候查找组件 |
|---|---|---|
| musicSource ??= GetComponent<AudioSource>(); | if (musicSource == null) { musicSource = GetComponent<AudioSource>(); } | 只有 musicSource 为空时 |
| Inspector 已经指定 musicSource | if 条件为 false,跳过大括号内的赋值 | 不会执行 GetComponent,也不会覆盖已有引用 |
五个字段回答五个问题
播放意图和实际播放状态不是同一个概念,不能只看 isPlaying。
左右滑动表格,可查看完整内容
| 字段 | 它回答的问题 | 类型 |
|---|---|---|
| musicSource | 哪一个播放器负责 BGM | 场景引用 |
| introSource | 哪一个播放器负责作品解说 | 场景引用 |
| backgroundMusicShouldPlay | 视频等功能是否允许 BGM | 播放意图 |
| introIsActive | 作品解说是否占用声音 | 声音焦点状态 |
| activeIntroClips | 哪些作品目前仍满足解说条件 | List<AudioClip>:只保存 AudioClip 的动态列表 |
List<AudioClip> 是会随进出变化的音频列表
List 表示数量可以增加或减少的列表。尖括号里的 AudioClip 规定这个列表只能保存音频资源,不能误放 GameObject 或字符串;这种用尖括号限定元素类型的写法称为泛型。
readonly 表示 activeIntroClips 这个字段始终指向同一个列表,但仍然可以使用 Add 增加元素、使用 Remove 移除元素。每件作品进入可播放状态时登记,离开时撤销登记。
背景音乐决策真值表
只有视频允许并且没有作品解说时,BGM 才能播放。
左右滑动表格,可查看完整内容
| BGM 播放意图 | 解说活跃 | BGM 结果 | 原因 |
|---|---|---|---|
| true | false | 播放或继续 | 两个条件都允许 |
| true | true | 暂停 | 作品解说优先 |
| false | false | 暂停 | 视频占用声音 |
| false | true | 暂停 | 视频与解说都不允许 BGM |
Update 只处理解说自然结束
当程序认为解说活跃,但 IntroAudioSource 已经不再播放时,通常表示解说自然结束。Update 先把已经播完的音频从候选列表移除,再把 introIsActive 改为 false,并重新计算 BGM。
主动离开作品区域时,PauseArtWorkIntro 会立即更新候选列表并重新选择;因此下一帧不会把主动切换误认为自然结束。
完成四种状态推演
不运行代码,先根据真值表预测每组状态下 BGM 是否播放。
- 视频未播放、没有解说:BGM 播放。
- 视频未播放、解说活跃:BGM 暂停。
- 视频播放、没有解说:BGM 暂停。
- 视频播放、解说活跃:BGM 暂停。
- 能够说明 isPlaying 为什么不能记录暂停原因。
最近进入的有效作品优先,离开后回到仍有效的作品
登记可播放作品并统一选择当前解说
一个共享 IntroAudioSource 负责全部作品。每件作品只提交“我现在可以播放”或“我已经离开”的请求,AudioManager 再从候选列表中统一选择最后登记的一件。 public void PlayBackgroundMusic()
{
backgroundMusicShouldPlay = true;
ApplyBackgroundMusicState();
}
public void PauseBackgroundMusic()
{
backgroundMusicShouldPlay = false;
ApplyBackgroundMusicState();
}PlayBackgroundMusic 与 PauseBackgroundMusic 先记录视频等功能的播放意图,再调用统一决策方法。
public void PlayArtWorkIntro(AudioClip introClip)
{
if (introSource == null || introClip == null)
{
return;
}
if (!activeIntroClips.Contains(introClip))
{
activeIntroClips.Add(introClip);
}
ApplyActiveIntroState();
}参数有效后,Contains 先确认列表中是否已有这段音频;没有时才 Add,避免同一作品重复登记。随后统一调用 ApplyActiveIntroState 选择当前解说。
PlayArtWorkIntro 的三条路径
这个方法只维护候选列表,真正的播放器切换由统一选择方法完成。
左右滑动表格,可查看完整内容
| 当前情况 | 列表动作 | 下一步 |
|---|---|---|
| 播放器或传入音频为空 | 立即 return | 保持当前状态 |
| 音频尚未登记 | Add 到列表末尾 | 统一选择这件最近进入的作品 |
| 音频已经登记 | 不重复 Add | 保持现有候选顺序后重新确认状态 |
public void PauseArtWorkIntro(AudioClip introClip)
{
if (introClip == null)
{
return;
}
activeIntroClips.Remove(introClip);
ApplyActiveIntroState();
}PauseArtWorkIntro 不直接决定该暂停哪段音频。它先从列表中移除离开的作品,再交给统一选择方法处理当前解说。
private void ApplyActiveIntroState()
{
AudioClip nextIntroClip = activeIntroClips.Count > 0
? activeIntroClips[activeIntroClips.Count - 1]
: null;
if (introSource == null || nextIntroClip == null)
{
if (introSource != null && introSource.isPlaying)
{
introSource.Pause();
}
introIsActive = false;
ApplyBackgroundMusicState();
return;
}
if (introSource.clip != nextIntroClip)
{
introSource.clip = nextIntroClip;
introSource.Play();
}
else
{
introSource.UnPause();
if (!introSource.isPlaying)
{
introSource.Play();
}
}
introIsActive = true;
ApplyBackgroundMusicState();
}列表最后一项是最近进入且仍满足条件的作品。没有候选时暂停解说并恢复背景音乐判断;有候选时切换或继续对应音频。
候选列表完整处理两个方向的重叠退出
学生先进入作品 A,再进入作品 B,列表顺序是 A、B,播放器选择最后一项 B。随后离开 A,只移除 A,B 继续播放。
如果学生仍在 A 的区域内,只离开后来进入的 B,列表移除 B 后还保留 A,AudioManager 会重新选择 A。这样不会在两个区域之间留下无声状态。
推演三次进入与离开
用圣彼得和蒙娜丽莎两段音频推演共享播放器的行为。
- 第一次进入圣彼得:从头播放圣彼得。
- 离开圣彼得:暂停并保留位置。
- 再次进入圣彼得:从暂停位置继续。
- 圣彼得自然结束后再次进入:从头播放。
- 进入蒙娜丽莎后离开圣彼得:蒙娜丽莎不被暂停。
- 仍在圣彼得范围内进入蒙娜丽莎,再离开蒙娜丽莎:重新播放圣彼得解说。
任一暂停原因存在,BGM 都不能播放
统一决定背景音乐并更新视频接口
PlayBackgroundMusic 只表示视频已经允许 BGM,不代表一定立即发出声音。最终还要确认作品解说是否占用声音。 private void ApplyBackgroundMusicState()
{
if (musicSource == null)
{
return;
}
if (!backgroundMusicShouldPlay || introIsActive)
{
if (musicSource.isPlaying)
{
musicSource.Pause();
}
return;
}
if (musicSource.isPlaying)
{
return;
}
musicSource.UnPause();
if (!musicSource.isPlaying)
{
musicSource.Play();
}
}
}任一暂停原因存在就 Pause;没有暂停原因时优先 UnPause,从未播放或已停止时再用 Play 补充。
统一决策方法的执行顺序
先处理不能播放的原因,再处理继续或重新开始,避免多个请求互相覆盖。
左右滑动表格,可查看完整内容
| 顺序 | 判断 | 动作 |
|---|---|---|
| 1 | musicSource 为空 | 立即结束 |
| 2 | 视频不允许或解说活跃 | 正在播放则 Pause,然后结束 |
| 3 | BGM 已经播放 | 不重复操作 |
| 4 | 可能处于暂停 | 先尝试 UnPause |
| 5 | 仍然没有播放 | 调用 Play 从头开始 |
AudioManager.Instance.PauseBackgroundMusic();在现有 PlayVideo() 方法的大括号内,把这一行插入为第一条可执行语句,用它替换上一课的 ToggleBGMusic(false)。不要复制新的方法声明,也不要覆盖后面的原有播放和幕布代码。
AudioManager.Instance.PlayBackgroundMusic();在现有 StopVideo() 方法的大括号内,把这一行插入为第一条可执行语句。PlayBackgroundMusic 表示视频已经允许 BGM;如果解说仍活跃,统一决策仍会保持 BGM 暂停。
AudioManager.Instance.PlayBackgroundMusic();在现有 OnVideoError(...) 方法的大括号内,把这一行插入为第一条可执行语句。错误路径与正常停止一样解除视频限制,其余错误恢复代码继续保留。
三个声音冲突场景
按照状态变化推演,避免把方法名理解为无条件播放。
左右滑动表格,可查看完整内容
| 场景 | 状态变化 | BGM 结果 |
|---|---|---|
| 只有作品解说 | introIsActive 由 true 变 false | 解说结束后继续 |
| 视频与解说同时存在 | 解说结束但播放意图仍为 false | 继续暂停 |
| 视频停止但解说仍活跃 | 播放意图变 true,解说仍为 true | 继续暂停,等待解说结束 |
更新 VideoController 并推演冲突
只替换三个现有方法开头的音乐调用,不复制方法外壳,不删除原有视频逻辑;再按顺序模拟视频和作品解说同时出现。
- PlayVideo 调用 PauseBackgroundMusic。
- StopVideo 调用 PlayBackgroundMusic。
- OnVideoError 调用 PlayBackgroundMusic。
- 原有视频、幕布和按钮逻辑保持完整。
- 能够解释视频停止而解说仍播放时,BGM 为什么继续暂停。
父对象统一管理,两个子对象分别播放
建立双 Audio Source 父子结构
AudioManager 放在根层级父对象上,背景音乐和作品解说分别使用两个子对象。这样可以一起跨场景保留,又不会把两类声音混在同一个播放器中。创建 AudioSource 父对象
- 01
在 Hierarchy 根层级创建空对象,命名为 AudioSource。
- 02
把上一课的 BGAudioSource 拖到 AudioSource 下面。
- 03
把 AudioManager 移动或重新挂载到父对象 AudioSource。
- 04
在父对象下新建子对象 IntroAudioSource。
- 05
为 IntroAudioSource 添加 Audio Source。
- 06
确认父对象本身没有第三个 Audio Source。
两个播放器的设置
BGM 保持自动循环,解说由脚本动态指定并且只播放一次。
左右滑动表格,可查看完整内容
| 属性 | BGAudioSource | IntroAudioSource |
|---|---|---|
| Audio Clip | BGM | None,运行时指定 |
| Play On Awake | 开启 | 关闭 |
| Loop | 开启 | 关闭 |
| Volume | 0.8 | 1,可根据听感调整 |
| Spatial Blend | 0(二维) | 0(二维) |
指定 Music Source 与 Intro Source
- 01
选中父对象 AudioSource。
- 02
把 BGAudioSource 拖到 Audio Manager 的 Music Source。
- 03
把 IntroAudioSource 拖到 Intro Source。
- 04
确认两个字段显示的是不同 Audio Source。
- 05
保存 MainScene。

重点观察 Hierarchy 中的父子结构,以及 Inspector 中 Music Source 与 Intro Source 分别指向两个子对象。
点击图片可以查看完整截图当前结构必须手动指定两个字段
GetComponent<AudioSource>() 只在 AudioManager 当前所在的父对象上寻找,不会自动进入子对象查找。
父对象只负责管理,两个 Audio Source 都位于子对象,因此 Music Source 和 Intro Source 都必须在 Inspector 中明确指定。
完成声音父子结构
把父对象、两个播放器和 AudioManager 配置成职责清楚的结构。
- AudioSource 位于场景根层级。
- AudioManager 位于父对象上。
- BGAudioSource 自动、循环播放 BGM。
- IntroAudioSource 不自动、不循环,Audio Clip 为空。
- Music Source 与 Intro Source 均已正确指定。
- 父对象本身没有多余 Audio Source。
使用 Trigger 报告进入和离开
创建不会阻挡移动的作品感应区
作品前方需要一个看不见的三维区域。学生进入时播放解说,离开时暂停;区域只能报告重叠事件,不能形成看不见的墙。在圣彼得作品前放置 Box Collider
- 01
在 Hierarchy 中创建空对象,命名为 ArtWorkIntro_圣彼得。
- 02
把对象移动到圣彼得作品正前方。
- 03
添加 Box Collider。
- 04
开启 Is Trigger。
- 05
点击 Edit Collider,调整 Center 和 Size。
- 06
让区域覆盖舒适观看位置,同时避开主要通道和相邻作品。

观察绿色边界:区域覆盖作品前方的停留位置,但不要延伸到主要通道,也不要与相邻作品区域大面积重叠。
点击图片可以查看完整截图普通碰撞体与触发器的区别
作品感应区关心进入和离开事件,不应该阻挡学生。
左右滑动表格,可查看完整内容
| 设置 | 学生能否穿过 | 是否报告重叠事件 | 适合用途 |
|---|---|---|---|
| Is Trigger 关闭 | 可能被阻挡 | 主要形成实体碰撞 | 墙面、地面、展台 |
| Is Trigger 开启 | 可以穿过 | 报告进入和离开 | 解说区、到达区、自动门 |
触发区尺寸的调整目标
感应区过小会频繁离开,过大又会在路过时误播放。
左右滑动表格,可查看完整内容
| 方向 | 调整目标 | 过度扩大的影响 |
|---|---|---|
| 宽度 | 略宽于作品或预期站位 | 覆盖相邻作品 |
| 高度 | 覆盖头部和身体可能经过的高度 | 捕获无关物体 |
| 深度 | 覆盖舒适观看距离 | 经过通道也会触发 |
| 中心 | 位于作品正前方 | 学生未关注作品就播放 |
触发事件还需要物理条件
两侧需要 Collider,其中一个开启 Is Trigger;参与对象还要具有能够产生三维物理事件的移动或刚体结构,并且物理层允许相互接触。
本课脚本不依赖 Player 标签。若可抓取物体也会进入区域,应使用专用物理层和层碰撞矩阵,只允许学生装置触发作品感应区。
完成圣彼得感应区域
在 Scene 视图中调整区域,并用学生移动路径判断尺寸是否合理。
- Box Collider 已启用且 Is Trigger 已开启。
- 区域覆盖舒适观看位置。
- 区域不会形成看不见的墙。
- 区域不占据过多通道。
- 区域没有与相邻作品大面积重叠。
第一个进入时播放,最后一个离开时暂停
用碰撞体计数控制作品触发
虚拟现实装置可能包含头部、身体和手部等多个碰撞体。简单布尔值会在某一个碰撞体离开时过早暂停,计数能够判断整个学生装置是否仍有部分位于区域内。下载完整 ArtWorkIntroController.cs
把脚本放入 Assets/_Scripts。下面三段代码按顺序连接后,与下载文件完全一致。
- 要求对象具有 Collider
- 使用进入与离开事件
- 使用计数适配多个 XR 碰撞体
- 对象停用时撤销残留的解说请求
using UnityEngine;
[RequireComponent(typeof(Collider))]
public class ArtWorkIntroController : MonoBehaviour
{
[SerializeField] private AudioClip introClip;
private int collidersInside;
private AudioManager audioManager;
private void Start()
{
audioManager = AudioManager.Instance;
}每个感应区保存自己的 introClip;collidersInside 记录仍在区域内的碰撞体数量;Start 取得场景中的统一 AudioManager,后续事件都通过这个引用提交请求。
沿数字变化阅读:0→1 时播放,1→2 不重复;2→1 不暂停,1→0 时才暂停。
手机上可左右滑动;点击图片可以查看完整图进入与离开方法由 Unity 自动调用
这两个方法不需要其他脚本主动调用。物理系统发现碰撞体与触发区开始或结束重叠时,会自动进入对应方法。
左右滑动表格,可查看完整内容
| 方法或参数 | 谁提供 | 表示什么 |
|---|---|---|
| OnTriggerEnter(Collider other) | Unity 物理系统自动调用 | 另一个碰撞体刚刚进入当前触发区 |
| OnTriggerExit(Collider other) | Unity 物理系统自动调用 | 另一个碰撞体刚刚离开当前触发区 |
| other | Unity 调用方法时自动传入 | 指向刚进入或离开的那个 Collider;本课只需要计数,所以暂时不读取它的其他信息 |
private void OnTriggerEnter(Collider other)
{
if (!isActiveAndEnabled)
{
return;
}
if (collidersInside++ == 0)
{
audioManager.PlayArtWorkIntro(introClip);
}
}Unity 检测到重叠开始后自动调用 OnTriggerEnter,并把刚进入的 Collider 作为 other 传入。isActiveAndEnabled 是 MonoBehaviour 提供的只读状态,只有游戏对象处于活动状态且脚本已启用时才为 true;停用时用 return 忽略可能到达的物理通知。随后 collidersInside++ 先使用旧值比较,再把数量加 1;只有旧值为 0 的第一次进入会调用播放。
后置递增的执行顺序
比较使用旧值,变量随后仍会加 1。
左右滑动表格,可查看完整内容
| 进入次数 | 比较时旧值 | 比较结果 | 进入后数量 | 声音动作 |
|---|---|---|---|---|
| 第一个碰撞体 | 0 | 0 == 0 为 true | 1 | 开始解说 |
| 第二个碰撞体 | 1 | 1 == 0 为 false | 2 | 不重复播放 |
private void OnTriggerExit(Collider other)
{
if (!isActiveAndEnabled)
{
return;
}
collidersInside = Mathf.Max(0, collidersInside - 1);
if (collidersInside == 0)
{
audioManager.PauseArtWorkIntro(introClip);
}
}
private void OnDisable()
{
if (audioManager != null)
{
audioManager.PauseArtWorkIntro(introClip);
}
collidersInside = 0;
}
private void Reset()
{
GetComponent<Collider>().isTrigger = true;
}
}脚本启用时,Unity 检测到重叠结束会调用 OnTriggerExit。Mathf.Max 保证数量不低于 0;OnDisable 在对象停用时撤销请求并清零计数,isActiveAndEnabled 保护又会阻止停用后的物理通知重新登记;文件末尾的 Reset 只帮助编辑阶段完成初始配置。
两个碰撞体完整经过区域
只有数量到达边界 1 或 0 时才改变解说状态。
左右滑动表格,可查看完整内容
| 事件 | 数量变化 | 声音动作 |
|---|---|---|
| 第一个进入 | 0 → 1 | 播放解说 |
| 第二个进入 | 1 → 2 | 保持播放 |
| 第一个离开 | 2 → 1 | 保持播放 |
| 最后一个离开 | 1 → 0 | 暂停解说 |
模拟多个碰撞体经过
用纸笔记录 0→1→2→1→0,并在每一步写出声音动作。
- 能够解释 ++ 写在变量后面的执行顺序。
- 能够说明为什么第二个进入不重新播放。
- 能够说明为什么第一个离开不暂停。
- 能够说明 Mathf.Max 如何避免负数。
- 能够说明 Reset 只在编辑阶段帮助开启 Is Trigger,不在运行中逐帧执行。
先完成一个可靠实例,再复制到其他作品
绑定圣彼得并制作可复用预制体
第一个感应区应完整完成脚本、音频、运行观察和尺寸调整。确认可靠后再制作预制体,避免把错误配置批量复制到全部作品。把圣彼得音频指定给触发区
- 01
选中 ArtWorkIntro_圣彼得。
- 02
确认同对象具有 Box Collider,且 Is Trigger 已开启。
- 03
添加 ArtWorkIntroController。
- 04
打开 Assets/_Audio/ArtworkIntroAudio。
- 05
把圣彼得.mp3 拖到 Intro Clip。
- 06
保存 MainScene。

确认 Is Trigger 已开启,Art Work Intro Controller 与碰撞体位于同一对象,Intro Clip 指向圣彼得。
点击图片可以查看完整截图观察进入、离开、继续与自然结束
- 01
进入运行状态,确认 BGM 正常播放。
- 02
缓慢进入圣彼得感应区,确认 BGM 暂停、解说开始。
- 03
在区域内轻微移动,确认解说不会重新开始。
- 04
离开区域,确认解说暂停、BGM 恢复。
- 05
再次进入,确认解说从暂停位置继续。
- 06
等待解说自然结束,确认没有视频时 BGM 自动恢复。
把正确感应区保存为 ArtWorkIntro
- 01
在 Assets 下创建 _Prefabs 文件夹。
- 02
确认圣彼得感应区的位置、尺寸、Is Trigger 和脚本都正确。
- 03
把 ArtWorkIntro_圣彼得 从 Hierarchy 拖入 _Prefabs。
- 04
把预制体命名为 ArtWorkIntro。
- 05
拖入新实例时重新设置作品名称、位置、碰撞体尺寸和 Intro Clip。
预制体保存共同结构,实例保存作品差异
所有作品共用一个 AudioManager,不要为每个实例创建新的声音管理器。
左右滑动表格,可查看完整内容
| 由预制体保存 | 每件作品重新设置 |
|---|---|
| Box Collider 组件 | 场景位置 |
| Is Trigger 开启 | 触发区中心与尺寸 |
| ArtWorkIntroController | 清楚的对象名称 |
| 碰撞体计数逻辑 | 对应的 Intro Clip |
本课先完成可靠的距离触发
当前条件只判断学生是否进入区域,因此路过或背对作品时也可能播放。这不是触发器失效,而是目前只有距离条件。
下一课会在可靠的距离触发基础上继续加入观看方向等条件,让解说更接近学生真正关注作品时才开始。
创建三个作品实例
完成圣彼得后,再为蒙娜丽莎和思想者建立感应区。
- 三个实例名称都包含作品标题。
- 三个感应区都位于作品正前方。
- 三个碰撞体尺寸都适合各自作品。
- 三个 Intro Clip 均与作品对应。
- 区域之间没有大面积重叠。
- 所有实例共用同一个 AudioManager。
从触发事件、音频引用和声音状态逐层定位
综合验证并定位语音解说问题
完整功能同时涉及物理触发、共享解说播放器、视频请求和背景音乐决策。根据最早出现的异常现象回到对应层,不要一次改变多个设置。按顺序组合视频与作品解说
- 01
进入场景,确认 BGM 播放。
- 02
播放视频,确认 BGM 暂停。
- 03
视频仍在播放时进入作品区,确认解说开始、BGM 仍暂停。
- 04
离开作品区,确认解说暂停,但 BGM 因视频仍在播放而不恢复。
- 05
停止视频,确认没有解说占用后 BGM 恢复。
- 06
再测试解说自然结束与视频播放失败两条路径。
- 07
进入相邻作品 A,再进入 B;保持仍在 A 的范围内离开 B,确认系统重新选择 A。
常见现象与处理方法
先确认现象属于触发、引用、音频切换还是统一状态,再处理对应原因。
左右滑动表格,可查看完整内容
| 现象 | 优先查看 | 处理方法 |
|---|---|---|
| 进入区域没有反应 | Collider、Is Trigger、脚本、Intro Clip、物理层 | 确认触发条件完整并允许两层接触 |
| 学生被看不见的墙挡住 | Is Trigger | 开启 Is Trigger |
| BGM 正常但解说无声 | Intro Source、Volume、Mute、Listener | 指定正确播放器并保持可听设置 |
| 播放了错误作品 | 作品画面、对应表、Intro Clip | 重新识别作品并绑定同名音频 |
| 进入区域后 BGM 仍播放 | introIsActive 与 Music Source | 先更新状态,再调用统一决策 |
| 离开区域后 BGM 不恢复 | 计数、视频状态、Music Source | 先判断是否仍有碰撞体或视频限制 |
| 自然结束后 BGM 不恢复 | Update、Intro Loop、视频状态 | 保留自然结束处理并关闭解说循环 |
| 再次进入总是从头播放 | 离开时是否使用 Pause | 使用 Pause 保留位置,不使用 Stop |
| 结束后再次进入没有声音 | UnPause 后的 Play 补充 | 自然结束时需要 Play 从头开始 |
| 一个碰撞体离开就暂停 | collidersInside 计数 | 只在数量减到 0 时暂停 |
| 离开作品 B 后没有回到仍有效的 A | activeIntroClips 候选列表 | 确认进入时 Add、离开时 Remove,并由 ApplyActiveIntroState 重新选择 |
| 停用感应区后解说仍残留 | OnDisable | 停用时撤销该作品请求并把计数清零 |
| 拿起的物体也触发 | 物理层碰撞矩阵 | 只允许学生装置与感应区接触 |
| VideoController 找不到旧方法 | 上一课的 ToggleBGMusic | 改用 PauseBackgroundMusic 与 PlayBackgroundMusic |
本课成果清单
逐项完成后,语音解说、视频声音和背景音乐已经形成统一系统。
- ArtworkIntroAudio 中存在 21 段解说音频。
- 能够使用作品介绍表找到正确音频。
- AudioSource 父对象包含 BGAudioSource 与 IntroAudioSource。
- Music Source 和 Intro Source 均已正确指定。
- BGM 自动循环,作品解说不自动、不循环。
- AudioManager.cs 与 ArtWorkIntroController.cs 能够正常编译。
- VideoController 已使用新的背景音乐接口。
- 圣彼得感应区不会阻挡学生。
- 第一个碰撞体进入时解说开始。
- 最后一个碰撞体离开时解说暂停。
- 再次进入同一作品时从暂停位置继续。
- 新作品会切换为新音频,旧区域退出不会暂停它。
- 离开新作品后,仍满足条件的上一件作品会重新被选中。
- 解说自然结束后声音状态正确。
- 视频与解说同时存在时 BGM 保持正确状态。
- 已制作 ArtWorkIntro 预制体并完成至少三个实例。
- MainScene 已保存,Console 没有新增错误。
把知识变成作品
本课练习
请按顺序完成以下任务,并保存自己的学习成果。- 01
不用查看架构表,独立建立 AudioSource 父对象和两个子播放器,并完成两个引用。
- 02
使用真值表推演视频、解说和 BGM 的三种冲突场景。
- 03
用纸笔记录两个碰撞体依次进入和离开的 0→1→2→1→0 变化。
- 04
故意绑定错误 Intro Clip,再根据作品介绍表重新定位正确音频。
- 05
使用 ArtWorkIntro 预制体为圣彼得、蒙娜丽莎和思想者完成三个可运行实例。
把本课进度保存下来
登录后可以保存浏览内容、有效学习时间、完成状态和答题参与记录。