前言

本文将会介绍如何使用audiomixer实现全局音量控制,并且会介绍如何实现游戏内的含静音功能的音量设置界面。

本人也是个初学者,在看过一些关于音量管理的教程后,发现使用audiomixer实现全局音量控制可能是最方便、功能最完备、强大的,因此在这里分享一下自己的实现方法。如果有错误,还请指出捏

基础

音效成功播放需要两个先决条件:音频源 (Audio Source) 音频监听器 (Audio Listener)。

音频源 (Audio Source) 在场景中播放音频,而音频监听器 (Audio Listener) 充当类似于麦克风的设备。它接收来自场景中任何给定音频源的输入,并通过计算机扬声器播放声音。

Android 声道全局控制 全局音量控制_unity

AudioListener和AudioSource

 其中,Audio Listener 会在相机创建时自动创建,当同一场景中的Audio Listener数量大于1时,unity编辑器会报错

Android 声道全局控制 全局音量控制_c#_02


混音器 (Audio Mixer) 允许混合各种音频源,对音频源应用效果。在音频源脚本(Audio Source)的Output挂载AudioMixer的Group Controller文件后,这个音频源就会放出应用了AudioMixer效果的音频。基于此,通过使用代码对AudioMixer的音量进行动态调整,我们就可以实现游戏内动态调整这一个音频源的音量,进而调整全局音量了。

逻辑:

游戏中的音频大致可分为 音效(Sound) 和音乐(Music)两种,因此我们这里就只实现对音效和音乐的设置,同时实现对主音量(Master)的设置

Android 声道全局控制 全局音量控制_游戏_03

逻辑关系

 

 AudioMixer制作

1.create->AudioMixer

Android 声道全局控制 全局音量控制_Android 声道全局控制_04

 2.打开AudioMixer

Android 声道全局控制 全局音量控制_游戏程序_05

这个NewAudioMixer就是新建的AudioMixer

 3.点击Group的加号创建Music、Sound,并设置好层级关系

Android 声道全局控制 全局音量控制_Android 声道全局控制_06

需要拖动,将三个group的层级关系设置为如图所示

 4.在每个group的inspector中右键Volume,并将volume属性暴露出去

Android 声道全局控制 全局音量控制_游戏程序_07

5.如图所示,重命名暴露出去的volume属性

Android 声道全局控制 全局音量控制_游戏程序_08

至此,AudioMixer就基本完成了。将AudioMixer文件夹下的Audio Mixer Group Controller文件拖动到AudioSource的Output中,这个AudioSource放出的音频就都将会应用对应Group的效果。

下面,我们就可以通过动态设置刚才暴露出的volume属性来实现音量设置了。开始愉快的写代码吧~

 

音量设置面板实现

在这里,我简单地假设开发情形如下:有两个场景,每个场景播放不同的背景音乐。那么,就只需要在两个场景中创建两个搭载了AudioSource,并循环播放bgm的空物体,即可实现播放背景音乐的需求

Android 声道全局控制 全局音量控制_游戏_09

如图所示

 而音效的播放,则是在所有场景中都会频繁地使用到,并且播放的音效各不相同,因此需要创建一个用于播放音效的持续单例脚本,使其在场景切换时不被摧毁,并且便于其他脚本调用播放音效的脚本,满足整个游戏的音效播放需求。

至于音量设置,由常理可知音量是全局均可设置的,因此也使用持续单例模式开发,并且用滑动条(sliider)来调节音量。

同时:我们需要实现音量设置面板的如下功能:

1.静音按钮

2.取消静音时回调滑动条到未静音时的值

那么,我们将这一个功能的实现分为两个部分:持续单例AudioManager,以及一个继承了monobehavior的脚本,用于控制其所处场景的音量ui。

 

AudioManager脚本

(我偷懒,就把播放音效功能和AudioManager脚本写到一起了,虽然这样不会有什么大问题,但二者终究不是同一个功能,可能会导致代码比较混乱)

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Audio;

