在实际游戏开发中,音效既是一个相对独立的部分,又与其他游戏逻辑密切关联。也就是说,与音效相关的代码会插入很多细节代码中。
而且在音效非常丰富的情况下,如果每一个游戏模块都单独播放音效,那么可能会带来一些问题。例如,Audio Source组件很多,但大部分限制,同时播放的音效太多会显得混乱。
成熟的技术开发思路是:如果音效不多、没有造成问题,则完全可以简单处理;如果音效已经引起了代码的混乱和性能问题,就有必要统一管理所有的音源和音效,也就是设计一个易用的音频管理器。有了音频管理器,所有的Audio Source组件都会统一创建,而所有音效播放的需求都要通过调用音频管理器的方法简介实现。
音频管理器有很多设计思路,其中一种比较简洁的思路是,事先指定游戏中最多同时播放多少个音频,然后创建若干个音源。例如,最多播放8个音频,那么就创建8个Audio Source组件,这8个组件可以看作8个频道。需要播放音频时,只要找到任意一个空闲的频道播放即可;而如果8个频道都正在播放,那么就可以用某种策略替换音频(如将播放时间最早的音频替换成新的音频)。用这种简单的思路创建音频管理器的代码如下
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//音频管理器
public class AudioManager : MonoBehaviour
{
// 整个游戏中,总的音源数量
private const int AUDIO_CHANNEL_NUM = 8;
private struct CHANNEL
{
public AudioSource channel;
public float keyOnTime; //记录最近一次播放音乐的时刻
};
private CHANNEL[] m_channels;
void Awake()
{
m_channels = new CHANNEL[AUDIO_CHANNEL_NUM];
for (int i = 0; i < AUDIO_CHANNEL_NUM; i++)
{
//每个频道对应一个音源
m_channels[i].channel = gameObject.AddComponent<AudioSource>();
m_channels[i].keyOnTime = 0;
}
}
//公开方法:播放一次,参数为音频片段、音量、左右声道、速度
//这个方法主要用于音效,因此考虑了音效顶替的逻辑
public int PlayOneShot(AudioClip clip, float volume, float pan, float pitch = 1.0f)
{
for (int i = 0; i < m_channels.Length; i++)
{
//如果正在播放同一个片段,而且刚刚才开始,则直接退出函数
if (m_channels[i].channel.isPlaying &&
m_channels[i].channel.clip == clip &&
m_channels[i].keyOnTime >= Time.time - 0.03f)
return -1;
}
//遍历所有频道,如果有频道空闲直接播放新音频,并退出
//如果没有空闲频道,先找到最开始播放的频道(oldest),稍后使用
int oldest = -1;
float time = 10000000.0f;
for (int i = 0; i < m_channels.Length; i++)
{
if (m_channels[i].channel.loop==false &&
m_channels[i].channel.isPlaying &&
m_channels[i].keyOnTime < time)
{
oldest = i;
time=m_channels[i].keyOnTime;
}
if (!m_channels[i].channel.isPlaying)
{
m_channels[i].channel.clip=clip;
m_channels[i].channel.volume=volume;
m_channels[i].channel.pitch=pitch;
m_channels[i].channel.panStereo=pan;
m_channels[i].channel.loop = false;
m_channels[i].channel.Play();
m_channels[i].keyOnTime = Time.time;
return i;
}
}
//运行到这里说明没有空闲频道。让新的音频顶替最早播出的音频
if(oldest>=0)
{
m_channels[oldest].channel.clip = clip;
m_channels[oldest].channel.volume = volume;
m_channels[oldest].channel.pitch = pitch;
m_channels[oldest].channel.panStereo = pan;
m_channels[oldest].channel.loop = false;
m_channels[oldest].channel.Play();
m_channels[oldest].keyOnTime = Time.time;
return oldest;
}
return -1;
}
//公开方法:循环播放,用于播放长时间的背景音乐,处理方式相对简单一些
public int PlayLoop(AudioClip clip, float volume, float pan, float pitch = 1.0f)
{
for(int i = 0; i < m_channels.Length; i++)
{
if (!m_channels[i].channel.isPlaying)
{
m_channels[i].channel.clip = clip;
m_channels[i].channel.volume = volume;
m_channels[i].channel.pitch = pitch;
m_channels[i].channel.panStereo = pan;
m_channels[i].channel.loop = true;
m_channels[i].channel.Play();
m_channels[i].keyOnTime = Time.time;
return i;
}
}
return -1;
}
//公开方法:停止所有音频
public void StopAll()
{
foreach(CHANNEL channel in m_channels)
channel.channel.Stop();
}
//公开方法:根据频道ID停止音频
public void Stop(int id)
{
if (id>= 0&& id < m_channels.Length){
m_channels[id].channel.Stop();
}
}
}
以上代码可以作为创建音频管理器的一种思路参考。
实际上,根据游戏类型的不同,音频管理器的创建思路也有区别。例如,在很多3D游戏中,需要考虑音效播放的空间位置(目的是营造真实感),这是统一创建音源就不是很合适了