前言

虽然现在稍微像样点的游戏项目都接上了像Wwise这类音频引擎,大概率用不上这类声音模块,不过GF的声音模块还是非常值得没有游戏音效管理经验的同学学习,声音模块也属于GF中较为轻量的一个模块,本文将简单讲解。

先抛出几个问题:

  1. 对于同一类型的音效,我希望在设置面板上做统一的音量大小调整,例如游戏设置中常有的BGM、UI、队友语音、场景音效等音量调节,但开发过程中不断有新的音效加入,如何做同一类型音效的音量的统一管理?
  2. 像RTS这类单位非常多,攻击频率也很高的游戏,如果每次攻击/受击都播放音效,那游戏整体声音将会十分混乱,如何在框架层面控制同个类型的声音的最大同时播放数量?
  3. 在问题2的基础上,如果我们只播放有限数量的音效,如何加入优先级控制,实现在已达到播放上限时,继续尝试播放音效,如果新播放的音效优先级比当前正在播放的音效要高,那么就顶替掉正在播放的音效?

结构

SoundAgent

SoundAgent是声音代理,在Unity中我们一般使用AudioSource来播放声音,在GF的声音管理下,我们不再自行创建AudioSource来播放声音,而是使用SoundAgent来播放。SoundAgent有一个ISoundAgentHelper接口的字段,这个接口在UGF层上有具体实现类,实现这个接口的类是一个挂载了AudioSource组件的Mono类,它持有了自身GameObject上的AudioSource的引用,并在ISoundAgentHelper接口的方法实现中去调用AudioSource的接口。

当然,游戏业务不会直接访问SoundAgent,而是通过SoundManager直接播放声音,而SoundManager会调用SoundGroup的接口,然后由SoundGroup取得SoundAgent去播放声音。

SoundGroup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/// <summary>
/// 播放声音。
/// </summary>
/// <param name="serialId">声音的序列编号。</param>
/// <param name="soundAsset">声音资源。</param>
/// <param name="playSoundParams">播放声音参数。</param>
/// <param name="errorCode">错误码。</param>
/// <returns>用于播放的声音代理。</returns>
public ISoundAgent PlaySound(int serialId, object soundAsset, PlaySoundParams playSoundParams, out PlaySoundErrorCode? errorCode)
{
errorCode = null;
SoundAgent candidateAgent = null;
foreach (SoundAgent soundAgent in m_SoundAgents)
{
if (!soundAgent.IsPlaying)
{
candidateAgent = soundAgent;
break;
}

if (soundAgent.Priority < playSoundParams.Priority)
{
if (candidateAgent == null || soundAgent.Priority < candidateAgent.Priority)
{
candidateAgent = soundAgent;
}
}
else if (!m_AvoidBeingReplacedBySamePriority && soundAgent.Priority == playSoundParams.Priority)
{
if (candidateAgent == null || soundAgent.SetSoundAssetTime < candidateAgent.SetSoundAssetTime)
{
candidateAgent = soundAgent;
}
}
}

if (candidateAgent == null)
{
errorCode = PlaySoundErrorCode.IgnoredDueToLowPriority;
return null;
}

if (!candidateAgent.SetSoundAsset(soundAsset))
{
errorCode = PlaySoundErrorCode.SetSoundAssetFailure;
return null;
}

candidateAgent.SerialId = serialId;
candidateAgent.Time = playSoundParams.Time;
candidateAgent.MuteInSoundGroup = playSoundParams.MuteInSoundGroup;
candidateAgent.Loop = playSoundParams.Loop;
candidateAgent.Priority = playSoundParams.Priority;
candidateAgent.VolumeInSoundGroup = playSoundParams.VolumeInSoundGroup;
candidateAgent.Pitch = playSoundParams.Pitch;
candidateAgent.PanStereo = playSoundParams.PanStereo;
candidateAgent.SpatialBlend = playSoundParams.SpatialBlend;
candidateAgent.MaxDistance = playSoundParams.MaxDistance;
candidateAgent.DopplerLevel = playSoundParams.DopplerLevel;
candidateAgent.Play(playSoundParams.FadeInSeconds);
return candidateAgent;
}

