利用脚本实现全局音效的控制|Unity2D学习日记(二)
引言
个人学习积累中,如有任何问题与错误,欢迎指出与讨论。
这系列将会记录我在搭建自己的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);
}
当然,这种管理方式不仅限于音频管理,各位大可修改后用作其他方式的处理。
后记
这种全局管理的结构,个人相信应该不是最优解,也许在之后学习了更多知识后,会有进一步的优化。这篇文章,就当是提供一种思路吧。另外,我在学习本文相关内容时,借鉴了不少帖子、视频,包括但不限于: