**作者:**Alexander Hodge
**作者主页:**http://anor.ac
**原文链接:**http://anor.ac/?page_id=93
**版权说明:**本文所有内容由原作者授权 Audiokinetic 翻译、转载并转存部分文件供镜像下载
蒙特利尔的公司 Audiokinetic 开发了流行的游戏音频中间件 Wwise。我特别写下本文来帮助 Unity 开发者将 Wwise 整合到游戏项目中,文中提供详细步骤和示例素材。
在下面的视频中你可以观看本文用到的示例游戏的玩法并试听最终音频效果。
本文将涉及 Wwise 的以下功能点,权当 Wwise 潜能的一碟开胃菜:
- Wwise 编辑器的视图和术语
- 事件(Event)
- 游戏同步器(Game Sync)如 State
- 共享集(ShareSet)
- 互动音乐
- 用音频总线(Audio Bus)混音
下载本文配套音频素材(6.7 MB)
下载镜像(提取码:f638)
本教程采用Unity 官方示例工程Survival Shooter
的修改版本,游戏可以从 Unity 素材商店免费下载。这是一个没有尽头的枪战场景,其中有三种敌人、一种武器还有一些音效和音乐。我们要利用 Wwise 来替换掉这些音效和音乐。
在这个 Unity 工程的根目录(里面有Assets
目录)下,新建并保存一个 Wwise 工程,在这个过程中要保证选中了合适的平台,本例中用的是 Mac 平台。工程新打开的时候会启动Designer
布局(F5
键)。
前往Project Explorer
(工程浏览器)视图的Audio
选项卡,它默认位于 Wwise Authoring Editor (译注:即 Wwise Authoring Tool, Wwise 设计工具)的左上区域。在Actor-Mixer Hierarchy
(角色混音器层级结构) 中右击叫作Default Work Unit
(默认工作单元) 的对象并选择 Import Audio Files ...
。这个 Actor-Mixer Hierarchy 是 Wwise 中的一个结构,它包含了游戏项目中所有的音效。从示例素材中选中并添加rifle_shot.wav
这个文件,在Object Type/Action
列选中Sound SFX
(音效对象)。小技巧:一个很有用的导入操作快捷键是Shift+I
。
这是我们要用在枪声上的唯一文件。选中rifle_shot
SFX 对象后,你可以按下空格键试听一下效果。编辑器底部Transport Control
(播放控制)的标题栏中会显示选中对象的名称。你还会注意到选中 SFX 对象后,界面会自动更新,显示出这个对象的 Sound Property Editor
(属性编辑器)和Contents Editor
(内容编辑器)视图。让我们运用Sound Property Editor
来给步枪枪声一点随机变化吧!
让rifle_shot
对象保持选中状态,双击Pitch
属性下的小圆圈图标,接着Randomizer
(随机化器)视图打开了。启用 Randomizer,并将Min
和Max
值分别设为-150和150,单位是音分,半音音阶中相邻半音间的音程是100音分,所以我们相当于给rifle_shot
对象的Pitch
属性增加了正负1.5个半音的随机化范围。这样做能让声音更多样一些,避免产生令人心烦的重复音效。
再试听几次rifle_shot
SFX 对象,你会听到音高有了随机变化。恭喜,你已经学会使用 Wwise 中的第一块积木 - SFX 对象了!
我们现在已经能在 Wwise 中播放声音了,但如何才能在Survival Shooter
游戏中当玩家开枪时播放rifle_shot
这个 SFX 对象呢?答案是事件(Event),这是让游戏触发 Wwise 对象来做事的一种方法。我们现在要添加一个名为PlayerShoot
的事件,每当玩家的武器打响时,游戏代码就会调用这个事件。
切换到Project Explorer
视图中的Events
选项卡。右击Events
文件夹中的Default Work Unit
并选择New Child
>Play
。给这个新创建的 Event 取名为PlayerShoot
。注意:Event 名称的字符串必须要和文中所述吻合 - 我们要从游戏代码里依据名称来调用这个 Event。
你可能已经注意到了PlayerShoot
事件已经自动加到Event Viewer
里去了,这个视图就在Project Explorer
视图下方。你可以在两个视图中的任何一个里点击PlayerShoot
事件。上一步中已经创建了一个 Play Event,Wwise自动给这个事件的Event Action
(事件动作)列表中添加了一个Play
动作。然而它还少了一个对 Object 的引用 - Wwise 现在还不知道当这个 Event 被调用时应该播放什么声音。你可以右击该列,选择Browse…
,再从Actor-Mixer Hierarchy
中选中rifle_shot
这个 SFX 对象。
你可以按照试听rifle_shot
对象的同样方法来试听PlayShoot
事件,方法是在Event Viewer
中选中这个 Event 并按下空格键。现在我们知道如何新建 Events 并把 Actions 添加到对象上去了。我们已经差不多准备好从游戏代码中调用这个 Event 了,只需要做最后一件在 Wwise 中非常重要的事情 …
SoundBanks 是来装载 Wwise 中你要创建的所有内容的容器,包括 SFX 对象,Events 等等。游戏要加载的不是别的正是 SoundBank,所以每当 Wwise 工程有了变化和更新的时候,你都必须要重新生成一个新版本的 SoundBank。同一个游戏可以有多个 SoundBanks(通常每个游戏关卡会有自己的 SoundBank),但因为Survival Shooter
只有一个场景,所以我们只会用到一个 SoundBank。
在Project Explorer
中切换到SoundBanks
选项卡。右击SoundBanks
文件夹中的Default Work Unit
之后选择New Child
>SoundBank
,接着给它取名叫MainSoundBank
。
***下面把 Wwise 界面布局切换为 SoundBank,方法是从Layouts
菜单中选择这个布局,或者按下快捷键F7
。***Wwise 会更新各个视图以反映新的工作环境。
你现在应该能在SoundBank Manager
视图中看到MainSoundBank
对象了,它是Default Work Unit
的子对象。从Events
选项卡中把Events
文件夹拖动到SoundBank Manager
视图中的MainSoundBank
对象上去。保证勾选了这个 SoundBank 跟它的目标平台Platform
和语言Languages
,然后点击Generate
。
切换到 Unity 并打开Level 01
这个场景。我们要导入 Unity Wwise Integration(WwiseUnityIntegration_v2015.1.1_Mac.unitypackage
),这是一个 Unity 的 Custom Package(用户包)。
如果一切进行顺利,你会看到Wwise Setup
屏幕出现了,注意到它已经定位到了我们的 Wwise 工程路径,工程保存在游戏根目录下。我们要保留默认设定,意味着 SoundBank 路径应该维持Audio/GeneratedSoundBanks
且保证勾选了底下的两个复选框 - Wwise 会自动帮我们把一些游戏对象添加到Survival Shooter
场景中去。
点击Start Installation
。
安装结束后,你会看到 Unity 中出现了一个新选项卡叫做Wwise Picker
。这给了我们一个高层次的视图来看清 Wwise 工程的结构以及目前我们已经创建了的所有内容。
如果你看向场景的Hierarchy
视图,会注意到一个新对象叫做WwiseGlobal
,它包括了两个脚本组件,用来处理场景中 Wwise 的初始化和关闭。我们会维持这个对象的默认设定不变。
如果你查看一下Main Camera
对象,也会发现 Wwise 已经将 Unity 的Audio Listener
对象换成了自己的变体 - Ak Audio Listener
- 并给了它一个Listener ID
属性,值为0
。如果现在就播放这个 Unity 场景,那么你只会听到一片空白。我们需要加载 SoundBank 并在游戏代码中调用PlayerShoot
Event,这样才能听到rifle_shot
SFX 对象。
在场景中创建一个空游戏对象并给它取名为LoadSoundBank
。从Add Component
的Wwise
子目录中将AkBank
脚本加到这个对象上去,并在Bank Name
属性中选择MainSoundBank
,保持其它属性不变。
在 MonoDevelop 中打开PlayerShooting.cs
脚本。在这个脚本中我们会添加对PlayerShoot
Event 的调用。看到第49行的Shoot()
调用,这是每次武器打响时被调用的函数,所以这里是调用PlayerShoot
Event 的一个合适的位置。
将下列代码添加到Shoot()
下方:
AkSoundEngine.PostEvent("PlayerShoot", this.gameObject);
PostEvent
方法需要两个输入参数:被调用 Event 的名称字符串和相关的游戏对象。
现在再播放一次场景,你应该可以听到rifle_shot
SFX 对象了,并且每次枪声响起时还有微妙的音高变化。
让我们回头仔细想想刚刚都做了些什么。我们只用一行游戏代码就调用了PlayerShoot
事件,于是就成功地解除了音频团队和游戏程序员的工作之间的耦合。这意味着只要约定了 Event 的名字,游戏程序员就可以继续编写游戏逻辑,而音频团队则可以集中注意力并行开发最佳的音频体验。假设我们想要改变步枪枪声的音频文件,或者用几个声音来层叠发声,或者甚至给PlayerShoot
Event 添加别的Play
动作,比如播放每发枪声后(短暂延迟)的换弹声。这些工作现在都可以不用游戏程序员干预了。音频程序员只需重新生成包含新变动的 SoundBanks,随后游戏音频内容会自动更新。
再回想一下,刚才为rifle_shot
声音的音高做随机化的过程是相对简单的,想想如果要用 C# 游戏代码来实现相似的效果会需要付出怎样的努力,那样至少需要好几行代码来生成随机数并赋值给AudioSource
的音高,而且如果我们要尝试不同的音频文件,那么就需要麻烦游戏程序员,发给他们新文件,让他们把新文件加到工程素材中并加到确切的AudioSource
里面去。相比之下,希望你能体会到使用 Wwise 能为整个团队省下的开发时间。
让我们切回我们的 Wwise 工程,并***按F5
来将界面布局换为Designer
。***切换到Project Explorer
的Audio
选项卡。
我们现在要添加一些玩家受创时要播放的声音。为了做到这点,我们要使用一种 Wwise 对象叫做 Random/Sequence Container(随机/序列容器)。这个对象会装上多个 SFX 对象,分别代表各种嘟哝声,并在玩家每次受创时随机选择并播放其中之一。导入(Shift+I
)叫做damage sounds
的文件夹,当导入装有多个音频文件的文件夹时,Wwise 导入器会自动创建一个 Random/Sequence Container 作为父对象。
选中damage sounds
对象,让我们关注一下它的Property Editor
。我们可以再次用 Wwise Randomizer 为整个容器添加一些随机音高变化。这一次,让我们将音高多样化设成严格为正值 - 我们要设这个值为300音分,这样嘟哝声会有种活泼的花栗鼠叫声的质感。我们还要保证Play Type
设为Random ``Shuffle
,并且因为我们不想连续听到同样的嘟哝声,所以我们可以告诉 Wwise 要Avoid repeating the last``2``played
(避免重复前面2次播放的内容)。我们还要仔细确保Play Mode
属性设置为Step
(步进)而不是Continuous
(连续)。
我们要采用和PlayerShoot
Event 同样的做法,来新建一个 Event 叫做PlayerDamage
,它包含一个 Action 来在damage sounds
这个容器对象上调用播放。
下面,我们要给三种敌人类型配上各自的声音。再次选中Actor-Mixer Hierarchy
中的Default Work Unit
,导入(Shift+I
)bunny.wav
、bear.wav
、hellephant.wav
这几个音频文件。这些会作为三种敌人类型的主要声音。
如果试听一下这三个新 SFX 对象,你会注意到它们其实都是一种怪物嚎叫的变体,并且各自反映了敌人的个头 - 兔子(bunny)声基本上是微喘了一口气,而地狱象(hellephant)的声音更长,更深沉响亮,而且感觉像是大得多的动物。熊(bear)的声音介于上面两者之间。
我们要让这些声音无限循环播放。每个 SFX 对象在Property Editor
中都有一个Loop
设定可以启用,但与其对声音一个个地做启用操作,我们不如使用 Wwise 的Multi-Editor
工具来节约时间。在Actor-Mixer Hierarchy
中选择全部三个敌人 SFX 对象并点击Ctrl+M
来打开Multi-Editor
窗口。在Audio
>General Settings
>Loop
下启用Is Looping Enabled
并点击OK
。默认设定是无限循环,这符合我们的需求。
既然要循环播放这些声音,那么我们应该保证循环是无缝的,这是指循环的开始和结束应该有同样的音量电平。如果不这么做,那么一次循环播放会结束于某个音量电平而再次起播于不同的电平,造成听得到的噼啪声。针对每个敌人的声音,打开其Source Editor
(源编辑器),方法是在其Contents Editor
视图中双击对象名字旁边的波形图标。
Source Editor
用来查看音频文件的波形并调整裁剪点和淡变值。我们感兴趣的是调整淡变值,这对应上图中波形视图顶端的蓝色三角。拖动这些三角将调整淡入到满度音量和淡出到无声所需的时间。你会注意到波形的幅度会随这些淡变值自动调整。
(2015年12月6日编辑)本教程早期版本中采用在每帧中计算玩家和敌人间的距离,再把距离值作为 RTPC 发给 Wwise 的方法来实现衰减,后来 Audiokinetic 团队很慷慨地教会了我一个更简洁的做法:我们不如让 Wwise 自己为我们来处理距离计算!
第一步是新建三个新 Play Events,每个敌人的 Sound SFX 对象配备一个 Event。我已经给它们取名为PlayZombear
、PlayZombunny
、PlayHellephant
。
然后,我们要前往Project Explorer
的ShareSets
选项卡并新建三个新的 Attnuations(衰减),每个敌人一个。Max distance
这个参数可能需要缩短 - 我发现设为15比较合适。你大可以按自己的喜好调节曲线形状。例如,如果让“地狱象”的声音在远处也能听见也许是个好的设计,因为这种野兽体积很大而且声音也很吵闹。而如果你希望熊和兔子能潜行突袭玩家,则可以把它们的衰减曲线设为只有在相当近的距离内音量才会攀升至满度。
接下来,我们需要把这些 Attenuation 曲线关联到对应的 Sound SFX 对象上去。对每个敌人声音的 Sound SFX 对象,前往其Sound Property Editor
的Positioning
选项卡。确保3D
选项已启用,并在3D
设定区块中设置相关的Attenuation
。将Mode
项更新为Use ShareSets
。
一旦为三种敌人的 Sound SFX 对象指派了 Attenuation,我们就要重新生成 SoundBank 然后切回 Unity。
前往 Unity 工程的Prefabs
文件夹,对每个敌人的 prefabs 添加一个 Wwise 的Ak Event
的脚本组件,把脚本加到 prefab 游戏对象上去,并在Event Name
属性下从我们之前创建的三个 Events 中选取一个合适的。
我们也需要把先前创建的PlayerDamage
事件加入。打开PlayerHealth.cs
,在TakeDamage()
中添加以下代码
AkSoundEngine.PostEvent("PlayerDamage", this.gemaeObject);
保存并播放场景。你现在应该能够听到三个敌人的声音会随着敌人接近玩家而变得更响(Ak Listener
组件附着在玩家上,这个对象是 Wwse 内部用来计算发送事件的 Game Object 和玩家之间距离的),而且玩家角色每次受创时会发出不同的嘟哝声!
##互动音乐
如果我们能给Survival Shooter
游戏添加些音乐,让音乐能随着玩家的动作而改变,是不是会很棒?我们可以利用一种 Game Sync 叫做 State(状态)。
简便起见,我们只会用到两个玩家状态来在两个不同的音乐段落之间过渡 - 把一段简单的弦乐合奏乐句用于玩家静止不动的时候,而一段更复杂的弦乐合奏乐句用于玩家移动的时候。音乐节奏为4/4拍、124 BPM、降B小调。Wwise 可以让你以音乐性的方式来做到在这些离散的状态间过渡,也即可以在音乐乐句的节拍点或小节线上触发状态改变,而不是简单的交叉淡变。
切回 Wwise 并按F10
将界面布局换到Interactive Music
。然后切换到Project Explorer
的Game Syncs
选项卡。
我们要新建两个 States(Idle
,Movement
)。首先,新建一个 State Group,方法是右击States
文件夹中的Default Work Unit
,并给它取名为MainStateGroup
。然后在这个组里面新建两个 State 子对象并给它们取名为Idle
和Movement
。
切换到Project Explorer
的Audio
选项卡,并右击Interactive Music Hierarchy
(互动音乐层级结构)文件夹中的Default Work Unit
。新建一个 Music Switch Container,并给它取名为SwitchContainer
。然后右击SwitchContainer
并新建两个 Music Playlst 类型的子对象,给它们取名为Music Playlist Idle
和Music Playlist Movement
。
我们现在要把创建好的 States 和SwitchContainer
对象关联起来。点击SwitchContainer
对象并在Music Switch Container Property Editor
中选中States
选项卡。点击标记为Add >>
的按钮然后添加我们创建好的MainStateGroup
。
接下来,切换到Transitions
选项卡。那里应该已经有了一个默认的过渡(Transition),那么选中它。你会注意到被选中的过渡下方有两片区域叫作Source
(源)和Destination
(目标)。我们要的是干净利落的音乐性过渡,所以在Source
下面,我们要选择Exit source at
Next Beat
。我们还可以添加一个Fade-out
,淡出时间为1秒,并给0.5秒的偏置,采用对数曲线。做完后你应该会得到这样的结果:
我们也可以给Destination
区添加Fade-in
,采用与上述类似的数值。我们还要把Destination
设定为Sync to
Same time as Playing Segment
。下面,我们要把MainStateGroup
添加到Music Switch Container Association Editor
(关联编辑器)中去,方法是点击标记为>>
的按钮。最后,我们需要将两个 Music Playlist 子对象从Interactive Music Hierarchy
中拖到Music Switch Container Association Editor
中它们对应的 States 上面去。这是至关重要的一步,而且我发现很容易忘记这一步。
现在,切换到Music Swtich Container Property Editor
的General Settings
选项卡,将Time Settings
中的Tempo
更新为124。SwitchContainer
的Music Playlist
子对象会自动更新它们的Tempo
属性来反映出父对象的变化。
现在我们可以把音乐文件添加到 Wwise 工程中去了。最简单的方法是把正确的文件拖放到 Music Playlist 对象上去,这样会自动新建 Music Segment(音乐段落),它里面装有代表音乐文件的 Music Track(音乐轨)。然而,你还是可以手动添加这些子对象的,方法是右击SwitchContainer
并新建 Music Segment 子对象然后再创建 Music Segment 的 Music Track 子对象。
将StringPhraseIdle.wav
拖放到Music Playlist Idle
上去,然后将StringPhraseMovement.wav
拖放到Music Playlist Movement
上去。
创建了 Music Segment 子对象之后,你还需要把它们添加到 Music Playlist Editor
中去。选中Music Playlist Idle
对象,将StringPhraseIdle
Music Segment 拖放到Music Playlist Editor
中去。将这个Group
的Loop Count
(循环次数)设为Infinite
(无限),方法是单击下箭头。要对Movement
Music Playlist 也重复一次这个过程。
到了这里,你可以试听一下互动音乐了,方法是选中SwitchContainer
对象,点击播放,并在Transport Control
视图中的两个状态之间切换。你会注意到音乐是在节拍点上切换的!
还差一步 ...
我们想让音乐在游戏开始时开始播放,所以我们要给StartGame
事件添加一个 Play 动作,这会让SwitchContainer
开始播放。***按 F5 键切回 Designer 布局,并把合适的 Action 加到 StartGame
Event中去。***结果看起来应该是这样的:
译注:原文和上图中的StartGame
事件里有几个问题需要澄清
- 这个事件需要单独在 Wwise 中创建,原文遗漏了这个说明;
- 为了在游戏场景启动时发送该事件,应该在 Unity 工程中某个
MonoBehaviour
对象的Start()
方法中调用PostEvent()
方法,或者利用 Wwise Unity 集成自带的 AkAmbient 脚本组件,将其封装的事件属性Trigger On
设为Start
; - 建议在手动创建
StartGame
事件时不要包括上图中的前面三个 Actions,而只保留音乐播放的 Action,原因是这三个 Actions 对应的三个 SFX 对象已在创建敌人音效
一节中被设为无限循环播放,所以一旦播放StartGame
事件,怪物嚎叫声会伴随音乐不断播放,而且前文ShareSets 介绍 - 衰减
一节中已提到为三种敌人分别创建了各自的事件来播放此处用到的三个 SFX 对象,所以上图中的情况不像是正常的设计。
你需要将Interactive Music Hierarchy
文件夹添加到 SoundBank 中去。重新生成 SoundBank 并切回 Unity。
我们需要添加一点代码,代码要检查玩家是否移动了。一个简单的办法是把下列代码加到PlayerMovement.cs
的Move()
方法中去:
if(h != 0 || v !=0)
{
AkSoundEngine.SetState("MainStateGroup", "Movement");
}
else
{
AkSoundEngine.SetState("MainStateGroup", "Idle");
}
变量h
、v
代表输入坐标的数值。如果其中任何一个变量的数值不为零,则表示玩家在移动,我们于是对 State 做相应的更新。
如果你现在播放这个场景,应该就可以听到音乐随玩家移动而变化了!
##音频总线和混音:旁链/闪避
既然我们给场景添加了音乐,你可能会发现当玩家开枪时听觉上会开始有点嘈杂。对此,一个可能的解决方案是当玩家开枪时让音乐音量衰减一点。这叫作旁链或者闪避(Ducking)。Wwise 让这种操作变得很简单,方法是创造性地运用音频总线。译注:原文中作者将 Side-chaining 一词与 Ducking 并用,但实际上只介绍了后者的功能,Wwise 的旁链比闪避功能更丰富,详情请见官方文档。
在Master-Mixer Hierarchy
(主混音器层级结构)的Master Audio Bus
下面新建两条子音频总线,取名为Music Bus
和Weapon Bus
。
我们要更新rifle_shot
SFX 对象和SwitchContainer
对象各自的音频总线。你可以修改两者的Property Editor
中的Output Bus
属性。
最后,我们要添加闪避,方法是在Master-Mixer Hierarchy
中选中Weapon Bus
并将Music Bus
插入到它的Auto-ducking
属性中去。我们也可以设置Auto-ducking
的音量:我选择-9 dB,这意味着当有信号通过Weapon Bus
的时候Music Bus
的音量会被衰减9 dB。你也可以设置闪避到达目标音量所需的时间,还可以设计衰减曲线的形状。你应该会得到这样的结果:
重新生成你的 SoundBank 并在 Unity 中播放场景。你现在应该可以听到当玩家开枪时音乐音量会被压低,随后当停止开枪后又会回升到满度音量。
##结论
本文权当一篇基于 Unity 工程的 Wwise 简介,只触及了 Wwise 的一小部分功能。前文只是介绍了在 Unity Editor 中的操作步骤,当要把最终工程部署到选定的目标平台上去时,还需要把我们生成的 SoundBanks 挪到 Unity 的StreamingAssets
文件夹下面去(译注:参见前文把 Wwise 添加到我们的 Unity 工程
一节配图中的SoundBank Path
设定)。感谢阅读!