带配音的对白是现代电子游戏的重要组成部分,藉此玩家不仅可以将角色与特定声音联系起来,而且还可通过整体声调更好地了解角色的感受。随着复杂对白系统的出现,这一点得到了进一步加强。此类系统可根据玩家言行改变角色说话的内容或方式。
如果玩家之前帮助过某个角色,其可能会表现得比较友好;倘若玩家做过违背其原则的事,便可能会表现得更为敌对。这就是所谓的“动态对白”。
在此,我们将着重介绍如何构建以叙事为中心的对白并使用 Wwise 和 Unity 加以实现。Wwise 专门设有将动态对白整合到电子游戏中的功能,不过在此我们会使用其他 Wwise 系统来驱动对白系统。在本文中,我们主要探讨以下 3 个方面:
1. 通过 Sequence Container 实现的基本叙事,其不会随玩家所做决定变化。
2. Wwise 内嵌的本地化系统,方便开发人员制作多语言游戏。
3. 通过 Switch Container 实现的动态叙事,其会随着受玩家影响的变量变化。
在此,各位既可了解 Wwise 的 Sequence Container 和 Switch Container,还能学到更多与 Wwise 相关的 Unity C# 脚本编写知识(如 Event 发送、Event 回调、Switch/State 变化)。
Wwise – 通过 Sequence Container 实现的基本叙事及本地化系统
在此,我们要设计一段“指挥官向玩家下达命令”的配音。该命令将被分成 5 段不同的配音,我们会通过编程来让其逐段播放。
1. “Hello? Hello? Can you hear me?”
2. “Oh good. Listen, I need you to do something very, very important for me.”
3. “In front of you are these little... obstacles that I need you to overcome.”
4. “I just need you to jump over the hurdle, go under the archway, and to the green path. Not the red one. That would be bad.”
5. “If you could do that for me, that would greatly help us over here in corporate. Over and out.”
这段叙事在游戏开始的时候固定播放,无论顺序还是内容都不会有任何改动。为此,我们可以使用 Sequence Container 来按照原样逐段播放每条音轨。除此之外,我们还将探究 Wwise 的本地化系统,看看如何根据玩家的偏好来播放 English 或 Spanish 语言的音频。
首先,我们要转到 Audio 选项卡。接着,在 Actor-Mixer Hierarchy 下创建新的 Sequence Container (Ctrl + Shift + Alt + Q),并将其命名为 MissionBriefing。然后,在新建的 Sequence Container 下载入音频文件。为此,可右键单击 Sequence Container 并选择 Import Audio Files... (Shift + I),或者直接将音频文件从系统拖到 Sequence Container 中。
在 Audio File Importer 中,将 Import as: 字段由 Sound SFX 切换为 Sound Voice。然后,单击 Import。
Sound Voice 跟 Sound SFX 差不多,只不过其允许针对 Wwise 工程中所用的各种语言将多个音频文件导入其中。在更改语言设置后,所有 Sound Voice 都将换成对应语言的音频文件。
现在,我们导入了所有 English 音频文件。接下来,我们要允许向 Wwise 工程添加其他语言。为此,可转到顶栏中的 Project,然后选择 Languages... 或按下 Shift + J。通过 Language Manager,我们可以添加、移除和重命名语言,甚至更改对应语言的所有 Sound Voice 的 Make-Up Gain 属性。首先,我们来单击 Add... 按钮,并在弹出的方框中键入 Spanish。接着,在随后的警告提示窗口中,单击 OK 确认更改。
在此,无论选中哪个新建的 Sound Voice,Contents Editor 中都会显示与 English 和 Spanish 对应的分区。现在,我们已经设置好了 English 音频文件。接下来,我们把 Spanish 音频文件也导入进来。为此,可将其逐个拖到 Contents Editor 的 Spanish 字段中。对于 5 段叙事音频,要重复执行此操作。
每次播放这些 Sound Voice,都会听到默认语言的声音。若转到 Wwise 的左上角,将默认语言切换为 Spanish,然后再次播放,将听到前面拖到 Spanish 字段中的声音。由此可见,开发者只需随便操作一下,便可从某一语言切换为另一语言。
现在,我们来看看之前创建的 Sequence Container。在选中该容器后,查看 Contents Editor 及右侧的 Playlist 分区。在默认情况下,Sequence Container 并不会播放其所含的全部声音对象,而只会播放 Playlist 中的声音对象。为此,需严格按照预期播放顺序将各个 Sound Voice 从 Contents Editor 拖到 Playlist 中。
除非要编辑声音,目前这样就可以。接下来,我们要转到 Events 选项卡,并在 Default Work Unit 中新建 2 个 Event:
1. "Play_MissionBriefing" Event,带有 Play 动作,负责播放 Sequence Container。
2. "Reset_MissionBriefing" Event,带有 Reset Playlist 动作,负责将 Sequence Container 的播放列表重置到第一个声音对象。
注意:在此设置动态对白系统时,我们并未使用 Events 选项卡的 Dynamic Dialogue 分区。
第二个 Event 的作用在于确保每次播放叙事音频都会从 Sequence Container 的第一个声音对象开始播放。所以,其本质上是一项保障措施。
接下来,只需创建 SoundBank 并向其添加新建的 2 个 Event。为此,可在顶栏中转到 Layouts 下,并选择 SoundBank 布局或直接单击 F7。然后,在 SoundBank Manager 中选择 New 来创建新的 SoundBank。在此,我将其命名为 Main。
在 SoundBank Manager 中选中新建的 SoundBank,然后同时将 "Play_MissionBriefing" 和 "Reset_MissionBriefing" Event 从 Event Viewer 拖到 SoundBank Editor 中。
在 SoundBank Manager 中,选中新建的 SoundBank 以及对应的平台(Windows、Mac 等)和语言。最后,单击 Generate Selected 按钮。
Wwise – 动态对白
在收到指挥官下达的命令之后,玩家会有一些时间来完成任务。依据玩家的任务执行情况,指挥官会做出不同的反应。在此,我们要确定哪些变量会决定指挥官对玩家说什么。首先,我们来看下玩家所要实现的 3 个目标:
1. 翻越障碍。
2. 到拱门下。
3. 走绿色小道,而非红色小道。
这一任务有多种完成结果。具体来说,有以下 5 种:
1. 玩家什么都不做 (Fail, Fail, Fail)。
2. 玩家只翻越障碍 (Pass, Fail, Fail)。
3. 玩家翻越障碍,到拱门下 (Pass, Pass, Fail)。
4. 玩家翻越障碍,到拱门下,然后走红色小道 (Pass, Pass, Red)。
5. 玩家翻越障碍,到拱门下,然后走绿色小道 (Pass, Pass, Green)。
在此,我们要追踪 3 个变量:障碍、拱门、小道。这个时候就要用到 Switch 和 State 了。
如果仍处于 SoundBank 布局,请返回 Designer 布局(依次选择 Layouts -> Designer 或按下 F5)。
接下来,我们要转到 Project Explorer 的 Game Syncs 选项卡,然后在 Switches 分区的 Default Work Unit 中新建 3 个 Switch Group:Hurdle、Archway 和 Path。对于 "Hurdle" 和 "Archway" Switch Group,我们要新建 2 个 Switch:Fail 和 Pass。对于 "Path" Switch Group,我们要新建 3 个 Switch:Fail、Red 和 Green。
返回 Audio 选项卡。在 Actor-Mixer Hierarchy 下的 Default Work Unit 中创建新的 Switch Container (Ctrl + Shift + Alt + W)。在此,我将其命名为 MissionResult。
选中新建的 Switch Container,然后查看 Property Editor 右侧的 Switch 设置。对于此容器,首先我们要确认玩家有没有通过 Hurdle 考验。若没能通过,会听到指挥官表达失望的音频。若成功通过,将前往接受 Archway 考验。
为此,首先将 Switch Group 设为 Hurdle,然后将 Default Switch/State 设为 Fail。藉此,可向 Assigned Objects 分区添加两个 Switch:Pass 和 Fail。对此,我们需要使用合适的对象来加以填充。
首先,将 "hurdlefailed" Sound Voice 导入到 "MissionResult" Switch Container 中。然后,在该 Switch Container 内创建新的 Switch Container。在此,我将其命名为 Archway。
接下来,我们只需将两者从 Contents Editor 移到相应的 Assigned Objects 分区:"hurdlefailed" Sound Voice 对应 Fail,"Archway" Switch Container 对应 Pass。
接下来,我们要在 "Archway" Switch Container 内重复这一流程。将 Switch Group 设为 Archway,然后将 Default Switch/State 设为 Fail。将 "archwayfailed" Sound Voice 导入到 "Archway" Switch Container 中,然后创建新的 Switch Container 并命名为 Path。像之前一样将其插入到 Assigned Objects 分区中:"archwayfailed" Sound Voice 对应 Fail,"Path" Switch Container 对应 Pass。
接下来,我们要再重复一次这一流程。不过,这次有 3 个 Switch 而非 2 个。在 "Path" Switch Container 内,将 Switch Group 设为 Path,并将 Default Switch/State 设为 Fail。
接下来只需导入最后 3 个音频文件:"pathfailed"、"pathred" 和 "pathgreen" Sound Voice。将其全部插入到对应的 Assigned Objects 分区中。
我们来简单回顾一下前面都做了什么。倘若没能通过 Hurdle 考验,将不会看到其余 Switch Container;此时会直接播放表示“没能通过 Hurdle 考验”的消息。如果成功通过,将前往接受 Archway 考验。假如没能通过这一项考验,将不会看到 "Path" Switch Container;此时会播放表示“没能通过 Archway 考验”的消息。如果成功通过 Archway 考验,将前往最终的 "Path" Switch Container;在此会检查是走了红色小道、绿色小道还是哪条小道都没走。这 3 个条件都会指向特定的音频文件。
在完成所有这些之后,为之创建 Event 就很简单了。与其转到 Events 选项卡,我们选择通过 Audio 选项卡来创建 Event。为此,可直接右键单击 "MissionResult" Switch Container,然后转到 New Event 下选择 Play。这样会自动创建名为 Play_MissionResult 的 Play Event。
通过按下 F7 返回 SoundBank 布局,然后将新建的 "Play_MissionResult" Event 由 Event Viewer 拖到 SoundBank Editor 中。跟之前一样生成 SoundBank,并确保保存 Wwise 工程。
Unity – 整合通过 Sequence Container 实现的基本叙事
接下来,我们要整合在 Sequence Container 中构建的叙事音频。通常来说,这并不难;我们完全可以逐段播放 5 次叙事音频。但是,如何确定某一段落何时结束以便开始播放下一段落?假如我们想在各个段落之间添加短暂的延迟,该怎么办呢?要想实现以上两项操作,其中一种方法就是使用 Event 回调和协程函数。
为此,我们要加载 SoundBank。首先,选中层级结构下的 WwiseGlobal 对象。接着,在 Inspector 中选择 Add Component 并搜索 AkBank。然后,将其放到 WwiseGlobal 中。最后,从 Name: 列表中选择所需 SoundBank。
接下来,我们需要创建新的脚本来驱动叙事音频。为此,可在 Hierarchy 下通过右键单击并选择 Create Empty 来创建新的 Empty Object。在此,我将其命名为 Narration。不过,大家可以根据需要随意命名。接着,在 Inspector 中再次单击 Add Component。然后,在“搜索”字段中键入 Narration。最后,依次选择 New Script > Create and Add。此时会创建新的脚本并将其添加到主 Assets 文件夹。之后可通过 Project 选项卡选择新建的脚本。
此脚本的主要目的在于“在玩家入场的时候播放第一段叙事音频,待其播完后稍加延迟并播放下一段音频”。这一过程会一直重复,直到播完 Sequence Container 中的所有对象。
我们先来说说 Start 函数上方的 3 个成员变量,其中:
1. narrationEvent 为之前在 Wwise 中创建的 "Play_MissionBriefing" Event。之所以将其设为 public,是为了能通过 Inspector 设置 Event。我们将在完成脚本后执行这一操作。
2. player 游戏对象用于告知 Unity 听者在哪里。之所以将其设为 public,是为了能通过 Inspector 设置 player 游戏对象。我们将在完成脚本后执行这一操作。
3. narration 整数用于告知脚本已经播放了多少个音频文件。一旦该数值达到 5,脚本就知道不再播放任何音频文件了。
在完成这些设置之后,我们来稍微清理一下脚本并创建新的函数。删除 Update 函数,因为用不到它。在其对应位置,我们要新建 PlayNarration 函数。
在播放此函数内的叙事音频之前,我们需要检查脚本是否已经播完 Sequence Container 中的所有音频文件。对此,我们可以直接用 if 语句来实现。确保仅在之前播放不到 5 次的情况下播放此音频。就编程而言,我们要使用 narration 整数:
在 if 语句内,最后要通过发送 Event 来播放叙事音频。首先,添加以下代码:narrationEvent.Post(player)。
这时,我们将向玩家发送这一 Event。就本身而言,并没有问题。不过,还记得我们想在此完成之后发送另一 Event 吗?对此,我们可以使用 Event 回调来实现。下面来看看这一行脚本。
AkCallbackType 代表我们要结合此 Event 使用什么样的回调。回调的类型有好几种,大多指向音乐 Event。但是在此,我们想让回调追踪 Event 的结束时机。
我们可以看到 AkCallbackType 的开头添加有 (uint),其会将 AkCallbackType 的结果由某一数值转换为无符号整数。它只是为了满足函数的需要;每次创建 Callback 脚本都会用到。
函数的最后部分 (NarrationEnd) 用于告知 Unity 在该 Event 结束后要播放此函数内的声音对象。就目前而言,因为函数不存在,所以会在 NarrationEnd 下收到错误消息。那么,下面我们就来创建一个!
注意,NarrationEnd 对应括号中的所有对象都是必要的。这是因为 Event Callback 函数要向 NarrationEnd 发送相关信息(比如发送的 Event 以及所用 Callback 类型)。在此,我们并不会用到这些信息。但是,最好知道都有哪些信息,这样在必要时可以使用。
在继续设置 NarrationEnd 函数之前,我们需要对 PlayNarration 函数做最后的扩充。下面来直接在 narrationEvent.Post 之后添加以下代码行:
只要在数字变量之后添加 ++,其数值就会增加 1 个单位。
现在,我们解决了一项难题:发送 Event 并等待其结束以执行某些操作。接下来,我们需要在这一 Event 结束后发送下一叙事 Event 之前添加延迟。对此,我们可以使用协程函数来实现。在 NarrationEnd 函数之下,我们要创建新的函数:
- 利用 IEnumerator 函数(即协程函数)在执行函数前在后台计时。这对将函数延迟一段时间来说再合适不过了。
- 通过 yield return new WaitForSeconds(1f) 来告知 Unity 要等多长时间再执行 yield 后面的代码。在本例中,我们告知 Unity 等待 1 秒。
- 最后,循环回 PlayNarration() 函数。
我们将在 NarrationEnd 函数内调用这一新的协程函数。对此,可采用有别于大多数函数调用的方式来实现:
StartCoroutine(Wait());
在循环回 PlayNarration 函数时,Unity 会检查是否还有尚未播放的叙事音频。如有,则播放下一叙事音频,重新循环一遍。若无,则停止循环,以此正式结束整段叙事。
在完成设置之前,我们还有几件事要做。下面来将以下代码添加到 Start 函数:
这样会在启动 PlayNarration() 循环之前发送 "Reset" Event。
最后,我们需要返回 Unity,并按下图在 Inspector 中设置 Narration 脚本中新增的 Narration Event 和 Player 字段。
在保存进度后,按下 Play 并检查所作设置。若是没有获得预期结果,不妨将自己的脚本与以下脚本进行对比:
Unity – 整合任务简报
现在,玩家收到了任务简报。接下来,要继续完成任务。游戏需要确定玩家是否真的翻越了障碍、到了拱门下、走了绿色或红色小道。为此,我们接下来要为障碍、拱门和两条小道创建 Box Collider。
对此,我们可以通过创建 Cube 对象来实现。在层级结构下,通过右键单击并依次选择 3D Object -> Cube 来新建 4 个 Cube 对象。将其中一个 Cube 放到障碍之上,确保玩家肯定会与之发生碰撞,然后在拱门之下执行同样的操作。最后,将剩下 2 个 Cube 直接放到绿色和红色小道之上。在此,可使用 Move (W)、Rotate (E) 或 Scale (R) 工具来自由调节这些 Cube。
在每个 Cube 中,确保通过取消选中 Mesh Renderer 组件左侧的复选框来将其禁用。这样可以隐藏 Cube。接下来,转到 Box Collider 组件,并选中 Is Trigger 复选框。这样将允许玩家穿过 Cube 并以此触发 Event。
针对 "Hurdle" 和 "Archway" Cube,在 Inspector 中单击 Add Component,然后搜索并选中 AkSwitch 脚本。然后,按照同样的方式将 AkTriggerEnter 组件添加到这两个 Cube。
首先,在 AkSwitches 中将 TriggerOn 改为 AkTriggerEnter 并取消选中 Start。接着,选中 Use Other Object 复选框。在启用此复选框之后,便可指定由哪个对象触发 Switch 切换。在本例中,AkTriggerEnter 组件中的 Trigger Object 为触发对象。然后,在 AkTriggerEnter 中直接将 player 对象由 Hierarchy 拖到 Trigger Object 字段中。
最后,选择与各个对象对应的 Switch 名称:Hurdle / Pass 对应 Hurdle 对象,Archway / Pass 对应 Archway 对象。
最终结果应如下图所示:
每次玩家进入这些 Cube,对应的 AkSwitch 都会由 Fail 切换为 Pass。只有在玩家穿过这些 Trigger 对象时才会发生这种情况,因为我们将 player 设为了 AkTriggerEnter 组件中的唯一触发对象。
接下来,我们继续设置 "Red" 和 "Green" Cube。我们要对这些 Collider 做一些不同的设置。与其直接针对每条小道设置 Switch,我们选择在玩家踏上其中某条小道时立即结束任务。为此,我们需要创建新的脚本。
转到与其中一条小道对应的 Inspector 并创建新的组件:首先键入脚本名称(如 MissionEnd),接着选择 New Script,然后选择 Create and Add。确保将此脚本也添加到其余小道。
在新建的脚本中,我们要添加 2 个新的成员变量:
- bool 变量:仅仅监测结果是 true 还是 false。在本例中,我们要监测任务是否完成。
- Wwise Switch(完成脚本后会在 Inspector 中进行设置):依据玩家所走的小道将 "Path" Switch Group 设为 Red 或 Green。
接下来,我们要删除 Start 和 Update 函数。然后,在其对应位置按照以下执行顺序添加新的函数:
- 首先,利用 Unity 专用函数 OnTriggerEnter 检查某个对象(带有 Collider)是否进入了另一对象的触发区域。若是,则函数将追踪带有 other 变量的触发对象。
- 接着,检查任务是否完成。若没有完成,则将 missionComplete 变量设为 true。这样是为了确保仅播放一次此脚本中的音频。
- 然后,根据玩家所选的小道将 Switch 的值设为 Red 或 Green。
- 最后,将 "Play_MissionResult" Event 发给 other.gameObject(本例中为 player)。
返回 Unity 并转到与 "Red" 和 "Green" Collider 对应的 Inspector。确保针对 Red 小道设置 "Path / Red" Switch,并针对 Green 小道设置 "Path / Green" Switch。对所作设置进行测试。试听任务简报,然后踏上红色或绿色小道以此完成任务。
目前为止,对我们的游戏来说确实可行。不过,这里有个问题。眼下能得到的结果只有两种:玩家走绿色小道或红色小道。可是,我们之前在 Wwise 中设置的另外 3 种结果怎么办呢?
为了解决这一问题,我们可以在开头叙事结束后设置一个 5 秒计时器。若在玩家踏上小道之前计时结束,则任务自动失败。此时将根据玩家的完成进度来播放 MissionResult 叙事音频。对此,我们可以在之前在刚开始的 Unity 章节中创建的 Narration 脚本中加以实现。
打开 Narration 脚本并创建新的协程函数:
- 就跟第一个协程函数一样,我们要将此脚本的函数暂停一段时间(本例中为 5 秒)。
- 之后,我们要在 MissionEnd 脚本中引用 missionComplete 变量(仅在其为 public static 成员变量的情况下可行)。首先,我们来检查玩家踏上其中一条小道之后任务是否结束。
- 若玩家尚未结束任务,则要将 missionComplete 变量设为 true,以表明任务现在结束了。这样的话,玩家便无法在 5 秒计时完毕后踏上小道并再次完成任务。
- 最后,我们针对玩家播放 MissionResult 叙事音频。
最后在 PlayNarration() 函数结尾添加 StartCoroutine(Wait2())。然后,保存并测试所作设置。逐个尝试 5 种不同的场景,看看是否都能正常运行。若不能,请检查所作设置,确认有没有遗漏。若能,那恭喜!您成功构建了切实可行的动态对白系统。
下周,我会说说如何使用 Wwise 和 Unreal 整合对白。敬请期待!
评论