//使用单例模式开发,继承单例父类,使其在场景切换时不销毁
//同时包括音量设置功能和播放音效功能
public class AudioManager : PersistentSingleton<AudioManager>
{
    [SerializeField] AudioMixer audioMixer;
    
    //改变音调
    const float pitchMin = 0.9f;
    const float pitchMax = 1.1f;
    
    //用于判断静音与否
    private bool IsMuteMaster = false;
    private bool IsMuteMusic = false;
    private bool IsMuteSound = false;
    //用于储存静音前的音量
    private float LastMaster;
    private float LastMusic;
    private float LastSound;

    //音量设置
    //Slider on click:调节音量,若静音则取消静音
    public void MasterSldOnClick(GameObject image, Slider slider)
    {
        audioMixer.SetFloat("vMaster", slider.value);
        if (IsMuteMaster == false) return;
        else
        {
            image.SetActive(false);
            IsMuteMaster = false;
        }
    }
    public void MusicSldOnClick(GameObject image, Slider slider)
    {
        audioMixer.SetFloat("vMusic", slider.value);
        if (IsMuteMusic == false) return;
        else
        {
            image.SetActive(false);
            IsMuteMusic = false;
        }
    }
    public void SoundSldOnClick(GameObject image, Slider slider)
    {
        audioMixer.SetFloat("vSound", slider.value);
        if (IsMuteSound == false) return;
        else
        {
            image.SetActive(false);
            IsMuteSound = false;
        };
    }

    //Button on click:若静音则取消静音并回调音量;若未静音则静音并储存音量
    public void MasterBtnOnClick(GameObject image, Slider Master)
    {
        if (IsMuteMaster)
        {
            image.SetActive(false);
            IsMuteMaster = false;
            Master.value = LastMaster;
        }

        else
        {
            image.SetActive(true);
            LastMaster = Master.value;
            Master.value = Master.minValue;
            IsMuteMaster = true;
        }
    }
    public void SoundBtnOnClick(GameObject image, Slider Sound)
    {
        if (IsMuteSound)
        {
            image.SetActive(false);
            IsMuteSound = false;
            Sound.value = Instance.LastSound;
        }

        else
        {
            image.SetActive(true);
            LastSound = Sound.value;
            Sound.value = Sound.minValue;
            IsMuteSound = true;
        }
    }
    public void MusicBtnOnClick(GameObject image, Slider Music)
    {
        if (IsMuteMusic)
        {
            image.SetActive(false);
            IsMuteMusic = false;
            Music.value = LastMusic;
        }

        else
        {
            image.SetActive(true);
            LastMusic = Music.value;
            Music.value = Music.minValue;
            IsMuteMusic = true;
        }
    }
    

    [SerializeField] AudioSource SoundPlayer;
    //播放音效
    public void PlaySound(AudioClip audioClip)
    {
        SoundPlayer.pitch = 1;
        SoundPlayer.PlayOneShot(audioClip);
    }

    // 改变音调,主要用于重复播放的音效
    public void PlayRandomSound(AudioClip audioClip)
    {
        SoundPlayer.pitch = Random.Range(pitchMin , pitchMax );
        SoundPlayer.PlayOneShot(audioClip);
    }

    public void PlayRandomSound(AudioClip[] audioClip)
    {
        PlayRandomSound(audioClip[Random.Range(0, audioClip.Length)]);
    }
}

其中 ,PersistentSingleton为持续泛型单例父类,使脚本挂载的物体不随场景改变而销毁,并且使其他脚本更容易调用本脚本内容。代码见下,详情可见unity单例,实现全局保存/跨场景传输数据

using UnityEngine;

public class PersistentSingleton<T> : MonoBehaviour where T : Component
{
    public static T Instance { get; private set; }