SoundGroup是本文前言中抛出的3个问题的解决方案的核心实现,上面是SoundGroup中PlaySound的实现代码。

问题1

每个声音播放的时候都会指定一个SoundGroup,SoundGroup有Mute、Volume两个方法来控制这个SoundGroup下每个Agent的静音设置和音量系数。所以我们只需要把不同类型的声音分到不到同组,我们就可以统一控制每个组的整体音量。

问题2

SoundGroup内部以List的形式来储存多个SoundAgent,每次播放声音都会取出一个SoundAgent(把agent的IsPlaying标记置为true),待播放完毕时,才会放回去(把agent的IsPlaying标记置为false),如果该SoundGroup中所有的SoundAgent都在播放中,那么这次播放就有可能会失败,这就解决了问题2中,同一类型(同一个SoundGroup)限制最大同时播放数量的问题。

上面是指有可能播放失败,是因为如果当SoundGroup中所有的SoundAgent都在播放中时,还会比较优先级,这个就是问题3要探讨的内容。

问题3

当播放声音时,SoundGroup会遍历内部的SoundAgent,若当前迭代中的SoundAgent没在播放状态中,则直接使用该SoundAgent来播放,如果处于播放状态,则会对比优先级等内容:

  1. 若新播放的音效的优先级比该SoundAgent当前播放的音效的优先级要低,则跳过,检测下一个SoundAgent。
  2. 若新播放的音效的优先级比该SoundAgent当前播放的音效的优先级要高,则把这个SoundAgent作为候选的Agent,后续有可能会用这个SoundAgent来播放新的音效而取代这个SoundAgent的当前音效,具体详见第三点。
  3. 若新播放的音效的优先级比该SoundAgent当前播放的音效的优先级要相等,这种情况GF提供了m_AvoidBeingReplacedBySamePriority字段,意味避免同优先级取代。
    • 当这个字段为true时,那相同优先级的新音效将无法取代正在播放的音效,只能检测下一个SoundAgent。
    • 当这个字段为false时,若当前候选SoundAgent是空或者当前候选SoundAgent的开始播放时间点晚于当前遍历的这个SoundAgent的开始播放时间,则把候选SoundAgent更新为当前遍历的SoundAgent,也就是若允许同优先级取代时,会取播放时间最早的SoundAgent来作为最后用来播放新音效的SoundAgent。

注意上述流程若遍历中检测到没有在播放中的SoundAgent时,会直接作为最终播放SoundAgent,中断遍历流程,而检测到SoundAgent正在播放音效时,就算作为候选SoundAgent也不会直接中断遍历流程,而是逐一对比取正在播放的SoundAgent中播放时间最早的一个作为最终播放SoundAgent。

PlaySoundParams

播放参数,对于同一个声音资源,每次播放可以通过传入不同的播放参数,以达到不同的播放效果,参数包括有音量、优先级、静音、播放开始时间、以及一系列音效(Sound Effect)设置等。

SoundManager

外部访问声音模块的入口。

  • 对外提供HasSoundGroup、GetSoundGroup、GetAllSoundGroups、AddSoundGroup、AddSoundAgentHelper等对SoundGroup进行查询、操作等接口。
  • 对外提供PlaySound、StopSound、StopAllLoadedSounds、PauseSound、ResumeSound等控制声音播放的接口。

关于SoundManager中的资源管理

SoundManager的资源加载卸载与GF的UI模块大同小异(不同的是由于音效资源不需要多个实例,所以SoundManager内部不需要对象池来维护),可以参考本系列的UI解析文章,本文不再赘述,本文开头的UML图中也对此部分进行了简化。

最后

GameFramework解析 系列目录:GameFramework解析:开篇

个人原创,未经授权,谢绝转载!