引言
个人学习积累中,如有任何问题与错误,欢迎指出与讨论。
这系列将会记录我在搭建自己的2D平台游戏时遇到的一些问题与解决方案,核心目的均为更好的游戏体验与更棒的代码逻辑结构。所有代码基于C#与Unity。
正文
恰到好处的音效能够为游戏提供更好的沉浸感。——鲁迅
音效是游戏创造中的重要一环,恰到好处的音效,能够准确的告诉你,主角在“做什么”,又“遭受了什么”,为玩家提供足够的信息。但是如何管理是个问题。
主角扛着几个大音响与数张“唱片”:受伤、跳跃、跑步、攻击......与另一个扛着大音响和唱片的BOSS相遇。他们开启战斗,打着打着,要开启对应的音响,甚至可能还要根据自己的动作切换唱片。
当然这并不是不行,正式游玩时又不会真的有个大音箱挂在主角身上。但当你调试修改代码时,看着Inspector栏里成堆的组件时,你也许会觉得,这并不是一个好办法。那么,有什么更好的解决方法吗?
使用一个脚本实现全局管理,也许是个可行的方法。
惯例,讲一点点的前置小知识。
Component|组件
游戏对象是 Unity Editor 中包含组件的对象。组件定义了该游戏对象的行为。——Unity手册
组件是Unity中最重要的一块内容,脚本也可以作为组件挂载在物体上。我们需要知道的是,组件,也是可以通过脚本在物体上动态挂载(卸载)的。
- 加载方式:
组件类型 组件名 = gameobject.AddComponent<组件类型>();
- 卸载方式:
Destroy(组件名);
所以,我们可以通过脚本控制音频,在需要播放的时候生成组件(注:查阅网上资料,也有说动态加载对资源的消耗很大,谨慎使用?),并在音乐播放完毕后删除组件。
枚举类与Switch-case语句的组合
这是我个人非常喜欢使用的一个组合,写出来的条例清晰,让人debug时心情愉悦(并不)。
为什么要使用枚举类?
通过枚举类来限制范围,配合代码自动补全,减少出错概率,同时,也提高代码的可读性(只要你不瞎取名)。另外,枚举类里的每个值,本质上是int,所以传入数组时,是以int类型存放的,也正是利用这个,我们可以实现与Switch-case语句的结合,如下:
switch (Enum) { case Enum.Name_1: /* 内容 */ break; case Enum.Name_2: /* 内容 */ break; case Enum.Name_3: /* 内容 */ break; }
另外,由于为int值,还可以作为数组等的下标来处理,这方面就留给各位自行研究了。
AudioManager|全局音乐管理类
接下来写我们的脚本吧。为了方便其他脚本快速的调用该类里的内容,我们要使用静态(static)变量,并在一开始就赋值。
/* 无特殊说明,代码都在AudioManger类中 */ public static AudioManager instance; private void Awake() { // 保证只有一个,丢弃后产生的 if (instance != null) { Destroy(this); return; } instance = this; DontDestroyOnLoad(gameObject);// 避免在场景切换时摧毁该脚本所挂载的物体 }
另外,我们还需要准备好唱片(AudioClip)。
/* 简单意思几个,节省篇幅~ */ /* Header("在Inspector里的显示内容"),相当于注释;[SerializeField]用于在Inspector里可视化私有变量,方便赋值 */ [Header("背景音乐")] [SerializeField] private AudioClip musicClip; [Header("玩家音效")] [SerializeField] private AudioClip runClip_King;
在放歌前,我们还需要做好记录准备,不然局部变量一下子就跑不见了,再找就麻烦了。
private List<AudioGroup> audioSource_Background = new List<AudioGroup>(); private List<AudioGroup> audioSource_King = new List<AudioGroup>();
接着,我们要提供一个一键万能按钮。调用它后,会自动生成组件(音响,AudioSource)并播放音效,结束后,自动卸载组件。
/* MusicType为我们的枚举类,target表明对应的物体 */ public void PlayMusic(MusicType musicType, GameObject target) { AudioSource tempS; AudioGroup tempAG; switch (musicType) { case MusicType.Background: tempS = gameObject.AddComponent<AudioSource>(); tempS.clip = musicClip; tempS.Play(); tempS.loop = true;// 背景音乐要循环播放 tempS.volume = 0.2f; tempAG = new AudioGroup(tempS, target); audioSource_Background.Add(tempAG); /* 背景音乐不需要卸载,一直存在 */ break; case MusicType.Run_King: tempS = gameObject.AddComponent<AudioSource>();// 生成组件 tempS.clip = runClip_King;// 确定唱片 tempS.volume = 0.7f;// 调整音量 tempS.Play();// 播放 tempAG = new AudioGroup(tempS, target); audioSource_King.Add(tempAG);// 记录在案 StartCoroutine(DeleteAudioAfterPlay(tempAG, audioSource_King));// 协程,具体见下 break; } } /* 等待音效播放完后自动卸载 */ IEnumerator DeleteAudioAfterPlay(AudioGroup ag, List<AudioGroup> agList) { yield return new WaitForSeconds(ag.audioSource.clip.length);// length获取音频长度,WaitForSeconds(等待时间) agList.Remove(ag); Destroy(ag.audioSource);// 卸载组件 }
等等,这里是不是出现了什么奇怪的东西?AudioGroup是什么?
这是我自己定义的一个类(不太喜欢用结构),主要考虑到这样的情况:有多个敌人开着音响,而根据已有的内容无法将敌人与音效一一对应(因为都绑定在AudioManager的物体上)。具体内容见下:(之后有需要,我们也可以扩充这个类的变量)
/* 在AudioManager类之外 */ public class AudioGroup { public AudioSource audioSource;// 音响 public GameObject target;// 对应的物体 /* 构造函数,用于赋值 */ public AudioGroup() { } public AudioGroup(AudioSource audioS, GameObject t) { audioSource = audioS; target = t; } }
最后,我们只要在合适的地方按这个万能按钮就行了~至于怎么调用,就看你们自己的想法了,写在对应执行的地方或者作为事件放在动画里都是可以的。
/* 在AudioManager类之外,额外写个函数是方便作为事件放在动画里。 */ void RunAudio() { AudioManager.instance.PlayMusic(MusicType.Run_King, gameObject); }
当然,这种管理方式不仅限于音频管理,各位大可修改后用作其他方式的处理。
后记
这种全局管理的结构,个人相信应该不是最优解,也许在之后学习了更多知识后,会有进一步的优化。这篇文章,就当是提供一种思路吧。另外,我在学习本文相关内容时,借鉴了不少帖子、视频,包括但不限于:
Unity 2D教程:从独立游戏学习开发12: 音效控制(Audio Manager)——M_Studio(https://www.bilibili.com/video/BV17E411Y7VN)
其实对于比较复杂的音频功能,推荐使用音频中间件进行管理,比如wwise,fmod(个人觉得wwise更好用)
文章里提到的动态加载component的方式,其实在这些中间件中都被抽象成了“音频事件”,可以非常高效地进行音频资源的管理
@Major:好的,我会去了解下的~