    protected virtual void Awake()
    {
        if (Instance == null)
        {
            Instance = this as T;
        }
        else if (Instance != this)
        {
            Destroy(gameObject);
        }

        DontDestroyOnLoad(gameObject);
    }
}

 写完AudioManager脚本后,将AudioManager挂载到一个第一个场景的空物体上,以便生效(或是挂载到可以调节音量的第一个场景也行)。然后再将刚做好的AudioMixer和用于播放音效的AudioSource拖进脚本里面就行了~

Android 声道全局控制 全局音量控制_c#_10

AudioSource和AudioManager如上图所示
在这种实现方式下,需要把AudioSource放在AudioManager下,防止跨场景销毁。但这是基于只用这一个音源就可满足整个游戏音效需求的大前提下的,可能会有更好的实现方法。

 

AudioManager是全局起作用的,只需要在调节滑动条(slider)时调用三个SldOnClick函数/点击按钮时调用三个BtnOnClick函数,即可以实现全局调节音量的功能。

不过值得注意的是,三个BtnOnClick函数中有一条命令:Sound.value = Instance.LastSound。这条命令不会导致bug,会正常触发SldOnClick,因为SldOnClick的触发条件实际上是On Value Change(所有slider都是),只要值变化就可以触发,而并非一定要click。

 

各场景音量设置界面,以及设置界面脚本

下面,我们需要为每个场景创建音量设置界面,这里只创建一个场景的,因为其他场景也是一样的,实际开发时复制粘贴就行。

1.滑动条(slider)

slider组件比较复杂,虽然不难,但是创建一个功能完备的slider也不是一两句就能说完的,这里时跳过,不懂的小伙伴可以去找些教程,先创建一个能用的滑动条。

Android 声道全局控制 全局音量控制_unity_11

 

2.音量设置界面

Android 声道全局控制 全局音量控制_游戏程序_12

按照上图思路,继续创建音效和音乐的设置界面。

Android 声道全局控制 全局音量控制_c#_13

 3.设置界面脚本

using UnityEngine;
using UnityEngine.UI;

public class TestScene : MonoBehaviour
{
    [Header("-----音量设置条-----")]
    [SerializeField] Slider Master;
    [SerializeField] Slider Music;
    [SerializeField] Slider Sound;

    //SldOnClick:传递参数以触发AudioManager的SldOnClick
    public void MasterSldOnClick(GameObject image)
    {
        AudioManager.Instance.MasterSldOnClick(image, Master);
    }
    public void MusicSldOnClick(GameObject image)
    {
        AudioManager.Instance.MusicSldOnClick(image, Music);
    }
    public void SoundSldOnClick(GameObject image)
    {
        AudioManager.Instance.SoundSldOnClick(image, Sound);
    }

    //BtnOnClick:传递参数以触发AudioManager的BtnOnClick
    public void MasterBtnOnClick(GameObject image)
    {
        AudioManager.Instance.MasterBtnOnClick(image, Master);
    }
    public void SoundBtnOnClick(GameObject image)
    {
        AudioManager.Instance.SoundBtnOnClick(image, Sound);
    }
    public void MusicBtnOnClick(GameObject image)
    {
        AudioManager.Instance.MusicBtnOnClick(image, Music);
    }

}

这个脚本实际上只是起到了传递参数的作用,这么做一方面时因为,多个参数的函数不能挂载到button、slider等组件上,另一方面是因为, AudioManager是持续单例,在某些场景中不是初始存在,无法直接挂载到组件上,需要使用代码动态调用。

将这个脚本挂载到场景中(随便哪都行),并将三个slider挂载到脚本上即可。

Android 声道全局控制 全局音量控制_unity_14

 3.配置按键与滑动条

以Master按键和滑动条为例:

Android 声道全局控制 全局音量控制_c#_15

将刚才设置好的脚本挂到button组件下,并选择MasterBtnOnClick函数,将按键的子物体image挂到这个函数下面

Android 声道全局控制 全局音量控制_游戏程序_16

Sound、Music同上

同样的,给Master的slider挂上 MasterSldOnClick函数,同样的把Master的叉号挂到函数下即可。

至此,整个音量设置界面就做完了

Android 声道全局控制 全局音量控制_c#_17