前言

​ 毕设中的demo需要用到声音模块,但是对于一些个人小项目来说Wwise的接入显然是不现实的。因此也根据了另外一个成熟框架-GameFramework中的声音框架进行了对ET的部分适配,对于GF中部分的OOP思路进行了ECS化(组件代替继承,分发代替虚函数),其中底层仍用到了Unity自带的AudioClip、AudioSource等组件。

本文参考了花花的GameFramework解析:声音,并进行了一定的修改。

模块化管理组件(ModuleComponent)

​ 在以前制作demo的过程中我们通常会把不同的全局模块采用统一的单例类管理,方便进行模块的插入、删除、使用、交互,而在ET中我同样采取了这样的思路,并且把这个单例管理类作为Entity插入到Game.Scene内名为”Game”的zoneScene中,其在客户端中仅存在一个,而在服务器中可以管理多个,同时我将一些传统的显示模块,如CameraModule(相机模块)、EffectModule(特效模块)、SceneTreeModule(场景四叉树管理模块)、SoundModule(声音模块)等都加入到这个模块中,方便进行模块的增加、删除、调用。

​ 以下是ModuleComponent的UML图:

EGUI可视化管理模块--.drawio

  • 添加新模块时实现AModuleEventHandler接口并打上ModuleAttribute(Type type)属性,OnLoad(BaseModule module)中对module添加相应的模块Entity,同时在OnRelease(BaseModule module)中移除相应的模块Entity。之后通过在ModuleComponent中调用Register()通过具体类型分发对应的AModuleEventHandler接口实现注入,本质上还是通过接口函数分发的方式来实现多态。
  • 在moduleDic中存储已生成模块的Type对应BaseModule,在Get()中通过具体泛型或类型Type获取BaseModule后通过GetComponent<>()的方式获取到对应的实际模块Entity(如CameraModule等)
  • 对于BaseModule中具有相应状态枚举来管理当前模块状态,同时在不需要时可以直接移除对应Entity,实现热插拔

模块管理设计方面相对简单,因为模块基本都是同步载入且为热插拔的方式,所以在RegisterMode中没有过多的状态区分。

声音管理模块

主体逻辑部分:

​ 声音管理模块的设计参考了GF中的SoundComponent,通过SoundAgent实现最终的声音播放,同时在SoundModule中将不同的SoundAgent划分在不同的SoundGroup中进行统一管理(如所有背景音乐、效果音乐可以划分在不同SoundGroup中进行分别管理),与此同时,通过代理模式SoundHelper、SoundAgentHelper、SoundGroupHelper实现了对Unity中AudioSource、AudioMixer等的调用解耦,SoundAgent无需得知与Unity音频组件的交互手段,而将其托付给对应Helper实现。

​ 先放上设计过后的UML图:

EGUI声音模块__.drawio

  • 对于SoundGroup、SoundAgent、SoundModule来说实现了Entity和System的分离,同时通过ISoundGroup和ISoundAgent对Entity类的属性接口进行封装
  • 在SoundModule中存储SoundGroup以及在SoundGroup中存储SoundAgent时,都通过AddChild<>的方式先将被存储的Entity以child的形式添加到存储Entity中,随后进行赋值,方便管理同时可在父类Entity调用Dispose时即将其存储的所有child Dispose掉并返回Entity引用池中
  • 通过PlaySoundParams中具体参数可以在播放时调节播放的时间、位置、绑定的GameObject、声调等
  • SoundAgent、SoundGroup具体与Audio组件的交互通过其对应的Helper进行

具体实现部分:

上面封装好了一套规范化的音频调用流程后,我们就需要对SoundGroup、SoundAgent、SoundModule写好对应的Helper,其对应的功能如下:

  • ISoundAgentHelper:获取当前是否正在播放、获取声音长度、获取或设置播放位置、静音等,其对应的是AudioSource的调用,实现这个接口的类本质上是一个挂在AudioSource组件的Mono类
  • ISoundGroupHelper:获取当前的音频混合组,对应的是AudioMixerGroup的调用
  • ISoundHelper:管理音频资源,进行动态同步、异步加载和释放,其对应的是不同加载情况下的AudioClip的资源管理

具体的UML图如下:

基于ET的声音模块Helper_.drawio

  • 将ISoundAgentHelper和ISoundGroupHelper放在Mono层,作为与Unity组件的交互Helper
  • 将ISoundHelper放在ET层,内部调用ResourceComponent与Resource类分别实现对AB包音频以及Resource目录下音频的动态加载与释放,其在SoundModule中采用函数分发的方式调用

具体使用:

  1. 我们在切换战斗场景结束的回调中测试战斗背景音乐的播放,先设置好对应的场景音乐的参数后调用
    image-20211219141711343

  2. 设置好预设的AudioMixer,方便通过AudioMixer对所有的处在同一Group的AudioSource进行调节
    image-20211219142050637

  3. Play运行,可以看到已经自动放入了对应的AudioMixer的Group中
    image-20211219142219552

    且在Hierarchy窗口中进行了分层管理:image-20211219142328302

一些小Tips:

  1. 在GF框架内部的SoundGroup对其内部所有SoundAgent的管理实际用到AudioMixer的其实基本没有,包括Mute、Volume的设置,都是通过代码内遍历soundAgent对其单独调节实现的,而并非借助AudioMixer暴露接口的方式调节,降低了与AudioMixer间的耦合性。猜测应该是想将AudioMixer的调节尽量放在运行前调节而非运行时。