游戏加入声音后,会大大的增强沉浸感,提高游戏体验,unity提供了强大的声音组件来帮助播放音效,即AudioSystem
使用Audio的最简单的例子
- 将音效文件导入unity工程
- 在场景中创建一个GameObject
- 在Project中找到导入的音效文件,直接拖动到上面创建的GameObject上
- 再创建一个Game Object,添加AudioListener组件来“听”场景中的声音
- 运行例子,我们将听到音效被播放了
在上面的过程中,我们涉及到了3个概念:
AudioListener
可以认为时场景中的“耳朵”,用来‘听’场景中的声音。该组件必须在场景中唯一,如果有多个,会报大量警告
AudioClip
声音资源,我们导入到工程中的声音资源,加载到Unity中后,将以AudioClip的形式存在,我们也可以在代码中访问它,将AudioClip指定给AudioSource,来创建音源。
在Project选中一个声音文件,将在Inspector面板中看到导入配置:
参数 | 含义 |
---|---|
Force To Mono | 是否将多声道音源混合到MonoTrack单省道 |
Normalize | 启用Force To Mono,在混合处理中进行Normalize |
Load In Background | 开启,则执行多线程加载,避免主线程阻塞 |
Ambisonic | 三维声音文件,多用于XR项目 |
Preload Audio Dta | 是否在加载场景时预加载声音。仅对挂在Unity behaviour上的,场景加载完后需要立即播放的音效起作用。如果不选择该选项,则音效在AudioSource.Play(),AudioSource.PlayOneShot()时,或者通过AudioSource.LoadAudioData()加载(相应通过AudioSource.UnloadAudioData())时加载 |
Load Type | 加载方式 |
- Decompress On Load | 加载完成后立即解压,播放时会更加流畅,多用于文件比较小的音效,如果对大声音文件选择该选项,会产生较大内存开销 |
- Compressed In Memory | 加载完后在内存中以压缩格式保存,在播放时在解压,这种模式会造成一定程度的性能开销,因此仅用于较大尺寸的问价来降低内存开销 |
- Streaming | 播放时解码,该模式利用一块较小的内存加载声音文件的一部分,并在播放时解码。解压则是在单独的Streaming线程进行。无论是否有声音播放,该模式都将占用200K内存 |
Compression Format | 压缩格式 |
- PCM | 声音质量做好,尺寸最大,最好用于比较短的音效,CPU占用最低 |
- ADPC | 该格式用于有背景音且需要大量播放的音效,如脚步,枪开火,撞击类声音。该格式比PCM小3.5倍,CPU占用比Vorbis/MP3要小,通常多数音效使用该格式压缩 |
- Vorbis/MP3 | 压缩比比较高,文件尺寸小,但是声音质量低,多用于较长的音效,以及音乐文件。该格式可以通过调节Quality来在压缩比和质量之间平衡,还可以通过采样率SampleRate来进一步压缩 |
Quality | 声音质量,仅对Vorbis/MP3。以多大的压缩比压缩 |
Sample Rate Setting | 采样率设置,仅对Vorbis/MP3 |
- Preserve Sample Rate | 保留原始采样率 |
- Optimize Sample Rate | 根据对音源高频部分的分析,自动进行优化采样率 |
- Override Sample Rate | 手动设置采样率 |
AudioSource
场景中的音源,用来播放AutioClip声音,同时提供参数来告诉unity如何播放AudioClip,主要参数:
AudioClip
参数 | 含义 |
---|---|
AudioClip | 播放的声音 |
Output | 声音输出到哪个混音器,如果没有指定,表示直接交给声卡播放 |
Mute | 静音该音源 |
PlayOnAwake | GameObject OnAwake时,自动播放声音 |
Loop | 是否循环播放 |
Volume | 设置该音源的音量,只影响自己 |
Pitch | 声音播放速度/频率 |
3D声音参数 | |
Doppler Level | 声音多普勒效应等级 |
Spread | 声音传播方向0=360,否则是定向传播 |
MinDistance | 最小传播距离,在该距离内,声音不会衰减 |
MaxDistance | 最大播放距离,超过该距离,将听不到该音源。如果MinDistance,MaxDistance都为0,将不会听到该音源 |
以上并没有列出所有参数,只是列出了常用的几个,更加详细的,看官方文档
Audio Mixer
Unity还提供了更加复杂的混音系统,来帮助我们实现更好的声音效果,即AudioMixer。
Audio Source播放时,可以指定AudioMixer混音器,来进行混音后交给声卡播放,当然也可以不指定,则直接交给声卡播放,但是该情况下,如果播放大量音效声卡会有爆炸杂音。
同时AudioMixer中的Audio MixerGroup允许我们以分组的形式播放和控制声音,如背景音乐组,UI音效组,战斗音效组。典型的,我们可以统一管理组内的音量控制。
还以通过为AudioMixerGroup添加Sender和Receiver效果器,将多个AudioMixerGroup串联起来,构成混音“路径”,来对声音进行一系列的处理,做音效的同学对该概念一定特别熟悉,一般游戏内(相对于音乐游戏),将音效播放分组就够了。
AudioMixer实际上是个资源,我们可以在Project视窗中右键菜单-Create-AudioMixer来创建。
具体的AudioMixer配置,使用,见官方文档。
音效管理器
游戏开发过程中,我们不可能将音效都挂在Game Object上进行播放,更多的情况是根据游戏逻辑的需要,动态地播放音效,所以,我们需要一个音效系统来管理音效的播放,并将Unity的音效复杂性隐藏起来,提供易用接口。
AudioManager : MonoBehavior
AudioManager派生自MonoBehariour,这样我们可以挂在启动场景的对象上进行配置
/// <summary>
/// 声音管理器,负责提供音效播放接口,管理声音播放
/// </summary>
public class AudioManager : MonoBehaviour
{
/// <summary>
/// 单体实例
/// </summary>
public static AudioManager Inst;
/// <summary>
/// 使用的混音器
/// </summary>
public AudioMixer mAudioMixer;
/// <summary>
/// 外部提供的资源加载接口
/// </summary>
private Func<string, AudioClip> mAudioClipLoader;
/// <summary>
/// 音效组管理器
/// </summary>
private AudioMixerGroupManager mGroupMgr = new AudioMixerGroupManager();
/// <summary>
/// 音效播放对象池,用于在指定位置播放音效
/// </summary>
private GameObjectPool mAudioSourceObjectPool = new GameObjectPool();
/// <summary>
/// 下个音效ID
/// </summary>
private long mNextAudioID = 1;
/// <summary>
/// 音效字典,存放所有正在播放的音效
/// </summary>
private Dictionary<long, AudioPlayEntry> mAudioEntryDic = new Dictionary<long, AudioPlayEntry>();
/// <summary>
/// 淡入淡出的音效
/// </summary>
private List<AudioPlayEntry> mAudioEntryInFading = new List<AudioPlayEntry>();
/// <summary>
/// 非循环,等待结束的音效
/// </summary>
private List<AudioPlayEntry> mAudioEntryWaitFinish = new List<AudioPlayEntry>();
/// <summary>
/// 启动
/// </summary>
private void OnStart()
{
DontDestroyOnLoad(this);
Inst = null;
}
/// <summary>
/// 初始化
/// </summary>
/// <param name="resLoader">外部提供资源加载接口</param>
public void Init(Func<string, AudioClip> resLoader)
{
mAudioClipLoader = resLoader;
mAudioSourceObjectPool.Init(gameObject);
}
/// <summary>
/// 更新逻辑
/// </summary>
private void Update()
{
// 更新淡入淡出逻辑
for(int i = 0; i < mAudioEntryInFading.Count;)
{
AudioPlayEntry audioEntry = mAudioEntryInFading[i];
audioEntry.mFadeTimer += Time.deltaTime;
float factor = audioEntry.mFadeTimer * audioEntry.mOneFadeTimeDiv;
if (audioEntry.mFadeState == EAudioFade.In)
{
if (factor >= 1)
{
mAudioEntryInFading.RemoveAt(i);
continue;
}
}
else
{
factor = 1 - factor;
if(factor <= 0)
{
mAudioEntryInFading.RemoveAt(i);
mAudioEntryDic.Remove(audioEntry.mID);
DestroyAudioEntry(ref audioEntry);
continue;
}
}
audioEntry.mAudioSource.volume = factor;
i++;
}
// 更新非循环音效,检查结束
for(int i = 0; i < mAudioEntryWaitFinish.Count;)
{
AudioPlayEntry audioEntry = mAudioEntryWaitFinish[i];
if(audioEntry.mAudioSource.isPlaying == false)
{
mAudioEntryWaitFinish.RemoveAt(i);
mAudioEntryDic.Remove(audioEntry.mID);
DestroyAudioEntry(ref audioEntry);
continue;
}
++i;
}
}
/// <summary>
/// 播放音效
/// </summary>
/// <param name="player">播放音效的Game Object,我们会将AudioSource创建到该对象上,并在播放完成后移除Audio Source</param>
/// <param name="soundName">音效名字</param>
/// <param name="mixerGroup">在那个混音组播放</param>
/// <param name="loop">是否循环</param>
/// <param name="minDist">3D音效最小距离,-1表示非3D音效</param>
/// <param name="maxDist">3D音效最大距离</param>
/// <param name="fadeTime">淡入淡出时间</param>
/// <returns>音效播放ID,用来控制音效</returns>
public long PlaySound(GameObject player, string soundName, string mixerGroup, bool loop, float minDist, float maxDist, float fadeTime)
{
AudioPlayEntry audioEntry = new AudioPlayEntry()
{
mID = mNextAudioID++,
mSoundName = soundName,
mMixerGroup = mixerGroup,
mLoop = loop,
mMinDist = minDist,
mMaxDist = maxDist,
mFadeTime = fadeTime,
mGameObject = player,
mIsPoolGameObject = false
};
if (PlaySound(ref audioEntry))
return audioEntry.mID;
return -1;
}
/// <summary>
/// 播放音效,与上个接口不同在于不提供Game Object,在指定位置播放,所以我们要实现一套GameObject管理器,来动态地创建,以进行Audio Source的播放
/// </summary>
/// <param name="player">播放音效的Game Object,我们会将AudioSource创建到该对象上,并在播放完成后移除Audio Source</param>
/// <param name="soundName">音效名字</param>
/// <param name="mixerGroup">在那个混音组播放</param>
/// <param name="loop">是否循环</param>
/// <param name="minDist">3D音效最小距离,-1表示非3D音效</param>
/// <param name="maxDist">3D音效最大距离</param>
/// /// <param name="fadeTime">淡入淡出时间</param>
/// <returns>音效播放ID,用来控制音效</returns>
public long PlaySound(Vector3 position, string soundName, string mixerGroup, bool loop, float minDist, float maxDist, float fadeTime)
{
AudioPlayEntry audioEntry = new AudioPlayEntry()
{
mID = mNextAudioID++,
mSoundName = soundName,
mMixerGroup = mixerGroup,
mLoop = loop,
mMinDist = minDist,
mMaxDist = maxDist,
mFadeTime = fadeTime,
mGameObject = mAudioSourceObjectPool.Get(),
mIsPoolGameObject = true
};
audioEntry.mGameObject.transform.position = position;
if (PlaySound(ref audioEntry))
return audioEntry.mID;
return -1;
}
/// <summary>
/// 停止播放音效
/// </summary>
/// <param name="id">要停止的音效的ID,为创建时的返回ID</param>
public void StopSound(long id, float fadeTime)
{
AudioPlayEntry audioEntry = new AudioPlayEntry();
if (mAudioEntryDic.TryGetValue(id, out audioEntry) == false)
return;
if (audioEntry.mFadeState == EAudioFade.Out)
return;
if(audioEntry.mFadeState == EAudioFade.In)
{
audioEntry.mFadeTimer = fadeTime * (1 - audioEntry.mFadeTimer * audioEntry.mOneFadeTimeDiv);
}
else
{
audioEntry.mFadeTimer = 0;
}
audioEntry.mOneFadeTimeDiv = 1.0f / fadeTime;
audioEntry.mFadeState = EAudioFade.Out;
}
/// <summary>
/// 设置指定混音组的音量
/// </summary>
/// <param name="mixerGroup"></param>
/// <param name="volume"></param>
public void SetVolume(string mixerGroup, float volume)
{
mGroupMgr.SetVolume(mixerGroup, volume);
}
/// <summary>
/// 获取指定混音组的音量
/// </summary>
/// <param name="mixerGroup"></param>
/// <returns></returns>
public float GetVolume(string mixerGroup)
{
return mGroupMgr.GetVolume(mixerGroup);
}
/// <summary>
/// 播放声音的接口
/// </summary>
/// <param name="audioEntry"></param>
/// <returns></returns>
private bool PlaySound(ref AudioPlayEntry audioEntry)
{
audioEntry.mAudioSource = audioEntry.mGameObject.AddComponent<AudioSource>();
// 加载资源
AudioClip ac = mAudioClipLoader(audioEntry.mSoundName);
if (ac == null)
return false;
// 指定混音组
audioEntry.mAudioSource.outputAudioMixerGroup = mGroupMgr.GetMixerGroup(audioEntry.mMixerGroup);
if (audioEntry.mAudioSource.outputAudioMixerGroup == null)
return false;
// 设置播放参数
audioEntry.mAudioSource.clip = ac;
audioEntry.mAudioSource.loop = audioEntry.mLoop;
audioEntry.mAudioSource.volume = audioEntry.mFadeTime > 0 ? 0 : 1;
if (audioEntry.Is3D)
{
audioEntry.mAudioSource.spatialBlend = 1;
audioEntry.mAudioSource.minDistance = audioEntry.mMinDist;
audioEntry.mAudioSource.maxDistance = audioEntry.mMaxDist;
}
else
{
audioEntry.mAudioSource.spatialBlend = 0;
}
mAudioEntryDic.Add(audioEntry.mID, audioEntry);
// 混音参数初始化
if(audioEntry.mFadeTime > 0)
{
audioEntry.mFadeState = EAudioFade.In;
audioEntry.mFadeTimer = 0;
audioEntry.mOneFadeTimeDiv = 1.0f / audioEntry.mFadeTime;
mAudioEntryInFading.Add(audioEntry);
}
// 非循环,加入检测结束的列表
if(audioEntry.mLoop == false)
{
mAudioEntryWaitFinish.Add(audioEntry);
}
return true;
}
/// <summary>
/// 销毁音效
/// </summary>
/// <param name="audioEntry"></param>
private void DestroyAudioEntry(ref AudioPlayEntry audioEntry)
{
DestroyImmediate(audioEntry.mAudioSource);
if (audioEntry.mIsPoolGameObject)
mAudioSourceObjectPool.Free(audioEntry.mGameObject);
}
}
为了便于管理,我们要记录正在播放的音效
/// <summary>
/// 记录正在播放的音效
/// </summary>
public struct AudioPlayEntry
{
/// <summary>
/// ID
/// </summary>
public long mID;
/// <summary>
/// 音效名字
/// </summary>
public string mSoundName;
/// <summary>
/// 在哪个混音组播放
/// </summary>
public string mMixerGroup;
/// <summary>
/// 是否循环
/// </summary>
public bool mLoop;
/// <summary>
/// 3D音效衰减最小距离
/// </summary>
public float mMinDist;
/// <summary>
/// 3D音效最大衰减距离
/// </summary>
public float mMaxDist;
/// <summary>
/// 淡入淡出时间
/// </summary>
public float mFadeTime;
/// <summary>
/// 声音播放时挂在哪个对象上
/// </summary>
public GameObject mGameObject;
/// <summary>
/// 声音播放对象是否时内部对象池分配的
/// </summary>
public bool mIsPoolGameObject;
/// <summary>
/// 音效播放组件
/// </summary>
public AudioSource mAudioSource;
/// <summary>
/// 淡入淡出配置
/// </summary>
public EAudioFade mFadeState;
/// <summary>
/// 1/mFadeTime
/// </summary>
public float mOneFadeTimeDiv;
/// <summary>
/// 淡入淡出计时器
/// </summary>
public float mFadeTimer;
/// <summary>
/// 是否时3D音效
/// </summary>
public bool Is3D { get { return mMinDist > 0 && mMaxDist > 0; } }
}
对unity的AudioMixerGroup进行封装,易于使用及方便管理:
/// <summary>
/// 混音组
/// </summary>
public class AudioMixerGroupEntry
{
/// <summary>
/// 混音器
/// </summary>
public AudioMixer mAudioMixer;
/// <summary>
/// U3D混音组
/// </summary>
public AudioMixerGroup mAudioMixerGroup;
/// <summary>
/// 音量参数名字
/// </summary>
public string mVolumeName;
/// <summary>
/// 当前音量
/// </summary>
public float mVolume = -1;
/// <summary>
/// 初始化
/// </summary>
/// <param name="mixer">混音器</param>
/// <param name="name">混音组名字</param>
/// <returns></returns>
public bool Init(AudioMixer mixer, string name)
{
mAudioMixer = mixer;
AudioMixerGroup[] groups = mAudioMixer.FindMatchingGroups(name);
if (groups == null || groups.Length == 0)
{
// Erro log
return false;
}
mAudioMixerGroup = groups[0];
mVolumeName = "Vol_" + name;
return true;
}
/// <summary>
/// 设置音量
/// </summary>
/// <param name="v"></param>
public void SetVolume(float v)
{
if (mAudioMixerGroup == null || mVolume == v)
return;
mVolume = Mathf.Clamp01(v);
float dbVolume = mVolume > 0 ? AudioMixerGroupManager.DBMin + AudioMixerGroupManager.DBRange * v : -80;
mAudioMixer.SetFloat(mVolumeName, dbVolume);
}
/// <summary>
/// 获取音量
/// </summary>
/// <returns></returns>
public float GetVolume()
{
if (mAudioMixerGroup == null)
return -1;
if (mVolume < 0)
{
float dbVolume = 0;
mAudioMixer.GetFloat(mVolumeName, out dbVolume);
mVolume = (AudioMixerGroupManager.DBMax - dbVolume) / AudioMixerGroupManager.DBRange;
}
return mVolume;
}
}
管理我们的混音组:
/// <summary>
/// 混音组管理器
/// </summary>
public class AudioMixerGroupManager
{
/// <summary>
/// 最小分贝,小于该值则按照-80分贝算
/// </summary>
public static float DBMin = -30;
/// <summary>
/// 最大分贝
/// </summary>
public static float DBMax = 20;
/// <summary>
/// 分贝范围
/// </summary>
public static float DBRange = DBMax - DBMin;
/// <summary>
/// 所有的混音组的字典
/// </summary>
public Dictionary<string, AudioMixerGroupEntry> mMixerGroups = new Dictionary<string, AudioMixerGroupEntry>();
/// <summary>
/// 查找一个混音组
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public AudioMixerGroup GetMixerGroup(string name)
{
return GetMixerGroupEntry(name).mAudioMixerGroup;
}
/// <summary>
/// 查找混音组
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public AudioMixerGroupEntry GetMixerGroupEntry(string name)
{
AudioMixerGroupEntry en = null;
if (mMixerGroups.TryGetValue(name, out en) == false)
{
en = new AudioMixerGroupEntry();
en.Init(AudioManager.Inst.mAudioMixer, name);
mMixerGroups.Add(name, en);
}
return en;
}
/// <summary>
/// 设置音量
/// </summary>
/// <param name="name"></param>
/// <param name="volume"></param>
public void SetVolume(string name, float volume)
{
GetMixerGroupEntry(name).SetVolume(volume);
}
/// <summary>
/// 获取音量
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
public float GetVolume(string name)
{
return GetMixerGroupEntry(name).GetVolume();
}
}
音效播放必须要挂在GameObject上,但有时接口的使用者可能只是想在一个指定的位置播放,并没有对象,所以我们内部要提供对象池,来解决这种情况:
/// <summary>
/// GameObject 池
/// </summary>
public class GameObjectPool
{
GameObject mRoot;
GameObject mEnabled;
GameObject mDisabled;
int mTotal;
Stack<GameObject> mGOStack = new Stack<GameObject>();
/// <summary>
/// 初始化
/// </summary>
/// <param name="root"></param>
public void Init(GameObject root)
{
mRoot = new GameObject("GOPool");
mRoot.transform.parent = root.transform;
mEnabled = new GameObject("Enabled");
mEnabled.transform.parent = mRoot.transform;
mDisabled = new GameObject("Disabled");
mDisabled.transform.parent = mRoot.transform;
}
/// <summary>
/// 获取对象
/// </summary>
/// <returns></returns>
public GameObject Get()
{
if(mGOStack.Count == 0)
{
mTotal++;
GameObject go = new GameObject("GOPool:" + mTotal.ToString());
return go;
}
return mGOStack.Pop();
}
/// <summary>
/// 归还对象
/// </summary>
/// <param name="go"></param>
public void Free(GameObject go)
{
mGOStack.Push(go);
}
}
该音效系统没有处理异步加载音效逻辑,各位看官,可以自己尝试解决